Search code examples
c#jsonserializationtypedescriptordotnetify

How to serialize runtime added "properties" to Json


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).


Solution

  • 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.