Reputation: 41
I'm writing an OData V4 service / Web API 2 with the latest OData NuGet packages. I have a problem I think is a formatting issue or maybe a configuration issue on the server but I am new to OData and WebAPI, so I am probably wrong.
The problem is as so: If I call my OData service with patch where a non-us string like "Mølgaard" is contained both in a field that points to a declared property and in a dynamic property, I get in my patch method on my controller "Mølgaard" in the declared property, but in the dynamic properties I get the raw-value "M\u00f8lgaard". The expected behaviour is to get "Mølgaard" in both, and what I find very strange is that the dynamic properties seem to get handled differently than the declared POCO-properties. I have tried this with a generated MS ODataClient bound to my service and also via a tool called Postman, in both instances I get into my Patch method with the same wrong value.
Being very new to OData, I have tried adding a new serializer as described here: http://odata.github.io/WebApi/#06-03-costomize-odata-formatter and from there modifications as described in the answer here: OData WebApi V4 .net - Custom Serialization I also found an example around using Newtonsoft.Json.JsonConvert. In short, neither did help and I am guessing that neither are actually meant to solve this issue.
I started from a demo project I found here: https://github.com/DevExpress-Examples/XPO_how-to-implement-odata4-service-with-xpo
I have added an POCO class like so:
public class OOrder : IDynamicProperties
{
[System.ComponentModel.DataAnnotations.Key]
public int ID { get; set; }
// SomeText is the declared property and its value
// is then repeated in DynamicProperties with another name
public string SomeText { get; set; }
public IDictionary<string, object> DynamicProperties { get; set; }
}
// I do not know if I need this, I am using
// it in a map function
public interface IDynamicProperties
{
IDictionary<string, object> DynamicProperties { get; set; }
}
And my config is pretty basic:
public static class WebApiConfig {
public static void Register(HttpConfiguration config) {
config.Count().Filter().OrderBy().Expand().Select().MaxTop(null);
ODataModelBuilder modelBuilder = CreateODataModelBuilder();
ODataBatchHandler batchHandler = new DefaultODataBatchHandler(GlobalConfiguration.DefaultServer);
config.MapODataServiceRoute(
routeName: "ODataRoute",
routePrefix: null,
model: modelBuilder.GetEdmModel(),
batchHandler: batchHandler);
}
static ODataModelBuilder CreateODataModelBuilder()
{
ODataModelBuilder builder = new ODataModelBuilder();
var openOrder = builder.EntityType<OOrder>();
openOrder.HasKey(p => p.ID);
openOrder.Property(p => p.SomeText);
openOrder.HasDynamicProperties(p => p.DynamicProperties);
builder.EntitySet<OOrder>("OOrders");
return builder;
}
}
And my patch function on my controller looks like this:
[HttpPatch]
public IHttpActionResult Patch([FromODataUri] int key, Delta<OOrder> order)
{
if (!ModelState.IsValid) return BadRequest();
using (UnitOfWork uow = ConnectionHelper.CreateSession()) {
OOrder existing = getSingle(key, uow);
if (existing != null) {
Order existingOrder = uow.GetObjectByKey<Order>(key);
order.CopyChangedValues(existing);
mapOpenWithDynamcPropertiesToPersisted(existing, existingOrder);
// Intentionally not storing changes for now
//uow.CommitChanges();
return Updated(existing);
}
else {
return NotFound();
}
}
}
private void mapOpenWithDynamcPropertiesToPersisted<TOpen, TPersisted>(TOpen open, TPersisted persisted)
where TPersisted : BaseDocument
where TOpen: IDynamicProperties {
if (open != null && persisted != null && open.DynamicProperties != null && open.DynamicProperties.Any()) {
XPClassInfo ci = persisted.ClassInfo;
foreach (string propertyName in open.DynamicProperties.Keys) {
var member = ci.FindMember(propertyName);
if (member != null) {
object val = open.DynamicProperties[propertyName];
// Here, I have tried to deserialize etc
member.SetValue(persisted, val);
}
}
}
}
After the call on order.CopyChangedValues(existing), the existing instance contains correctly encoded value in its "SomeText" property, but not in the corresponding Dynamic property.
Upvotes: 2
Views: 997
Reputation: 41
I found somewhat of an answer and it has apparently to do with myself not reading properly the articles I mention in the question. The answer seems to be an injected deserializer who uses an json converter to convert the dynamic properties, because they are apparently always in raw format. My config is now as so:
public static void Register(HttpConfiguration config) {
config.Count().Filter().OrderBy().Expand().Select().MaxTop(null);
ODataBatchHandler batchHandler = new DefaultODataBatchHandler(GlobalConfiguration.DefaultServer);
config.MapODataServiceRoute(
routeName: "ODataRoute",
routePrefix: null,
configureAction: builder => builder.AddService<IEdmModel>(ServiceLifetime.Singleton, sp => CreateODataModel())
.AddService<ODataBatchHandler>(ServiceLifetime.Singleton, bb => new DefaultODataBatchHandler(GlobalConfiguration.DefaultServer))
.AddService<IEnumerable<IODataRoutingConvention>>(ServiceLifetime.Singleton, sp => ODataRoutingConventions.CreateDefaultWithAttributeRouting("ODataRoute", config))
.AddService<Microsoft.AspNet.OData.Formatter.Serialization.ODataSerializerProvider>(ServiceLifetime.Singleton, sp => new MiTestSerializerProvider(sp))
.AddService<Microsoft.AspNet.OData.Formatter.Deserialization.ODataDeserializerProvider>(ServiceLifetime.Singleton, sp => new MiDynamicPropertiesDeserializerProvider(sp))
);
}
Where the deserializer is the important one in this context. My beginning of a deserializer looks as so (it requires a provider/implementation coupling):
public class MiDynamicPropertiesDeserializerProvider : DefaultODataDeserializerProvider
{
MiDynamicPropertiesDeserializer _edmSerializer;
public MiDynamicPropertiesDeserializerProvider(IServiceProvider rootContainer) : base(rootContainer) {
_edmSerializer = new MiDynamicPropertiesDeserializer(this);
}
public override ODataEdmTypeDeserializer GetEdmTypeDeserializer(IEdmTypeReference edmType) {
switch (edmType.TypeKind()) { // Todo: Do I need more deserializers ?
case EdmTypeKind.Entity: return _edmSerializer;
default: return base.GetEdmTypeDeserializer(edmType);
}
}
}
public class MiDynamicPropertiesDeserializer : ODataResourceDeserializer {
public MiDynamicPropertiesDeserializer(ODataDeserializerProvider serializerProvider) : base(serializerProvider) { }
private static Dictionary<Type, Func<object, object>> simpleTypeConverters = new Dictionary<Type, Func<object, object>>() {
{ typeof(DateTime), d => new DateTimeOffset((DateTime)d) } // Todo: add converters or is this too simple ?
};
public override void ApplyStructuralProperty(object resource, ODataProperty structuralProperty, IEdmStructuredTypeReference structuredType, ODataDeserializerContext readContext) {
if (structuralProperty != null && structuralProperty.Value is ODataUntypedValue) {
// Below is a Q&D mapper I am using in my test to represent properties
var tupl = WebApplication1.Models.RuntimeClassesHelper.GetFieldsAndTypes().Where(t => t.Item1 == structuralProperty.Name).FirstOrDefault();
if (tupl != null) {
ODataUntypedValue untypedValue = structuralProperty.Value as ODataUntypedValue;
if (untypedValue != null) {
try {
object jsonVal = JsonConvert.DeserializeObject(untypedValue.RawValue);
Func<object, object> typeConverterFunc;
if (jsonVal != null && simpleTypeConverters.TryGetValue(jsonVal.GetType(), out typeConverterFunc))
{
jsonVal = typeConverterFunc(jsonVal);
}
structuralProperty.Value = jsonVal;
}
catch(Exception e) { /* Todo: handle exceptions ? */ }
}
}
}
base.ApplyStructuralProperty(resource, structuralProperty, structuredType, readContext);
}
}
Thanks to everyone who has used time on this, I hope someone else will find this info useful.
Upvotes: 2