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, это сработало для меня.