JSON.NET come serializzatore ODAT WebAPI 2 vs ODataMediaTypeFormatter

Sto cercando di utilizzare JSON.NET come serializzatore predefinito nello stack WebAPI 2. Ho implementato JsonMediaTypeFormatter, in cui ho utilizzato il serializzatore JSON.NET per serializzare / deserializzare i dati e creato JsonContentNegotiator per l’utilizzo di questo formattatore del tipo di supporto. Funziona tutto bene tranne l’interrogazione OData – se aggiungo il metadata [Queryable] del metodo di azione, l’object risposta non contiene alcuna informazione sui metadati, solo un elenco di quadro.

Piccolo esempio Il mio metodo di azione:

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

Se uso la serializzazione OData predefinita e chiamo alcune query per tipo di regola (ad esempio – .../odata/RuleType?$inlinecount=allpages&$skip=0&$top=1 ), ricevo la classica risposta OData con informazioni sui metadati e conteggio delle proprietà :

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

(alcuni campi saltati, ma ho la proprietà Notes con valore null) Ma se aggiungo il mio JsonContentNegotiator con JsonMediaTypeFormatter come serializzatore – Ricevo solo un elenco di quadro:

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

(Nessun campo Note qui a causa di NullValueHandling.Ignore ) Ancora di più. Se [Queryable] attributo [Queryable] nel metodo action, ricevo un altro risultato:

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

In questo caso ho ricevuto Count, ma ancora nessun metadata qui. E anche i nomi delle proprietà di risposta odata differiscono completamente dal default.

La mia mente sta esplodendo. Voglio solo utilizzare JSON.NET come serializzatore in qualsiasi parte della mia app Web (a causa di alcune forti restrizioni). Come posso fare questo?

Ho già capito il mio problema e ho trovato la soluzione. OData utilizza formattatori di tipi di file separati, ereditati da ODataMediaTypeFormatter. Anche OData utilizza diversi formattatori per la serializzazione e la deserializzazione. Per sostituire questo comportamento, dobbiamo implementare i discendenti delle classi ODataDeserializerProvider e / o ODataSerializerProvider e aggiungere tali classi alle raccolte HttpConfiguration.Formatters di

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

Esempio di provider di deserializzazione di piccole dimensioni:

 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); } } 

E ho anche aggiunto la class EdmLibsHelper dal codice sorgente ODAP WebAPI nel mio progetto con i metodi GetEdmTypeReference () e GetEdmType () perché questa class è interna.

Se aiuta qualcun altro, ecco come ho riutilizzato il serializzatore personalizzato Json.NET in OData.

In Startup , inserisci il tuo provider serializzatore personalizzato:

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

Ecco il mio 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; } } } 

Questo quindi chiama in 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; } } 

Nota che YourObject è la tua class personalizzata che ha un serializzatore Json.NET collegato, via attributo o configurazione.

Ecco la class del convertitore:

 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(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 subobjects = property.GetValue(serialized) as IList; List subobjects_list = new List(); 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 new_properties = new List(); 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 unsuppressed_properties = new List(); 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; } } 

Infine, ecco la mia class 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 child_properties = new List(); 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; } } 

È tutto un orribile hack, ma se hai un mucchio di codice di serializzazione speciale Json.NET per i tuoi oggetti che vuoi riutilizzare in OData, questo ha funzionato per me.