Подтвердить что ты не робот

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

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

В контроллере у меня есть метод вроде:

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

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

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

Я вижу:

  • actionContext.Request.Content

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

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

4b9b3361

Ответ 1

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

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

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

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

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 для вашего вызывающего абонента чисто основанным на вкусе, а не потому, что имена типов легче запоминать в одном или другой.

Ответ 2

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

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

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

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

Например, если вы передадите этот параметр в действие веб-API:

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

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

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

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

Здесь вы можете увидеть образец в документации JSON.NET TypeNameHandling.Auto.

Это работает хотя бы с версии JSON.NET 4.

Примечание

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

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

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

СРАВНЕНИЕ К CUSTOM JsonConverter/JsonConverterAttribute

Я сравниваю собственное решение с этим ответом.

Чтобы реализовать JsonConverter/JsonConverterAttribute:

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

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

И это преимущества собственного решения JSON.NET:

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

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

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

ПОЧЕМУ ИСПОЛЬЗОВАНИЕ CUSTOM JsonConverter/JsonConverterAttribute

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

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

Ответ 3

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

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

Это даст вам сырой JSON.

Ответ 4

Я создал для этого метод помощника:

Просто передайте объект запроса, и он предоставит вам тело запроса.

Посмотрите на функцию ниже:

    public static string GetRequestContent(HttpRequestMessage request)
    {
        string rawRequest = string.Empty;
        try
        {
            using (var stream = new StreamReader(request.Content.ReadAsStreamAsync().Result))
            {
                stream.BaseStream.Position = 0;
                rawRequest = stream.ReadToEnd();
            }
        }
        catch (Exception ex){ throw; }
        return rawRequest;
    }

Использование

string baseRequestContent = HelperClassName.GetRequestContent(request);

Примечание. HelperClassName будет вашим именем помощника/класса службы.