【问题标题】:JsonConvert.DeserializeObject w/ DynamicObject and TypeCreationConverterJsonConvert.DeserializeObject w/ DynamicObject 和 TypeCreationConverter
【发布时间】:2018-04-19 08:21:37
【问题描述】:

我有一个派生自 DynamicObject 的类 EntityBase,没有空的默认构造函数。

// 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 对象填充到类型“DynamicObjectJson.EntityBase”。 路径“名称”,第 1 行,位置 8。

似乎DeserializeObjectCustomCreationConverter 创建的对象上调用PopulateObject。当我尝试手动执行此操作时,错误保持不变

JsonConvert.PopulateObject(JSON, new EntityBase("Story"));

我进一步假设PopulateObject 不会检查目标类型是否源自DynamicObject,因此不会退回到TrySetMember

请注意,我对EntityBase 类型定义没有影响,它来自外部库,无法更改。

任何见解都将受到高度赞赏!

编辑:添加示例:https://dotnetfiddle.net/EGOCFU

【问题讨论】:

标签: c# json.net dynamicobject


【解决方案1】:

您似乎偶然发现了 Json.NET 支持反序列化动态对象(定义为生成 JsonDynamicContract 的对象)中的几个错误或限制:

  1. 不支持参数化构造函数。即使一个标有[JsonConstructor],它也不会被使用。

    JsonSerializerInternalReader.CreateDynamic() 似乎完全缺少预加载所有属性的必要逻辑。与 JsonSerializerInternalReader.CreateNewObject() 进行比较,这表明需要什么。

    由于逻辑看起来相当复杂,这可能是一个限制而不是错误。实际上有closed issue #47 表明它没有实现:

    添加此功能需要做很多工作。如果您确实添加了它,欢迎您提交拉取请求。

  2. Json.NET 无法填充预先存在的动态对象。与常规对象(生成JsonObjectContract 的对象)不同,构造和填充的逻辑完全包含在前面提到的JsonSerializerInternalReader.CreateDynamic() 中。

    我不明白为什么这不能通过相当简单的代码重组来实现。你可能会submit an issue 要求这个。如果实现了这一点,您的 StoryCreator 将按原样工作。

在没有#1 或#2 的情况下,可以创建一个custom JsonConverter,其逻辑大致以JsonSerializerInternalReader.CreateDynamic() 为模型,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 fiddle here.

【讨论】:

  • 太棒了!使用您描述的自定义转换器,它就像一个魅力!非常感谢您提供高质量的答案!
猜你喜欢
  • 2011-04-11
  • 2018-01-16
  • 1970-01-01
  • 2011-04-03
  • 1970-01-01
  • 1970-01-01
  • 2016-12-17
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多