3

I implemented the possibility to add "properties" at runtime to objects with special SystemComponent.PropertyDescriptor-s.

Due to the fact that these properties are only accessible with the ComponentModel.TypeDescriptor and not via Reflection, the properties work well in WPF environment but not with Serialization.

This is because of all JSON serializers, that I know, use reflection on the type. I analyzed Newtonsoft.Json, System.Json, System.Web.Script.JavaScriptSerializer, System.Runtime.Serialization.Json.

I don't think I can use any of these serializers because none of these allow modifying the retrieval of the properties on an instance (e.g. ContractResolver not possible).

Is there any way to make the JSON serialization work with one of those serializers? Maybe by special configuration, overriding certain methods on the Serializer or similar? Is there another serializer available that fulfills this requirement?

Background:

The idea of the runtime properties is based on this blog entry.

The serialization requirement comes from using dotNetify that serializes the viewmodels to send them to the client.

Currently, I made a fork of dotnetify and made a temporary workaround for the serialization by partially serializing with Newtonsoft.Json and a recursive helper. (You can look at the diff if interested in it: the Fork).

Tw Bert
  • 3,659
  • 20
  • 28
Felix Keil
  • 2,344
  • 1
  • 25
  • 27
  • 1) *e.g. ContractResolver not possible* -- I don't see why a custom Json.NET contract resolver couldn't work. You can definitely override [`CreateProperties()`](https://www.newtonsoft.com/json/help/html/M_Newtonsoft_Json_Serialization_DefaultContractResolver_CreateProperties.htm) and add custom synthetic properties. 2) Can you point to minimal implementation of custom properties that could be used for testing purposes in, say, a console app? – dbc Oct 23 '17 at 17:09
  • @dbc The properties are only accessible on the object via TypeDescriptor. You can't get them from Type. I will provide sample implementation later. Too bad that there is no CreateProperties(Object object, MemberSerialization memberSerialization). Everything in the resolver relays on the Type. – Felix Keil Oct 23 '17 at 17:23
  • @dbc here is [Github repository](https://github.com/KeilFelix/DynamicProperties) of the DynamicProperties (minimal classes, I have built more around this with Rx Stuff) and a Consoleapp to try out the serialization – Felix Keil Oct 27 '17 at 01:27

2 Answers2

4

One possibility would be to create a custom ContractResolver that, when serializing a specific object of type TTarget, adds a synthetic ExtensionDataGetter that returns, for the specified target, an IEnumerable<KeyValuePair<Object, Object>> of the properties specified in its corresponding DynamicPropertyManager<TTarget>.

First, define the contract resolver as follows:

public class DynamicPropertyContractResolver<TTarget> : DefaultContractResolver
{
    readonly DynamicPropertyManager<TTarget> manager;
    readonly TTarget target;

    public DynamicPropertyContractResolver(DynamicPropertyManager<TTarget> manager, TTarget target)
    {
        if (manager == null)
            throw new ArgumentNullException();
        this.manager = manager;
        this.target = target;
    }

    protected override JsonObjectContract CreateObjectContract(Type objectType)
    {
        var contract = base.CreateObjectContract(objectType);

        if (objectType == typeof(TTarget))
        {
            if (contract.ExtensionDataGetter != null || contract.ExtensionDataSetter != null)
                throw new JsonSerializationException(string.Format("Type {0} already has extension data.", typeof(TTarget)));
            contract.ExtensionDataGetter = (o) =>
                {
                    if (o == (object)target)
                    {
                        return manager.Properties.Select(p => new KeyValuePair<object, object>(p.Name, p.GetValue(o)));
                    }
                    return null;
                };
            contract.ExtensionDataSetter = (o, key, value) =>
                {
                    if (o == (object)target)
                    {
                        var property = manager.Properties.Where(p => string.Equals(p.Name, key, StringComparison.OrdinalIgnoreCase)).SingleOrDefault();
                        if (property != null)
                        {
                            if (value == null || value.GetType() == property.PropertyType)
                                property.SetValue(o, value);
                            else
                            {
                                var serializer = JsonSerializer.CreateDefault(new JsonSerializerSettings { ContractResolver = this });
                                property.SetValue(o, JToken.FromObject(value, serializer).ToObject(property.PropertyType, serializer));
                            }
                        }
                    }
                };
            contract.ExtensionDataValueType = typeof(object);
        }

        return contract;
    }
}

Then serialize your object as follows:

var obj = new object();

//Add prop to instance
int propVal = 0; 
var propManager = new DynamicPropertyManager<object>(obj);
propManager.Properties.Add(
    DynamicPropertyManager<object>.CreateProperty<object, int>(
    "Value", t => propVal, (t, y) => propVal = y, null));

propVal = 3;

var settings = new JsonSerializerSettings
{
    ContractResolver = new DynamicPropertyContractResolver<object>(propManager, obj),
};

//Serialize object here
var json = JsonConvert.SerializeObject(obj, Formatting.Indented, settings);

Console.WriteLine(json);

Which outputs, as required,

{"Value":3}

Obviously this could be extended to serializing a graph of objects with dynamic properties by passing a collection of dynamic property managers and targets to an enhanced DynamicPropertyContractResolver<TTarget>. The basic idea, of creating a synthetic ExtensionDataGetter (and ExtensionDataSetter for deserialization) can work as long as the contract resolver has some mechanism for mapping from a target being (de)serialized to its DynamicPropertyManager.

Limitation: if the TTarget type already has an extension data member, this will not work.

dbc
  • 104,963
  • 20
  • 228
  • 340
  • Awesome, thank you! This got me in the right direction. The ExtensionDataGetter and Setter have scope to the object again! :) I have made a version that just uses the System.ComponentModel.TypeDescriptor and has no dependency to DynamicProperties. It is commited to the repo. – Felix Keil Oct 27 '17 at 08:30
4

Thanks to dbc's answer my solution is a ContractResolver that uses System.ComponentModel.TypeDescriptor

public class TypeDescriptorContractResolver : DefaultContractResolver
{

    public TypeDescriptorContractResolver()
    {
    }

    protected override JsonObjectContract CreateObjectContract(Type objectType)
    {
        var contract = base.CreateObjectContract(objectType);


        if (contract.ExtensionDataGetter != null || contract.ExtensionDataSetter != null)
            throw new JsonSerializationException(string.Format("Type {0} already has extension data.", objectType));

        contract.ExtensionDataGetter = (o) =>
        {
            return TypeDescriptor.GetProperties(o).OfType<PropertyDescriptor>().Select(p => new KeyValuePair<object, object>(p.Name, p.GetValue(o)));
        };

        contract.ExtensionDataSetter = (o, key, value) =>
        {
            var property = TypeDescriptor.GetProperties(o).OfType<PropertyDescriptor>().Where(p => string.Equals(p.Name, key, StringComparison.OrdinalIgnoreCase)).SingleOrDefault();
            if (property != null)
            {
                if (value == null || value.GetType() == property.PropertyType)
                    property.SetValue(o, value);
                else
                {
                    var serializer = JsonSerializer.CreateDefault(new JsonSerializerSettings { ContractResolver = this });
                    property.SetValue(o, JToken.FromObject(value, serializer).ToObject(property.PropertyType, serializer));
                }
            }
        };
        contract.ExtensionDataValueType = typeof(object);

        return contract;
    }
}

I have posted this, because it is a more general approach without any Dependencies to the DynamicProperties

Felix Keil
  • 2,344
  • 1
  • 25
  • 27