JsonConvert.DeserializeObject с DynamicObject и TypeCreationConverter

У меня есть класс 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 типом DynamicObjectJson.EntityBase. Путь 'Имя', строка 1, позиция 8.

Кажется, что DeserializeObject вызывает PopulateObject объект, созданный CustomCreationConverter. Когда я пытаюсь сделать это вручную, ошибка остается прежней.

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

Я также предполагаю, что PopulateObject не проверяет, является ли целевой тип производным от DynamicObject и, следовательно, не возвращается к TrySetMember.

Обратите внимание, что я не могу повлиять на определение типа EntityBase, оно взято из внешней библиотеки и не может быть изменено.

Любые идеи будут высоко оценены!

Изменить: добавлен пример: https://dotnetfiddle.net/EGOCFU.


person user1859022    schedule 19.04.2018    source источник
comment
Вы бы хотели создать конструктор без параметров (возможно, закрытый и помеченный [JsonCOnstructor]) и пометить EntityName с помощью [JsonProperty] или [DataMember], как показано в C # Как сделать сериализовать (JSON, XML) обычные свойства в классе, который наследуется от DynamicObject? Я считаю, что это должно сработать.   -  person dbc    schedule 19.04.2018
comment
к сожалению, я не могу повлиять на определение типа, это внешняя библиотека   -  person user1859022    schedule 19.04.2018
comment
Есть ли у EntityName частный установщик в реальном типе, который вы пытаетесь десериализовать?   -  person dbc    schedule 19.04.2018


Ответы (1)


Похоже, вы наткнулись на пару ошибок или ограничений в поддержке Json.NET десериализации динамических объектов (определенных как те, для которых _ 1_):

  1. # P2 # # P3 # # P4 #
    # P5 #
  2. Json.NET не имеет возможности заполнять существующий динамический объект. В отличие от обычных объектов (для которых создается JsonObjectContract), логика построения и пополнения полностью содержится в упомянутом ранее JsonSerializerInternalReader.CreateDynamic().

    Я не понимаю, почему это нельзя реализовать с помощью довольно простой реструктуризации кода. Вы можете отправить вопрос с просьбой об этом. Если бы это было реализовано, ваш StoryCreator работал бы как есть.

При отсутствии №1 или №2 можно создать пользовательский 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 с предположением, что все они имеют параметризованный конструктор с одинаковой сигнатурой.

Обратите внимание, что я беру EntityName из JSON. Если вы предпочитаете жестко запрограммировать его на "Story", вы можете это сделать, но вы все равно должны добавить фактическое имя свойства EntityName в коллекцию usedParameters, чтобы предотвратить создание динамического свойства с тем же именем.

Пример работающей скрипты .Net здесь.

person dbc    schedule 20.04.2018
comment
классно! используя специальный конвертер, как вы описали, он работает как шарм! Большое спасибо за этот качественный ответ! - person user1859022; 20.04.2018