Как передать аргументы конструктору, отличному от стандартного?

У меня примерно следующее изображение:

public class Foo
{
   public Foo(Bar bar, String x, String y)
   {
       this.Bar = bar;
       this.X = x;
       this.Y = y;
   }

   [JsonIgnore]
   public Bar Bar { get; private set; }

   public String X { get; private set; }
   public String Y { get; private set; }
}

public class Bar
{
    public Bar(String z)
    {
        this.Z = z;
    }

    public String Z { get; private set; }
}

Я хочу каким-то образом передать объект типа Bar в конструктор типа Foo во время десериализации, то есть:

var bar = new Bar("Hello world");
var x = JsonConvert.DeserializeObject<Foo>(fooJsonString, bar);

Ответ 1

Вот мои мысли относительно решения проблемы:

Проблема:

Пользовательская десериализация Json.Net api не прозрачна, то есть влияет на мою иерархию классов.

На самом деле это не проблема в случае, если у вас 10-20 классов в вашем проекте, хотя, если у вас есть огромный проект с тысячами классов, вы не очень довольны тем фактом, что вам нужно выполнить свой проект ООП с помощью Json. Чистые требования.

Json.Net хорош с объектами POCO, которые заполняются (инициализируются) после их создания. Но это не правда во всех случаях, иногда вы получаете свои объекты, инициализированные внутри конструктора. И чтобы сделать эту инициализацию, вам нужно передать "правильные" аргументы. Эти "правильные" аргументы могут быть внутри сериализованного текста, или они могут быть уже созданы и инициализированы некоторое время раньше. К сожалению, Json.Net во время десериализации передает значения по умолчанию аргументам, которые он не понимает, и в моем случае он всегда вызывает ArgumentNullException.

Решение:

Вот подход, который позволяет создавать реальные пользовательские объекты во время десериализации с использованием любого набора аргументов, сериализованных или несериализованных, основная проблема заключается в том, что подход не оптимален, он требует двух фаз десериализации на каждый объект, который требует пользовательской десериализации, но он работает и позволяет десериализовать объекты так, как вам нужно, так что:

Сначала мы собираем класс CustomCreationConverter следующим образом:

public class FactoryConverter<T> : Newtonsoft.Json.JsonConverter
{
    /// <summary>
    /// Writes the JSON representation of the object.
    /// </summary>
    /// <param name="writer">The <see cref="JsonWriter"/> to write to.</param>
    /// <param name="value">The value.</param>
    /// <param name="serializer">The calling serializer.</param>
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotSupportedException("CustomCreationConverter should only be used while deserializing.");
    }

    /// <summary>
    /// Reads the JSON representation of the object.
    /// </summary>
    /// <param name="reader">The <see cref="JsonReader"/> to read from.</param>
    /// <param name="objectType">Type of the object.</param>
    /// <param name="existingValue">The existing value of object being read.</param>
    /// <param name="serializer">The calling serializer.</param>
    /// <returns>The object value.</returns>
    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (reader.TokenType == JsonToken.Null)
            return null;

        T value = CreateAndPopulate(objectType, serializer.Deserialize<Dictionary<String, String>>(reader));

        if (value == null)
            throw new JsonSerializationException("No object created.");

        return value;
    }

    /// <summary>
    /// Creates an object which will then be populated by the serializer.
    /// </summary>
    /// <param name="objectType">Type of the object.</param>
    /// <returns></returns>
    public abstract T CreateAndPopulate(Type objectType, Dictionary<String, String> jsonFields);

    /// <summary>
    /// Determines whether this instance can convert the specified object type.
    /// </summary>
    /// <param name="objectType">Type of the object.</param>
    /// <returns>
    ///     <c>true</c> if this instance can convert the specified object type; otherwise, <c>false</c>.
    /// </returns>
    public override bool CanConvert(Type objectType)
    {
        return typeof(T).IsAssignableFrom(objectType);
    }

    /// <summary>
    /// Gets a value indicating whether this <see cref="JsonConverter"/> can write JSON.
    /// </summary>
    /// <value>
    ///     <c>true</c> if this <see cref="JsonConverter"/> can write JSON; otherwise, <c>false</c>.
    /// </value>
    public override bool CanWrite
    {
        get
        {
            return false;
        }
    }
}

Далее мы создаем класс factory, который создаст наш Foo:

public class FooFactory : FactoryConverter<Foo>
{
    public FooFactory(Bar bar)
    {
        this.Bar = bar;
    }

    public Bar Bar { get; private set; }

    public override Foo Create(Type objectType, Dictionary<string, string> arguments)
    {
        return new Foo(Bar, arguments["X"], arguments["Y"]);
    }
}

Вот пример кода:

var bar = new Bar("BarObject");

var fooSrc = new Foo
(
    bar,
    "A", "B"
);

var str = JsonConvert.SerializeObject(fooSrc);

var foo = JsonConvert.DeserializeObject<Foo>(str, new FooFactory(bar));

Console.WriteLine(str);

В этом случае foo содержит аргумент, который нам нужно передать в конструктор Foo во время десериализации.

Ответ 2

Я не эксперт Json.NET, но AFAIK просто невозможно. Если бы я был вами, я бы рассмотрел варианты исправления этой после десериализации.

Очень мало API-интерфейсов сериализации позволит вам контролировать конструкцию до такой степени; четыре наиболее типичных подхода (наиболее часто встречающиеся):

  • вызывать конструктор без параметров
  • полностью пропустить конструктор
  • используйте конструктор, который имеет очевидное сопоставление 1:1 членам, которые сериализуются
  • используйте предоставленный пользователем метод factory

Похоже, вы хотите последнее, что довольно редко. Возможно, вам придется согласиться на это за пределами конструктора.

Некоторые API-интерфейсы сериализации предлагают "обратные вызовы сериализации/десериализации", которые позволяют вам запускать метод на объекте в разных точках (как правило, до и после сериализации и десериализации), включая передачу некоторой контекстной информации в обратный вызов. IF Json.NET поддерживает обратные вызовы десериализации, на которые может обратить внимание. Этот вопрос предполагает, что шаблон обратного вызова [OnDeserialized] действительно может поддерживаться; context происходит от свойства JsonSerializerSettings .Context, которое вы можете по желанию предоставить методу десериализации.

В противном случае просто запустите его вручную после десериализации.

Мой грубый псевдокод (полностью непроверенный):

// inside type: Foo
[OnDeserialized]
public void OnDeserialized(StreamingContext ctx) {
    if(ctx != null) {
        Bar bar = ctx.Context as Bar;
        if(bar != null) this.Bar = bar; 
    }
}

и

var ctx = new StreamingContext(StreamingContextStates.Other, bar);
var settings = new JsonSerializerSettings { Context = ctx };
var obj = JsonConvert.DeserializeObject<Foo>(fooJsonString, settings);

Ответ 3

Если у вас есть конструктор, единственными параметрами которого являются ваши несериализованные значения, сначала создайте свой экземпляр, а затем заселите свой объект вместо десериализации. Класс JsonConvert имеет метод PopulateObject, определяемый следующим образом:

public static void PopulateObject(
    string value,                      // JSON string
    object target)                     // already-created instance

Если у вас есть определенные параметры сериализации, есть перегрузка, которая также включает параметр JsonSerializerSettings.

Добавьте конструктор Foo, который имеет один параметр Bar, вы можете сделать что-то вроде:

var bar = new Bar("Hello World");
var foo = new Foo(bar);
JsonConvert.PopulateObject(fooJsonString, foo);

Вам может потребоваться настроить класс для использования полей для сопоставления или внести изменения в NHibernate, чтобы разрешить запись в частные сеттеры (использование пользовательского класса IProxyValidator).