How to serialize runtime added “properties” to Json

孤者浪人 提交于 2019-12-06 01:49:32

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.

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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!