JSON.NET как сериализатор OAP для WebAPI 2 и ODataMediaTypeFormatter

Я пытаюсь использовать JSON.NET в качестве сериализатора по умолчанию в стеке WebAPI 2. Я реализовал JsonMediaTypeFormatter, в котором я использовал сериализатор JSON.NET для сериализации/десериализации данных и создал JsonContentNegotiator для использования этого формата. Все работает отлично, за исключением запросов OData - если я добавлю [Queryable] метаданные из метода действия, тогда объект ответа не содержит никакой информации метаданных, только список объектов.

Небольшой пример. Мой метод действий:

[Queryable]
public async Task<PageResult<RuleType>> GetRuleType(ODataQueryOptions<RuleType> options)
{
    var ret = await _service.ListRuleTypesAsync(options);
    return new PageResult<RuleType>(
        ret,
        Request.GetNextPageLink(),
        Request.GetInlineCount());
}

Если я использую сериализацию по умолчанию OData и вызываю некоторый запрос по типу Rule (например - .../odata/RuleType?$inlinecount=allpages&$skip=0&$top=1), я получаю классический ответ OData с информацией метаданных и свойством count:

odata.metadata ".../odata/$metadata#RuleType" 
odata.count    "2" 
value
        0    {
                 Id: 1
             Name: "General"
             Code: "General"
             Notes: null
             }

(некоторые поля пропущены, но у меня есть свойство Notes с нулевым значением) Но если я добавлю JsonContentNegotiator с JsonMediaTypeFormatter в качестве сериализатора - я получаю только список объектов:

[
  {
    "Id": 1,
    "Name": "General",
    "Code": "General"
  }
]

(здесь нет поля для комментариев из-за NullValueHandling.Ignore) Даже больше. Если я удалю атрибут [Queryable] в методе действия - я получаю следующий результат:

{
  "Items": [
    {
      "Id": 1,
      "Name": "General",
      "Code": "General"
    }
  ],
  "Count": 2
}

В этом случае я получил Count, но все равно метаданных здесь нет. А также имена свойств ответа odata полностью отличаются от значений по умолчанию.

Мой разум взрывается. Я просто хочу использовать JSON.NET в качестве своего сериализатора в любой части моего веб-приложения (из-за сильных ограничений). Как я могу это сделать?

Ответы

Ответ 1

Я уже выяснил свою проблему и нашел решение. OData использует отдельные форматы форм мультимедиа, унаследованные от ODataMediaTypeFormatter. Также OData использует различные форматы для сериализации и десериализации. Для замены этого поведения мы должны реализовать потомков классов ODataDeserializerProvider и/или ODataSerializerProvider и добавить эти классы в коллекции HttpConfiguration.Formatters с помощью

var odataFormatters = ODataMediaTypeFormatters
    .Create(new MyODataSerializerProvider(), new MuODataDeserializerProvider());
config.Formatters.AddRange(odataFormatters);

Пример поставщика небольшой десериализации:

public class JsonODataDeserializerProvider : ODataDeserializerProvider
{
    public override ODataEdmTypeDeserializer GetEdmTypeDeserializer(IEdmTypeReference edmType)
    {
        var kind = GetODataPayloadKind(edmType);

        return new JsonODataEdmTypeDeserializer(kind, this);
    }

    private static ODataPayloadKind GetODataPayloadKind(IEdmTypeReference edmType)
    {
        switch (edmType.TypeKind())
        {
            case EdmTypeKind.Entity:
                return ODataPayloadKind.Entry;
            case EdmTypeKind.Primitive:
            case EdmTypeKind.Complex:
                return ODataPayloadKind.Property;
            case EdmTypeKind.Collection:
                IEdmCollectionTypeReference collectionType = edmType.AsCollection();
                return collectionType.ElementType().IsEntity() ? ODataPayloadKind.Feed : ODataPayloadKind.Collection;
            default:
                return ODataPayloadKind.Entry;
        }
    }

    public override ODataDeserializer GetODataDeserializer(IEdmModel model, Type type, HttpRequestMessage request)
    {
        var edmType = model.GetEdmTypeReference(type);

        return edmType == null ? null : GetEdmTypeDeserializer(edmType);
    }
}

ODataDeserializer:

public class JsonODataEdmTypeDeserializer : ODataEdmTypeDeserializer
{
    public JsonODataEdmTypeDeserializer(ODataPayloadKind payloadKind) : base(payloadKind)
    {
    }

    public JsonODataEdmTypeDeserializer(ODataPayloadKind payloadKind, ODataDeserializerProvider deserializerProvider) : base(payloadKind, deserializerProvider)
    {
    }

    public override object Read(ODataMessageReader messageReader, Type type, ODataDeserializerContext readContext)
    {
        var data = readContext.Request.Content.ReadAsStringAsync().Result;

        return JsonConvert.DeserializeObject(data, type);
    }
}

И я также добавил класс EdmLibsHelper из исходного кода WebAPI OData в моем проекте с методами GetEdmTypeReference() и GetEdmType(), потому что этот класс является внутренним.

Ответ 2

Если это помогает кому-то еще, вот как я повторно использовал собственный сериализатор Json.NET в OData.

В Startup введите свой собственный поставщик сериализатора:

    var odataFormatters = ODataMediaTypeFormatters.Create(new MyODataSerializerProvider(), new DefaultODataDeserializerProvider());
    config.Formatters.InsertRange(0, odataFormatters);

Вот мой MyODataSerializerProvider.cs:

    public class MyODataSerializerProvider : DefaultODataSerializerProvider
    {
        public override ODataEdmTypeSerializer GetEdmTypeSerializer(IEdmTypeReference edmType)
        {
            switch (edmType.TypeKind())
            {
                case EdmTypeKind.Enum:
                    ODataEdmTypeSerializer enumSerializer = base.GetEdmTypeSerializer(edmType);
                    return enumSerializer;

                case EdmTypeKind.Primitive:
                    ODataEdmTypeSerializer primitiveSerializer = base.GetEdmTypeSerializer(edmType);
                    return primitiveSerializer;

                case EdmTypeKind.Collection:
                    IEdmCollectionTypeReference collectionType = edmType.AsCollection();
                    if (collectionType.ElementType().IsEntity())
                    {
                        ODataEdmTypeSerializer feedSerializer = base.GetEdmTypeSerializer(edmType);
                        return feedSerializer;
                    }
                    else
                    {
                        ODataEdmTypeSerializer collectionSerializer = base.GetEdmTypeSerializer(edmType);
                        return collectionSerializer;
                    }

                case EdmTypeKind.Complex:
                    ODataEdmTypeSerializer complexTypeSerializer = base.GetEdmTypeSerializer(edmType);
                    return complexTypeSerializer;

                case EdmTypeKind.Entity:
                    ODataEdmTypeSerializer entityTypeSerializer = new MyODataEntityTypeSerializer(this);
                    return entityTypeSerializer;                    

                default:
                    return null;
            }
        }
    }

Затем он вызывает MyODataEntityTypeSerializer.cs:

public class MyODataEntityTypeSerializer : ODataEntityTypeSerializer
{
    private static Logger logger = LogManager.GetCurrentClassLogger();

    public DocsODataEntityTypeSerializer(ODataSerializerProvider serializerProvider)
        : base(serializerProvider)
    {
    }

    public override ODataEntry CreateEntry(SelectExpandNode selectExpandNode, EntityInstanceContext entityInstanceContext)
    {
        ODataEntry entry = base.CreateEntry(selectExpandNode, entityInstanceContext);
        if(entry.TypeName == typeof(YourObject).FullName)
        {
            YourObjectEntryConverter converter = new YourObjectEntryConverter(entry);
            entry = converter.Convert();
        }
        return entry;
    }
}

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

Здесь класс преобразователя:

public class YourObjectEntryConverter
{
    private ODataEntry _entry;
    private string[] _suppressed_properties = {
                                                "YourProperty1", "YourProperty2"
                                              };

    public YourObjectEntryConverter(ODataEntry entry)
    {
        _entry = entry;
    }

    public ODataEntry Convert()
    {
        // 1st pass: create a poco from odata
        YourObject yours = new YourObject();
        PropertyInfo[] properties = typeof(YourObject).GetProperties();           
        foreach (PropertyInfo property in properties)
        {
            foreach (ODataProperty odata_property in _entry.Properties)
            {
                if (property.Name == odata_property.Name)
                {
                    if (odata_property.Value is ODataCollectionValue)
                    {
                        // my json de/serialization populates these; ymmv
                    }
                    else if (odata_property.Value is DateTimeOffset)
                    {
                        DateTimeOffset? dto = odata_property.Value as DateTimeOffset?;
                        property.SetValue(yours, dto.Value.DateTime);
                    }
                    else if (odata_property.Value == null)
                    {
                        property.SetValue(yours, odata_property.Value);
                    }
                    else if (ODataUtils.IsPrimitiveType(odata_property.Value.GetType()))
                    {
                        property.SetValue(yours, odata_property.Value);
                    }
                    // todo complex types
                    break;
                }
            }
        }

        // 2nd pass: use json serializer in the business layer to add markup
        // this call fires the "decorators" in YourObjectSerializer.cs via Json.NET
        string json = JsonConvert.SerializeObject(yours);
        // suck the newly added info back in
        YourObject serialized = JsonConvert.DeserializeObject<YourObject>(json);

        // 3rd pass: scrape the json poco and shovel it back into odata
        foreach (PropertyInfo property in properties)
        {
            foreach (ODataProperty odata_property in _entry.Properties)
            {
                if (property.Name == odata_property.Name)
                {
                    if (odata_property.Value is ODataCollectionValue)
                    {
                        var collection = odata_property.Value as ODataCollectionValue;
                        var collection_typename = property.PropertyType.ToString();
                        if (collection_typename.Contains("List") && collection_typename.Contains("YourSubObject"))
                        {
                            IList<YourSubObject> subobjects = property.GetValue(serialized) as IList<YourSubObject>;
                            List<ODataComplexValue> subobjects_list = new List<ODataComplexValue>();
                            foreach(YourSubObject subobject in subobjects)
                            {
                                subobjects_list.Add(ODataUtils.CreateComplexValue(typeof(YourSubObject), subobject));
                            }
                            collection.Items = subobjects_list.AsEnumerable();
                        }
                    }
                    else if (odata_property.Value is DateTimeOffset)
                    {
                        DateTimeOffset? dto = odata_property.Value as DateTimeOffset?;
                        property.SetValue(yours, dto.Value.DateTime);
                    }
                    else
                    {
                        object new_value = property.GetValue(serialized);
                        object old_value = property.GetValue(yours);
                        if (null == old_value && null != new_value)
                        {
                            Type t = new_value.GetType();
                            if (!ODataUtils.IsPrimitiveType(t))
                            {
                                odata_property.Value = ODataUtils.CreateComplexValue(t, new_value);
                            }
                            else
                            {
                                odata_property.Value = new_value;
                            }                                
                        }
                        else if (odata_property.Value is Guid)
                        {
                            Guid? new_guid = new_value as Guid?;
                            Guid? old_guid = old_value as Guid?;
                            if (Guid.Empty == old_guid.Value && Guid.Empty != new_guid.Value)
                            {
                                odata_property.Value = new_value;
                            }
                        }
                    }
                    break;
                }
            }
        }

        // 4th pass: add stuff that json added to the entry
        List<ODataProperty> new_properties = new List<ODataProperty>();
        foreach (PropertyInfo property in properties)
        {
            object value = property.GetValue(serialized);
            if (null != value)
            {
                bool lost_property = true; // couldn't resist
                foreach (ODataProperty odata_property in _entry.Properties)
                {
                    if (property.Name == odata_property.Name)
                    {
                        lost_property = false;
                        break;
                    }
                }
                if (lost_property)
                {
                    ODataProperty new_property = ODataUtils.CreateProperty(property.Name, value);
                    new_properties.Add(new_property);
                }
            }
        }


        // 5th pass: strip odata properties we don't want to expose externally
        List<ODataProperty> unsuppressed_properties = new List<ODataProperty>();
        foreach (ODataProperty odata_property in _entry.Properties)
        {
            if (!_suppressed_properties.Contains(odata_property.Name))
            {
                unsuppressed_properties.Add(odata_property);
            }
        }
        unsuppressed_properties.AddRange(new_properties); // from 4th pass
        _entry.Properties = unsuppressed_properties.AsEnumerable();

        return _entry;

    }
}

Наконец, здесь мой utils класс:

public class ODataUtils
{
    public static bool IsPrimitiveType(Type t)
    {
        if (!t.IsPrimitive && t != typeof(Decimal) && t != typeof(String) && t != typeof(Guid) && t != typeof(DateTime)) // todo
        {
            return false;
        }
        return true;
    }

    public static ODataProperty CreateProperty(string name, object value)
    {
        object property_value = value;
        if(value != null)
        {
            Type t = value.GetType();
            if (!IsPrimitiveType(t))
            {
                property_value = CreateComplexValue(t, value);
            }
            else if (t == typeof(DateTime) || t == typeof(DateTime?))
            {
                DateTime dt = (DateTime)value;
                dt = DateTime.SpecifyKind(dt, DateTimeKind.Utc);
                DateTimeOffset dto = dt;
                property_value = dto;
            }
        }
        ODataProperty new_property = new ODataProperty()
        {
            Name = name,
            Value = property_value
        };
        return new_property;
}

    public static ODataComplexValue CreateComplexValue(Type type, object value)
    {
        ODataComplexValue complex_value = new ODataComplexValue();
        complex_value.TypeName = type.ToString();
        PropertyInfo[] complex_properties = type.GetProperties();
        List<ODataProperty> child_properties = new List<ODataProperty>();
        foreach (PropertyInfo property in complex_properties)
        {
            ODataProperty child_property = CreateProperty(property.Name, property.GetValue(value));
            child_properties.Add(child_property);
        }
        complex_value.Properties = child_properties.AsEnumerable();
        return complex_value;
    }
}

Все это ужасный взлом, но если у вас есть куча специального кода сериализации Json.NET для ваших объектов, которые вы хотите повторно использовать в OData, это сработало для меня.