Skip to content

[WIP] Allows runtime overriding of certain serialization attributes #46

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions JSONAPI.Tests/Data/OverrideSerializationAttributesTest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"posts": {
"id": "2",
"title": "How to fry an egg",
"links": {
"author": "5"
}
},
"linked": {
"users": [
{
"id": "5",
"name": "Bob"
}
]
}
}
3 changes: 3 additions & 0 deletions JSONAPI.Tests/JSONAPI.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,9 @@
<None Include="Data\NonStandardIdTest.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Include="Data\OverrideSerializationAttributesTest.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Include="Data\SerializerIntegrationTest.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
Expand Down
28 changes: 27 additions & 1 deletion JSONAPI.Tests/Json/LinkTemplateTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
}
}
110 changes: 88 additions & 22 deletions JSONAPI/Core/MetadataManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ISet<System.Attribute>> AttributeOverrides
= new Lazy<ISet<System.Attribute>>(
() => new HashSet<System.Attribute>()
);
}

private class ModelMetadata
{
public Lazy<IDictionary<PropertyInfo, PropertyMetadata>> PropertyMetadata
= new Lazy<IDictionary<PropertyInfo, PropertyMetadata>>(
() => new Dictionary<PropertyInfo, PropertyMetadata>()
);
}

#region Singleton pattern

private static readonly MetadataManager instance = new MetadataManager();
Expand All @@ -26,8 +43,8 @@ public static MetadataManager Instance

#endregion

private readonly ConditionalWeakTable<object, Dictionary<string, object>> cwt
= new ConditionalWeakTable<object, Dictionary<string, object>>();
private readonly ConditionalWeakTable<object, ModelMetadata> cwt
= new ConditionalWeakTable<object, ModelMetadata>();

/*
internal void SetDeserializationMetadata(object deserialized, Dictionary<string, object> meta)
Expand All @@ -36,32 +53,40 @@ internal void SetDeserializationMetadata(object deserialized, Dictionary<string,
}
*/

internal void SetMetaForProperty(object deserialized, PropertyInfo prop, object value)
private ModelMetadata GetMetadataForModel(object model)
{
Dictionary<string, object> meta;
if (!cwt.TryGetValue(deserialized, out meta))
ModelMetadata meta;
lock(cwt)
{
meta = new Dictionary<string, object>();
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<String, object> DeserializationMetadata(object deserialized)
private PropertyMetadata GetMetadataForProperty(object model, PropertyInfo prop)
{
Dictionary<string, object> retval;
if (cwt.TryGetValue(deserialized, out retval))
{
return retval;
}
else
ModelMetadata mmeta = GetMetadataForModel(model);
IDictionary<PropertyInfo, PropertyMetadata> 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;
}

/// <summary>
Expand All @@ -75,8 +100,49 @@ internal Dictionary<String, object> 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;
}

/// <summary>
/// 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.
/// </summary>
/// <param name="model">The model object that is to be serialized, for which you want to change serialization behavior.</param>
/// <param name="prop">The property for which to override attributes.</param>
/// <param name="attrs">One or more attribute instances that will override the declared behavior.</param>
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<System.Attribute> GetPropertyAttributeOverrides(object model, PropertyInfo prop)
{
return this.GetMetadataForProperty(model, prop).AttributeOverrides.Value;
}

}
}
6 changes: 4 additions & 2 deletions JSONAPI/Json/JsonApiFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down