Атрибут Newtonsoft Json.NET JsonConverter сохраняет проблему с ссылками при десериализации - PullRequest
0 голосов
/ 06 февраля 2019

В моделях проекта я использую атрибут JsonConverter, чтобы помочь с (де) сериализацией этих моделей.

В настоящее время конвертер выглядит так:

public class CustomJsonConverter : Newtonsoft.Json.JsonConverter
{
    bool _canWrite = true;
    public override bool CanWrite
    {
        get { return _canWrite; }
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        serializer.PreserveReferencesHandling = PreserveReferencesHandling.Objects;
        serializer.DefaultValueHandling = DefaultValueHandling.Ignore;
        serializer.NullValueHandling = NullValueHandling.Ignore;

        _canWrite = false;
        var jObject = JObject.FromObject(value, serializer);
        _canWrite = true;

        jObject.WriteTo(writer);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        serializer.PreserveReferencesHandling = PreserveReferencesHandling.Objects;
        if (reader.TokenType == JsonToken.StartObject)
        {
            existingValue = existingValue ?? serializer.ContractResolver.ResolveContract(objectType).DefaultCreator();
            serializer.Populate(reader, existingValue);
            return existingValue;
        }
        else if (reader.TokenType == JsonToken.Null)
        {
            return null;
        }
        else
        {
            throw new JsonSerializationException();
        }
    }

    public override bool CanConvert(Type objectType)
    {
        return typeof(IModelBase).IsAssignableFrom(objectType);
    }
}

Модели имеют базовый класс, который выглядит следующим образом:

[JsonConverter(typeof(CustomJsonConverter))]
public abstract class ModelBase : IModelBase
{
    public string ID { get; set; }
    public DateTime CreatedAt { get; set; }
    public DateTime ModifiedAt { get; set; }
}

Производные классы класса ModelBase имеют свойства, тип которых также является производным от ModelBase.Например:

public class CustomerModel : ModelBase
{
    public string Name { get; set; }
    public UserModel CreatedBy { get; set; }
    public UserModel ModifiedBy { get; set; }
}

public class UserModel : ModelBase
{
    public string Name { get; set; }
    public UserModel CreatedBy { get; set; }
    public UserModel ModifiedBy { get; set; }
}

Я использую эти модели в приложении ASP.NET Web API 2 (на стороне сервера) и в приложениях C # (на стороне клиента).Для большинства вызовов API возвращается массив моделей.При сериализации моделей все работает как положено.Однако при десериализации только первое вхождение каждой ссылки заполняется информацией.

Например:

[
    {
        "$id": "1",
        "Name": "Customer1",
        "CreatedBy": {
            "$id": "2",
            "ID": "1",
            "Name": "User1"
        },
        "ModifiedBy": {
            "$id": "3",
            "ID": "3",
            "Name": "User3"
        },
        "ID": "1",
        "CreatedAt": "2019-02-06T00:00:04",
        "ModifiedAt": "2019-02-06T00:20:12"
    },
    {
        "$id": "4",
        "Name": "Customer2",
        "CreatedBy": {
            "$ref": "2"
        },
        "ModifiedBy": {
            "$ref": "2"
        },
        "ID": "2",
        "CreatedAt": "2019-02-06T00:10:00",
        "ModifiedAt": "2019-02-06T00:10:00"
    }
]

При попытке десериализации этого объекта JSON, возвращаемого веб-API, *Свойства 1020 * и ModifiedBy будут правильными для первого объекта CustomerModel.Однако для второго объекта CustomerModel эти свойства будут новыми UserModel экземплярами без каких-либо установленных свойств.

Для десериализации строки JSON я использую следующий код:

using (var sr = new StreamReader(streamFromWebAPICall))
{                
    using (var jtr = new JsonTextReader(sr))
    {
        var js = new JsonSerializer();
        return js.Deserialize(jtr, objectType);
    }
}

Что я могу сделать, чтобы правильно установить свойства всех десериализованных объектов?

Редактировать:

Проблема, похоже, в serializer.Populate(reader, existingValue), где ссылки не запоминаются.

1 Ответ

0 голосов
/ 07 февраля 2019

Ваша основная проблема заключается в том, что вы предоставляете custom JsonConverter для типа, для которого вы также хотите включить PreserveReferencesHandling.Но всякий раз, когда применяется пользовательский конвертер, он должен позаботиться о все вручную, , включая разбор и генерацию "$ref" и "$id" свойств .Ваш конвертер этого не делает, поэтому ваш код десериализации неправильно десериализует ваш граф объектов.

принятый ответ на Сериализация пользовательских объектов против PreserveReferencesHandling включает конвертер шаблонов, который показывает, как эти свойства могут быть обработаны.Однако, поскольку ваш конвертер, похоже, переключает некоторые настройки сериализатора до (де) сериализации, вы можете просто сделать вызов, чтобы рекурсивно (де) сериализовать объект, отключив конвертер на время, например:

public class CustomJsonConverter : Newtonsoft.Json.JsonConverter
{
    [ThreadStatic]
    static bool disabled;

    // Disables the converter in a thread-safe manner.
    bool Disabled { get { return disabled; } set { disabled = value; } }

    public override bool CanWrite { get { return !Disabled; } }

    public override bool CanRead { get { return !Disabled; } }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        using (new PushValue<bool>(true, () => Disabled, val => Disabled = val))
        using (new PushValue<PreserveReferencesHandling>(PreserveReferencesHandling.Objects, () => serializer.PreserveReferencesHandling, val => serializer.PreserveReferencesHandling = val))
        using (new PushValue<DefaultValueHandling>(DefaultValueHandling.Ignore, () => serializer.DefaultValueHandling, val => serializer.DefaultValueHandling = val))
        using (new PushValue<NullValueHandling>(NullValueHandling.Ignore, () => serializer.NullValueHandling, val => serializer.NullValueHandling = val))
        {
            serializer.Serialize(writer, value);
        }
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (reader.TokenType == JsonToken.Null)
        {
            return null;
        }
        using (new PushValue<bool>(true, () => Disabled, val => Disabled = val))
        using (new PushValue<PreserveReferencesHandling>(PreserveReferencesHandling.Objects, () => serializer.PreserveReferencesHandling, val => serializer.PreserveReferencesHandling = val))
        using (new PushValue<DefaultValueHandling>(DefaultValueHandling.Ignore, () => serializer.DefaultValueHandling, val => serializer.DefaultValueHandling = val))
        using (new PushValue<NullValueHandling>(NullValueHandling.Ignore, () => serializer.NullValueHandling, val => serializer.NullValueHandling = val))
        {
            return serializer.Deserialize(reader, objectType);
        }
    }

    public override bool CanConvert(Type objectType)
    {
        throw new NotImplementedException();
    }
}

public struct PushValue<T> : IDisposable
{
    Action<T> setValue;
    T oldValue;

    public PushValue(T value, Func<T> getValue, Action<T> setValue)
    {
        if (getValue == null || setValue == null)
            throw new ArgumentNullException();
        this.setValue = setValue;
        this.oldValue = getValue();
        setValue(value);
    }

    // By using a disposable struct we avoid the overhead of allocating and freeing an instance of a finalizable class.
    public void Dispose()
    {
        if (setValue != null)
            setValue(oldValue);
    }
}

Обратите внимание, что логика отключения конвертера должна быть поточно-ориентированной, поскольку Json.NET будет совместно использовать контракты и конвертеры между потоками.

Демонстрационная скрипта # 1 здесь .

В качестве альтернативы вы можете полностью исключить преобразователь и применить [JsonObject(IsReference = true)] непосредственно к ModelBase:

[JsonObject(IsReference = true)]
public abstract class ModelBase : IModelBase
{
    public string ID { get; set; }
    public DateTime CreatedAt { get; set; }
    public DateTime ModifiedAt { get; set; }
}

Затем сериализовать и десериализоватьс DefaultValueHandling.Ignore и NullValueHandling.Ignore, указанными в настройках следующим образом:

static object Deserialize(Stream streamFromWebAPICall, Type objectType)
{
    using (var sr = new StreamReader(streamFromWebAPICall))
    {
        using (var jtr = new JsonTextReader(sr))
        {
            var settings = new JsonSerializerSettings
            {
                DefaultValueHandling = DefaultValueHandling.Ignore,
                NullValueHandling = NullValueHandling.Ignore,
            };
            var js = JsonSerializer.CreateDefault(settings);
            return js.Deserialize(jtr, objectType);
        }
    }
}

Демонстрационная скрипка # 2 здесь .

Обратите внимание, вы также можете установить [JsonObject(ItemNullValueHandling = NullValueHandling.Ignore)] по состоянию на Json.NET 11.0.1 , но, по-видимому, нет настройки ItemDefaultValueHandling для JsonObjectAttribute, поэтому добавьте ее в настройки сериализатора (или используйте обнуляемые значения для необязательного значения)значения типа, такие как CreatedAt).

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...