问题
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).
回答1:
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.
回答2:
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
来源:https://stackoverflow.com/questions/46888732/how-to-serialize-runtime-added-properties-to-json