我有一个类EntityBase
,它派生自DynamicObject
而没有空的默认构造函数。
// this is not the actual type but a mock to test the behavior with
public class EntityBase : DynamicObject
{
public string EntityName { get; private set; }
private readonly Dictionary<string, object> values = new Dictionary<string, object>();
public EntityBase(string entityName)
{
this.EntityName = entityName;
}
public virtual object this[string fieldname]
{
get
{
if (this.values.ContainsKey(fieldname))
return this.values[fieldname];
return null;
}
set
{
if (this.values.ContainsKey(fieldname))
this.values[fieldname] = value;
else
this.values.Add(fieldname, value);
}
}
public override IEnumerable<string> GetDynamicMemberNames()
{
return this.values.Keys.ToList();
}
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
result = this[binder.Name];
return true;
}
public override bool TrySetMember(SetMemberBinder binder, object value)
{
this[binder.Name] = value;
return true;
}
}
JSON我想反序列化看起来像这样:
{'Name': 'my first story', 'ToldByUserId': 255 }
EntityBase
既没有Name
也没有ToldByUserId
属性。它们应该添加到DynamicObject。
如果我让DeserializeObject
像这样创建对象,一切都按预期工作:
var story = JsonConvert.DeserializeObject<EntityBase>(JSON);
但由于我没有一个空的默认构造函数而且无法更改我为CustomCreationConverter而去的课程:
public class StoryCreator : CustomCreationConverter<EntityBase>
{
public override EntityBase Create(Type objectType)
{
return new EntityBase("Story");
}
}
但是
var stroy = JsonConvert.DeserializeObject<EntityBase>(JSON, new StoryCreator());
引发
无法将JSON对象填充到类型&#39; DynamicObjectJson.EntityBase&#39;上。 路径&#39;名称&#39;,第1行,第8位。
似乎DeserializeObject
调用PopulateObject
创建的对象CustomCreationConverter
。当我尝试手动执行此操作时,错误保持不变
JsonConvert.PopulateObject(JSON, new EntityBase("Story"));
我进一步假设PopulateObject
不会检查目标类型是否来自DynamicObject
,因此不会回退到TrySetMember
。
请注意,我对EntityBase
类型定义没有影响,它来自外部库并且无法更改。
任何见解都将受到高度赞赏!
修改:添加了一个示例:https://dotnetfiddle.net/EGOCFU
答案 0 :(得分:1)
您似乎偶然发现Json.NET支持反序列化动态对象(定义为生成JsonDynamicContract
的对象)的一些错误或限制:
不存在对参数化构造函数的支持。即使其中一个标有[JsonConstructor]
,也不会被使用。
此处JsonSerializerInternalReader.CreateDynamic()
似乎完全没有预加载所有属性的必要逻辑。与JsonSerializerInternalReader.CreateNewObject()
比较,表明需要什么。
由于逻辑看起来相当精细,这可能是一个限制而不是一个错误。实际上有closed issue #47表示它没有实现:
添加此功能会有相当多的工作。如果您添加拉取请求,欢迎提交拉取请求。
Json.NET无法填充预先存在的动态对象。与常规对象(生成JsonObjectContract
的对象)不同,构造和填充的逻辑完全包含在前面提到的JsonSerializerInternalReader.CreateDynamic()
中。
我不明白为什么用相当简单的代码重构无法实现这一点。你可以submit an issue要求这个。如果实施此操作,您的StoryCreator
将按原样运作。
在没有#1或#2的情况下,可以创建一个custom JsonConverter
,其逻辑大致在JsonSerializerInternalReader.CreateDynamic()
上建模,调用指定的创建方法然后填充动态和非动态属性,像这样:
public class EntityBaseConverter : ParameterizedDynamicObjectConverterBase<EntityBase>
{
public override EntityBase CreateObject(JObject jObj, Type objectType, JsonSerializer serializer, ICollection<string> usedParameters)
{
var entityName = jObj.GetValue("EntityName", StringComparison.OrdinalIgnoreCase);
if (entityName != null)
{
usedParameters.Add(((JProperty)entityName.Parent).Name);
}
var entityNameString = entityName == null ? "" : entityName.ToString();
if (objectType == typeof(EntityBase))
{
return new EntityBase(entityName == null ? "" : entityName.ToString());
}
else
{
return (EntityBase)Activator.CreateInstance(objectType, new object [] { entityNameString });
}
}
}
public abstract class ParameterizedDynamicObjectConverterBase<T> : JsonConverter where T : DynamicObject
{
public override bool CanConvert(Type objectType) { return typeof(T).IsAssignableFrom(objectType); } // Or possibly return objectType == typeof(T);
public abstract T CreateObject(JObject jObj, Type objectType, JsonSerializer serializer, ICollection<string> usedParameters);
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
// Logic adapted from JsonSerializerInternalReader.CreateDynamic()
// https://github.com/JamesNK/Newtonsoft.Json/blob/master/Src/Newtonsoft.Json/Serialization/JsonSerializerInternalReader.cs#L1751
// By James Newton-King https://github.com/JamesNK
var contract = (JsonDynamicContract)serializer.ContractResolver.ResolveContract(objectType);
if (reader.TokenType == JsonToken.Null)
return null;
var jObj = JObject.Load(reader);
var used = new HashSet<string>();
var obj = CreateObject(jObj, objectType, serializer, used);
foreach (var jProperty in jObj.Properties())
{
var memberName = jProperty.Name;
if (used.Contains(memberName))
continue;
// first attempt to find a settable property, otherwise fall back to a dynamic set without type
JsonProperty property = contract.Properties.GetClosestMatchProperty(memberName);
if (property != null && property.Writable && !property.Ignored)
{
var propertyValue = jProperty.Value.ToObject(property.PropertyType, serializer);
property.ValueProvider.SetValue(obj, propertyValue);
}
else
{
object propertyValue;
if (jProperty.Value.Type == JTokenType.Null)
propertyValue = null;
else if (jProperty.Value is JValue)
// Primitive
propertyValue = ((JValue)jProperty.Value).Value;
else
propertyValue = jProperty.Value.ToObject<IDynamicMetaObjectProvider>(serializer);
// Unfortunately the following is not public!
// contract.TrySetMember(obj, memberName, propertyValue);
// So we have to duplicate the logic of what Json.NET has already done.
CallSiteCache.SetValue(memberName, obj, propertyValue);
}
}
return obj;
}
public override bool CanWrite { get { return false; } }
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
internal static class CallSiteCache
{
// Adapted from the answer to
// https://stackoverflow.com/questions/12057516/c-sharp-dynamicobject-dynamic-properties
// by jbtule, https://stackoverflow.com/users/637783/jbtule
// And also
// https://github.com/mgravell/fast-member/blob/master/FastMember/CallSiteCache.cs
// by Marc Gravell, https://github.com/mgravell
private static readonly Dictionary<string, CallSite<Func<CallSite, object, object, object>>> setters
= new Dictionary<string, CallSite<Func<CallSite, object, object, object>>>();
public static void SetValue(string propertyName, object target, object value)
{
CallSite<Func<CallSite, object, object, object>> site;
lock (setters)
{
if (!setters.TryGetValue(propertyName, out site))
{
var binder = Binder.SetMember(CSharpBinderFlags.None,
propertyName, typeof(CallSiteCache),
new List<CSharpArgumentInfo>{
CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null),
CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null)});
setters[propertyName] = site = CallSite<Func<CallSite, object, object, object>>.Create(binder);
}
}
site.Target(site, target, value);
}
}
然后使用它:
var settings = new JsonSerializerSettings
{
Converters = { new EntityBaseConverter() },
};
var stroy = JsonConvert.DeserializeObject<EntityBase>(JSON, settings);
因为似乎EntityBase
可能是多个派生类的基类,所以我编写了转换器以适用于EntityBase
的所有派生类型,并假设它们都具有参数化构造函数相同的签名。
注意我从JSON中获取EntityName
。如果您希望将其硬编码为"Story"
,则可以执行此操作,但您仍应将EntityName
属性的实际名称添加到usedParameters
集合,以防止具有相同的动态属性来自创建的名称。
示例工作.Net小提琴here。