diff --git a/JSONAPI.Tests/Data/OverrideSerializationAttributesTest.json b/JSONAPI.Tests/Data/OverrideSerializationAttributesTest.json new file mode 100644 index 00000000..074bf20a --- /dev/null +++ b/JSONAPI.Tests/Data/OverrideSerializationAttributesTest.json @@ -0,0 +1,17 @@ +{ + "posts": { + "id": "2", + "title": "How to fry an egg", + "links": { + "author": "5" + } + }, + "linked": { + "users": [ + { + "id": "5", + "name": "Bob" + } + ] + } +} \ No newline at end of file diff --git a/JSONAPI.Tests/JSONAPI.Tests.csproj b/JSONAPI.Tests/JSONAPI.Tests.csproj index d61bedf1..e6ebb4ba 100644 --- a/JSONAPI.Tests/JSONAPI.Tests.csproj +++ b/JSONAPI.Tests/JSONAPI.Tests.csproj @@ -109,6 +109,9 @@ Always + + Always + Always diff --git a/JSONAPI.Tests/Json/LinkTemplateTests.cs b/JSONAPI.Tests/Json/LinkTemplateTests.cs index e5eb0d83..3dff8f47 100644 --- a/JSONAPI.Tests/Json/LinkTemplateTests.cs +++ b/JSONAPI.Tests/Json/LinkTemplateTests.cs @@ -61,7 +61,33 @@ public void GetResourceWithLinkTemplateRelationship() var expected = JsonHelpers.MinifyJson(File.ReadAllText("LinkTemplateTest.json")); var output = Encoding.ASCII.GetString(stream.ToArray()); Trace.WriteLine(output); - Assert.AreEqual(output.Trim(), expected); + Assert.AreEqual(expected,output.Trim()); + } + + [TestMethod] + [DeploymentItem(@"Data\OverrideSerializationAttributesTest.json")] + public void OverrideSerializationAttributesTest() + { + // Arrange + var formatter = new JsonApiFormatter + ( + new JSONAPI.Core.PluralizationService() + ); + var stream = new MemoryStream(); + + // Act + JSONAPI.Core.MetadataManager.Instance.SetPropertyAttributeOverrides( + ThePost, typeof(Post).GetProperty("Author"), + new SerializeAs(SerializeAsOptions.Ids), + new IncludeInPayload(true) + ); + formatter.WriteToStreamAsync(typeof(Post), ThePost, stream, null, null); + + // Assert + var expected = JsonHelpers.MinifyJson(File.ReadAllText("OverrideSerializationAttributesTest.json")); + var output = Encoding.ASCII.GetString(stream.ToArray()); + Trace.WriteLine(output); + Assert.AreEqual(expected, output.Trim()); } } } diff --git a/JSONAPI/Core/MetadataManager.cs b/JSONAPI/Core/MetadataManager.cs index d1541d52..2dbe5bd4 100644 --- a/JSONAPI/Core/MetadataManager.cs +++ b/JSONAPI/Core/MetadataManager.cs @@ -10,6 +10,23 @@ namespace JSONAPI.Core { public sealed class MetadataManager { + private class PropertyMetadata + { + public bool PresentInJson { get; set; } // only meaningful for incoming/deserialized models! + public Lazy> AttributeOverrides + = new Lazy>( + () => new HashSet() + ); + } + + private class ModelMetadata + { + public Lazy> PropertyMetadata + = new Lazy>( + () => new Dictionary() + ); + } + #region Singleton pattern private static readonly MetadataManager instance = new MetadataManager(); @@ -26,8 +43,8 @@ public static MetadataManager Instance #endregion - private readonly ConditionalWeakTable> cwt - = new ConditionalWeakTable>(); + private readonly ConditionalWeakTable cwt + = new ConditionalWeakTable(); /* internal void SetDeserializationMetadata(object deserialized, Dictionary meta) @@ -36,32 +53,40 @@ internal void SetDeserializationMetadata(object deserialized, Dictionary meta; - if (!cwt.TryGetValue(deserialized, out meta)) + ModelMetadata meta; + lock(cwt) { - meta = new Dictionary(); - cwt.Add(deserialized, meta); + if (!cwt.TryGetValue(model, out meta)) + { + meta = new ModelMetadata(); + cwt.Add(model, meta); + } } - if (!meta.ContainsKey(prop.Name)) // Temporary fix for non-standard Id reprecussions...this internal implementation will change soon anyway. - meta.Add(prop.Name, value); - + return meta; } - - internal Dictionary DeserializationMetadata(object deserialized) + private PropertyMetadata GetMetadataForProperty(object model, PropertyInfo prop) { - Dictionary retval; - if (cwt.TryGetValue(deserialized, out retval)) - { - return retval; - } - else + ModelMetadata mmeta = GetMetadataForModel(model); + IDictionary pmetadict = mmeta.PropertyMetadata.Value; + PropertyMetadata pmeta; + lock (pmetadict) { - //TODO: Throw an exception here? If you asked for metadata for an object and it's not found, something has probably gone pretty badly wrong! - return null; + if (!pmetadict.TryGetValue(prop, out pmeta)) + { + pmeta = new PropertyMetadata(); + pmetadict.Add(prop, pmeta); + } } + return pmeta; + } + + internal void SetPropertyWasPresent(object deserialized, PropertyInfo prop, bool value) + { + PropertyMetadata pmeta = GetMetadataForProperty(deserialized, prop); + pmeta.PresentInJson = value; } /// @@ -75,8 +100,49 @@ internal Dictionary DeserializationMetadata(object deserialized) [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool PropertyWasPresent(object deserialized, PropertyInfo prop) { - object throwaway; - return this.DeserializationMetadata(deserialized).TryGetValue(prop.Name, out throwaway); + return this.GetMetadataForProperty(deserialized, prop).PresentInJson; } + + /// + /// Set different serialization attributes at runtime than those that were declared on + /// a property at compile time. E.g., if you declared a relationship property with + /// [SerializeAs(SerializeAsOptions.Link)] but you want to change that to + /// SerializeAsOptions.Ids when you are transmitting only one object, you can do: + /// + /// MetadataManager.SetPropertyAttributeOverrides( + /// theModelInstance, theProperty, + /// new SerializeAsAttribute(SerializeAsOptions.Ids) + /// ); + /// + /// Further, if you want to also include the related objects in the serialized document: + /// + /// MetadataManager.SetPropertyAttributeOverrides( + /// theModelInstance, theProperty, + /// new SerializeAs(SerializeAsOptions.Ids), + /// new IncludeInPayload(true) + /// ); + /// + /// Calling this function resets all overrides, so calling it twice will result in only + /// the second set of overrides being applied. At present, the order of the attributes + /// is not meaningful. + /// + /// The model object that is to be serialized, for which you want to change serialization behavior. + /// The property for which to override attributes. + /// One or more attribute instances that will override the declared behavior. + public void SetPropertyAttributeOverrides(object model, PropertyInfo prop, params System.Attribute[] attrs) + { + var aoverrides = this.GetMetadataForProperty(model, prop).AttributeOverrides.Value; + lock (aoverrides) + { + aoverrides.Clear(); + aoverrides.UnionWith(attrs); + } + } + + internal IEnumerable GetPropertyAttributeOverrides(object model, PropertyInfo prop) + { + return this.GetMetadataForProperty(model, prop).AttributeOverrides.Value; + } + } } diff --git a/JSONAPI/Json/JsonApiFormatter.cs b/JSONAPI/Json/JsonApiFormatter.cs index a87d9674..40721aa0 100644 --- a/JSONAPI/Json/JsonApiFormatter.cs +++ b/JSONAPI/Json/JsonApiFormatter.cs @@ -252,6 +252,8 @@ protected void Serialize(object value, Stream writeStream, JsonWriter writer, Js SerializeAsOptions sa = SerializeAsOptions.Ids; object[] attrs = prop.GetCustomAttributes(true); + // aha...this way the overrides will be applied last! + attrs = attrs.Concat(MetadataManager.Instance.GetPropertyAttributeOverrides(value, prop)).ToArray(); foreach (object attr in attrs) { @@ -656,7 +658,7 @@ public object Deserialize(Type objectType, Stream readStream, JsonReader reader, prop.SetValue(retval, propVal, null); // Tell the MetadataManager that we deserialized this property - MetadataManager.Instance.SetMetaForProperty(retval, prop, true); + MetadataManager.Instance.SetPropertyWasPresent(retval, prop, true); // pop the value off the reader, so we catch the EndObject token below!. reader.Read(); @@ -805,7 +807,7 @@ private void DeserializeLinkedResources(object obj, Stream readStream, JsonReade } // Tell the MetadataManager that we deserialized this property - MetadataManager.Instance.SetMetaForProperty(obj, prop, true); + MetadataManager.Instance.SetPropertyWasPresent(obj, prop, true); } else reader.Skip();