Десериализация Json в производные типы в Asp.Net Web API


Я вызываю метод моего WebAPI отправки json, который я хотел бы сопоставить (или привязать) с моделью.

в контроллере у меня есть способ, как:

public Result Post([ModelBinder(typeof(CustomModelBinder))]MyClass model);

'MyClass', который задается в качестве параметра является абстрактным классом. Я хотел бы, чтобы в зависимости от типа передаваемого json был создан правильный унаследованный класс.

чтобы достичь этого, я пытаюсь реализовать пользовательскую привязку. Проблема в том, что (я не знаю, если это очень простой, но я не могу найти ничего) я не знаю, как получить raw Json (или лучше, какую-то сериализацию), которая входит в запрос.

Я вижу:

  • actionContext.Запрос.Содержание

но все методы доступны как async. Я не знаю, кто это подходит для передачи модели генерации в метод контроллера...

Спасибо большое!

4 54

4 ответа:

вам не нужна пользовательская модель связующего. Также вам не нужно возиться с конвейером запросов.

взгляните на этот другой так:как реализовать пользовательский JsonConverter в JSON.NET десериализовать список объектов базового класса?.

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

начиная с JsonCreationConverter<T> ссылка в этом так (немного изменен, чтобы исправить проблемы с сериализацией типов в ответы):

public abstract class JsonCreationConverter<T> : JsonConverter
{
    /// <summary>
    /// this is very important, otherwise serialization breaks!
    /// </summary>
    public override bool CanWrite
    {
        get
        {
            return false;
        }
    }
    /// <summary> 
    /// Create an instance of objectType, based properties in the JSON object 
    /// </summary> 
    /// <param name="objectType">type of object expected</param> 
    /// <param name="jObject">contents of JSON object that will be 
    /// deserialized</param> 
    /// <returns></returns> 
    protected abstract T Create(Type objectType, JObject jObject);

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

    public override object ReadJson(JsonReader reader, Type objectType,
      object existingValue, JsonSerializer serializer)
    {
        if (reader.TokenType == JsonToken.Null)
            return null;
        // Load JObject from stream 
        JObject jObject = JObject.Load(reader);

        // Create target object based on JObject 
        T target = Create(objectType, jObject);

        // Populate the object properties 
        serializer.Populate(jObject.CreateReader(), target);

        return target;
    }

    public override void WriteJson(JsonWriter writer, object value, 
      JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
} 

и теперь вы можете комментировать свой тип с JsonConverterAttribute, указывая Json.Net в пользовательский конвертер:

[JsonConverter(typeof(MyCustomConverter))]
public abstract class BaseClass{
  private class MyCustomConverter : JsonCreationConverter<BaseClass>
  {
     protected override BaseClass Create(Type objectType, 
       Newtonsoft.Json.Linq.JObject jObject)
     {
       //TODO: read the raw JSON object through jObject to identify the type
       //e.g. here I'm reading a 'typename' property:

       if("DerivedType".Equals(jObject.Value<string>("typename")))
       {
         return new DerivedClass();
       }
       return new DefaultClass();

       //now the base class' code will populate the returned object.
     }
  }
}

public class DerivedClass : BaseClass {
  public string DerivedProperty { get; set; }
}

public class DefaultClass : BaseClass {
  public string DefaultProperty { get; set; }
}

теперь вы можете использовать базовый тип в качестве параметра:

public Result Post(BaseClass arg) {

}

и если бы мы опубликовали:

{ typename: 'DerivedType', DerivedProperty: 'hello' }

затем arg был бы экземпляр DerivedClass, но если мы разместили:

{ DefaultProperty: 'world' }

тогда вы получите экземпляр DefaultClass.

EDIT-почему я предпочитаю этот метод TypeNameHandling.Auto/All

я действительно считаю, что с помощью TypeNameHandling.Auto/All поддерживаемый JotaBe не всегда является идеальным решением. Это вполне может быть в этом случае - но лично я не буду этого делать, если:

  • мой API-это только будет использоваться мной или моей командой
  • меня не волнует наличие двойной XML-совместимой конечной точки

Когда Json.Net TypeNameHandling.Auto или All используются, ваш веб-сервер начнет отправлять имена типов в формат MyNamespace.MyType, MyAssemblyName.

я сказал в комментариях, что я думаю, что это проблема безопасности. Об этом упоминалось в некоторых документах, которые я читал от Microsoft. Кажется, это больше не упоминается, однако я все еще чувствую, что это обоснованная озабоченность. Я не когда-нибудь хотите предоставить имена типов с полным пространством имен и имена сборок для внешнего мира. Это увеличивает мою поверхность атаки. Так что, да, я не могу иметь Object свойства / параметры мой API типы, но кто сказал, что остальная часть моего сайта полностью свободна от дыр? Кто сказал, что будущая конечная точка не предоставляет возможность использовать имена типов? Зачем рисковать только потому, что так проще?

также-если вы пишете "правильный" API, т. е. специально для потребления третьими сторонами, а не только для себя, и вы используете веб-API, то вы, скорее всего, хотите использовать обработку типа контента JSON/XML (как минимум). Смотрите, как далеко вы получаете, пытаясь написать документация, которую легко использовать, которая относится ко всем вашим типам API по-разному для форматов XML и JSON.

путем переопределения как JSON.Net понимая имена типов, вы можете привести их в соответствие, сделав выбор между XML/JSON для вашего вызывающего абонента исключительно на основе вкуса, а не потому, что имена типов легче запомнить в одном или другом.

вам не нужно реализовывать его самостоятельно. JSON.NET имеет родную поддержку для него.

вы должны указать желаемый параметр TypeNameHandling для JSON форматирования, как это (в global.asax событие старт приложения):

JsonSerializerSettings serializerSettings = GlobalConfiguration.Configuration
   .Formatters.JsonFormatter.SerializerSettings;
serializerSettings.TypeNameHandling = TypeNameHandling.Auto;

если указать Auto, как и в приведенном выше примере, параметр будет десериализован до типа, указанного в $type свойства объекта. Если $type свойство отсутствует, оно будет десериализовано в тип параметра. Так что вам нужно только указать тип, когда вы передаете параметр производного типа. (Это самый гибкий вариант).

например, если вы передадите этот параметр в действие Web API:

var param = {
    $type: 'MyNamespace.MyType, MyAssemblyName', // .NET fully qualified name
    ... // object properties
};

параметр будет десериализован в объект MyNamespace.MyType класса.

это также работает Для под-свойств, т. е. вы можете иметь такой объект, который указывает, что внутреннее свойство имеет заданное значение типа

var param = { 
   myTypedProperty: {
      $type: `...`
      ...
};

здесь вы можете увидеть образец дальше JSON.NET документация TypeNameHandling.Авто.

это работает по крайней мере с тех пор JSON.NET 4 релиз.

Примечание

вам не нужно украшать что-нибудь с attirbutes, или делать любые другие настройки. Он будет работать без каких-либо изменений в коде веб-API.

ВАЖНОЕ ПРИМЕЧАНИЕ

в $type должно быть первым свойством сериализованного объекта JSON. Если нет, то он будет проигнорирован.

сравнение с пользовательским Jsonconverter/JsonConverterAttribute

я сравниваю собственное решение ответ.

для реализации JsonConverter/JsonConverterAttribute:

  • вам нужно реализовать пользовательский JsonConverter, и обычай JsonConverterAttribute
  • вы должны использовать атрибуты, чтобы отметить параметры
  • вы должны заранее знать возможные типы, ожидаемые для параметра
  • вам нужно реализовать или изменить реализацию вашего JsonConverter всякий раз, когда ваши типы или изменить свойства
  • есть код запах волшебные строки, чтобы указать ожидаемые имена свойств
  • вы не реализуете что-то общее, что может быть использовано с любым типом
  • вы изобретаете колесо

в ответе автора есть комментарий относительно безопасности. Если вы не делаете что-то неправильно (например, принимая слишком общий тип для вашего параметра, например Object) нет никакого риска получить экземпляр неправильного типа: JSON.NET собственное решение только создает экземпляр объекта типа параметра или типа, производного от него (если нет, вы получаете null).

и это преимущества JSON.NET родной решение:

  • вам не нужно ничего реализовывать (вам нужно только настроить TypeNameHandling один раз в вашем приложении)
  • вам не нужно использовать атрибуты в параметрах действий
  • вам не нужно заранее знать возможные типы параметров: вам просто нужно знать базовый тип и указать его в параметре (это может быть абстрактный тип, чтобы сделать полиморфизм более очевидным)
  • решение работает в большинстве случаев (1) ничего не меняется
  • это решение широко протестировано и оптимизировано
  • вам не нужны волшебные строки
  • реализация является универсальной и будет принимать любой производный тип

(1): Если вы хотите получать значения параметров, которые не наследуются от одного и того же базового типа, это не будет работать, но я не вижу смысла делать это

так что я не могу найти никаких недостатков, и найти много преимущества на JSON.NET решение.

зачем использовать пользовательский Jsonconverter / JsonConverterAttribute

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

если вы хотите сделать что-то, что не может сделать собственное решение, например, настроить имена типов или вывести тип параметра на основе доступных имен свойств, используйте это решение, адаптированное к твое собственное дело. Другой не может быть настроен и не будет работать для ваших нужд.

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

string jsonContent = await actionContext.Request.Content.ReadAsStringAsync();

это даст вам raw JSON.

Если вы хотите использовать TypeNameHandling.Авто, но обеспокоены безопасностью или не любят потребителей api, нуждающихся в этом уровне знаний за кулисами, вы можете обрабатывать $type десериализовать себя.

public class InheritanceSerializationBinder : DefaultSerializationBinder
{
    public override Type BindToType(string assemblyName, string typeName)
    {
        switch (typeName)
        {
            case "parent[]": return typeof(Class1[]);
            case "parent": return typeof(Class1);
            case "child[]": return typeof(Class2[]);
            case "child": return typeof(Class2);
            default: return base.BindToType(assemblyName, typeName);
        }
    }
}

затем подключить это в глобальном.асакс.Приложение__Начало

var config = GlobalConfiguration.Configuration;
        config.Formatters.JsonFormatter.SerializerSettings = new JsonSerializerSettings { Binder = new InheritanceSerializationBinder() };

наконец, я использовал класс-оболочку и [JsonProperty (TypeNameHandling = TypeNameHandling.Auto)] на свойстве, содержащем объект с разными типами, поскольку я не смог чтобы заставить его работать, настроив фактический класс.

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

кредит : https://mallibone.com/post/serialize-object-inheritance-with-json.net для того, чтобы показать мне пользовательский десериализатор этого поля свойство.