I\'m trying to use JSON.NET as a default serializer in WebAPI 2 stack. I\'ve implemented JsonMediaTypeFormatter, in which I\'ve used JSON.NET serializer for serialize/deseri
If it helps anyone else, here is how I re-used my Json.NET custom serializer in OData.
In Startup
, insert your custom serializer provider:
var odataFormatters = ODataMediaTypeFormatters.Create(new MyODataSerializerProvider(), new DefaultODataDeserializerProvider());
config.Formatters.InsertRange(0, odataFormatters);
Here is my 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;
}
}
}
This then calls into 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;
}
}
Note that YourObject
is your custom class that has a Json.NET serializer attached, via attribute or config.
Here's the converter class:
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;
}
}
Lastly, here's my utils class:
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;
}
}
Its all a horrible hack, but if you have a bunch of special Json.NET serialization code for your objects that you want to re-use in OData, this worked for me.
I've already figured out my problem and found the solution. OData uses separate media type formatters, inherited from ODataMediaTypeFormatter. Also OData uses different formatters for serialization and deserialization. For replacing this behavior we have to implement descendants of ODataDeserializerProvider and/or ODataSerializerProvider classes and add those classes to the HttpConfiguration.Formatters collections by
var odataFormatters = ODataMediaTypeFormatters
.Create(new MyODataSerializerProvider(), new MuODataDeserializerProvider());
config.Formatters.AddRange(odataFormatters);
Small deserialization provider example:
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);
}
}
And I also have added EdmLibsHelper class from WebAPI OData source code in my project with GetEdmTypeReference() and GetEdmType() methods because this class is internal.