The Microsoft OData library Roadmap? Please some insights?

WP_20140923_005Microsoft has taken the road of open source and the community. Right, but I guess that this is the reason, that some libraries, in the past maintained by strictly managed teams, now are more or less ‘loosely’ managed.

About OData and bugs and questions, I have seen comments in the community, that suggest that Microsoft abandons maintenance on oData, however, I doubt this is the case, since Azure heavily depends on Odata.

For the reason of better versioning and no dependencies on Windows Updates for .NET components, .NET library has been split up, into smaller parts, the future part is ‘.NET Core’ and the current path, but popular as well, is .NET for which we see a mysterious overlap in versions, so say, if you target your assembly, you could target it using Visual Studio 2015 and C# to both as well .NET 462 and .Net Core 1.6 (standard core library). My focus on this posting, is not the .NET versions but the OData versions.

It looks as if OData 5.8.x is still the most popular used OData Service library, while Microsoft guys, also created a System.WEb.Odata 6.0.0 version, which is compatible with the ‘Next’ platform of .NET (say, .NET Core, I am not quite sure what exactly is going on here).

To make it worse, if you develop on Azure services, some clients, such as Azure Search Index Client, use OData 5.8 while DocumentDB already likes 6.0.0, these two odata branches, bite each other.

Anyway, OData 6.0.0 has improved functionality when we talk about modelling, but it has a lot of BUGS in the Controller handling section, that still, since the release of it have not been solved.

So, If you have these libraries, which are from the nuget packages.config file, read on!

<package id="Microsoft.AspNet.OData" version="6.0.0" targetFramework="net462" />

<package id="Microsoft.OData.Core" version="7.1.1" targetFramework="net462" />
<package id="Microsoft.OData.Edm" version="7.1.1" targetFramework="net462" />

For complete details about the bug, I have created an Issue on Github, where the OData WebApi Git Repository resides.

Now the question, how to workaround the BUG?

Good news, you can workaround it, but the workaround requires you to break some consistancy with your OData API behavior. I tried to minimize the damage as much as possible AND if Microsoft fixes these, you just could rename a few things and your OData API Controller, should work on!

Second, I just could post the binaries here, and say: “Good luck with it!” But it also explains a few other things

  1. How to intercept an OData server method by getting binary data and do it yourselves?
  2. How to deal with ‘custom’ serialisation on Odata, see, using Newtonsoft.JSON. (Which is possible, but I do not recommend it for 6.0.0)
  3. How to impress your team members with this neat OData PATCH-patch? Glimlach

 

My Odata Controller layout

Say, this is my Controller. You will recognize it, it’s not very different probably.

[EnableQuery]
[Authorize]
[ODataRoutePrefix("companies")]
public class CompaniesController : BaseODataController

private static readonly ODataValidationSettings settings = new ODataValidationSettings()
       {
           // Initialize settings as needed.
           AllowedFunctions = AllowedFunctions.IndexOf | AllowedFunctions.ToLower | AllowedFunctions.All | AllowedFunctions.Any | AllowedFunctions.AllStringFunctions //includes contains
       };

public CompaniesController(IManager<company> companyManager, // DEAL with IoC, just a sample
       )
    {
        _companyManager = companyManager;
        }

try
           {
               options.Validate(settings);
           }
           catch (Exception ex)
           {
               return BadRequest(ex.Message);
           }
           try
           {
               var data = (await _companyManager.SearchAsync(options));

               return Ok(data);
           }
           catch (Exception ex)
           {
               return BadRequest(ex.Message);
           }

 


       [EnableQuery]
     public async Task<IHttpActionResult> Get(ODataQueryOptions<company> options)
     {

try
           {
               options.Validate(settings);
           }
           catch (Exception ex)
           {
               return BadRequest(ex.Message);
           }
           try
           {
               var data = (await _companyManager.SearchAsync(options));

               return Ok(data);
           }
           catch (Exception ex)
           {
               return BadRequest(ex.Message);
           }

}

 

public async Task<IHttpActionResult> Delete(Guid key)
     {
         if (!ModelState.IsValid)
         {
             return BadRequest(ModelState);
         }
         var result = await _companyManager.DeleteAsync(key);
         if (!result)
         {
             return BadRequest(_companyManager.GetValidator());
         }
         return this.StatusCode(HttpStatusCode.NoContent);
     }

public async Task<IHttpActionResult> Post(company c)
     {
         if (!ModelState.IsValid)
         {
             return BadRequest(ModelState);
         }
         c.id = Guid.NewGuid();

   var success = await _companyManager.InsertAsync(c);

if (!success)
              {
                  return BadRequest(_companyManager.GetValidator());
              }
    return Created(c);

}


// NOW we come to the BUG on Odata 6.0.0. The OData PATCH method would be USEless if you consider the fact, that complex entitytypes canNOT be patched as of the moment of writing.

public async Task<IHttpActionResult> Patch(Guid key, [DeltaBody] Data.OData.Delta<company> delta)
     {

         if (delta == null)
         {
             return BadRequest("delta cannot be null");
         }
         var instance = delta.GetEntity();

         Validate(instance);

         if (!ModelState.IsValid)
         {
             return BadRequest(ModelState);
         }
        
         var curCompany = await _companyManager.GetById(key);
         if (curCompany == null)
         {
             return NotFound();
         }
             delta.Patch(curCompany);

           try
         {

             var result = await _companyManager.UpdateAsync(curCompany);
             if (!result)
             {
                 return BadRequest(_companyManager.GetValidator());
             }
             if (WantContent)
             {
                 return Content(HttpStatusCode.Created, curCompany);
             }
             return Updated(curCompany);
         }
         catch (Exception ex)
         {

//yadda
                 }
     }

DeltaBodyAttribute my little Gem!

Rick Strahl on Rick Strahls custom Body filter helped me to customize my PATCH method. Normally, the Delta<> would be like System.Web.OData.Delta<company>. But now we have to tweak this code into some Delta override which is a duplicate code of OData 5.8 where the Delta Patch works.

AS you can see, your PATCH method, only needs this adaption, and later on, you could change it back to the intended OData behavior if Microsoft fixes the library version 6.0.0 or higher.

Keep in mind!

  1. Since we use NewtonSoft serialisation, ODataConventionModelBuilder, is ignored. All configuration, such as how to deal with nullables, capitalisation, complex property serialisation, must be emulated by Newtonsoft. OData has it’s own serialisation fabric, and especially for 6.0.0 the samples simply don’t work, and documentation lacks. (Enough with the rants, for now)
  2. Your model, that normally applies to Odata, must be copied so Newtonsoft works.
  3. I don’t use camelcase/ruby on rails naming conventions and JSonProperty tricks for our frontend HTML consumers, to get my C# naming conventions ‘right’. So my classes (POC) are really like this:  public string first_name {get;set;}
    This is to avoid further complex compatibility code, between OData entitytypes and our ‘patch’ that uses Newtonsoft. 
  4. if you use dynamic_properties, for your entitymodel, which OData supports, they are called Open Type definitions, this might bite
  5. Exceptions, during serialisation, will be Newtonsoft exceptions, not OData exceptions.

If you keep these changes in mind, maybe, you could live with this workaround? Because after all,

DeltaBodyAttribute

using System;
using System.Web.Http;
using System.Web.Http.Controllers;


// see https://github.com/RickStrahl/AspNetWebApiArticle/tree/master/AspNetWebApi

namespace MyMuke.Data.OData
{
    /// <summary>
    /// An attribute that captures the entire content body and stores it
    /// into the parameter of type <see cref="Data.OData.Delta{TEntityType}"/>
    /// </summary>
    /// <remarks>
    /// The parameter marked up with this attribute should be the only parameter as it reads the
    /// entire request body and assigns it to that parameter.   
    /// </remarks>
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)]
    public sealed class DeltaBodyAttribute : ParameterBindingAttribute
    {
        /// <summary>
        ///
        /// </summary>
        /// <param name="parameter"></param>
        /// <returns></returns>
        public override HttpParameterBinding GetBinding(HttpParameterDescriptor parameter)
        {
            if (parameter == null)
                throw new ArgumentNullException(nameof(parameter));

            return new DeltaBodyParameterBinding(parameter);
        }
    }
}

DeltaBodyParameterBinding


using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Http.Controllers;
using System.Web.Http.Metadata;

 

namespace MyMuke.Data.OData
{
    /// <summary>
    /// Reads the Request body into a Delta&lt;&gt; <see cref="Delta{TEntityType}"/>
    /// assigns it to the parameter bound.
    ///
    /// Should only be used with a single parameter on
    /// a Web API method using the [RawBody] attribute
    /// </summary>
    public class DeltaBodyParameterBinding : HttpParameterBinding
    {
        /// <summary>
        /// ctor
        /// </summary>
        /// <param name="descriptor"></param>
        public DeltaBodyParameterBinding(HttpParameterDescriptor descriptor)
            : base(descriptor)
        {

        }

        /// <summary>
        /// Check for simple
        /// </summary>
        /// <param name="metadataProvider"></param>
        /// <param name="actionContext"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        public override Task ExecuteBindingAsync(ModelMetadataProvider metadataProvider,
                                                    HttpActionContext actionContext,
                                                    CancellationToken cancellationToken)
        {
 
            if (!actionContext.Request.Method.Method.Equals("PATCH", StringComparison.InvariantCultureIgnoreCase))// PUT ~Patch is considered
                return Task.FromResult(0);

           var binding = actionContext
                .ActionDescriptor
                .ActionBinding;
            var type = binding
                        .ParameterBindings.FirstOrDefault(f => f is DeltaBodyParameterBinding)
                        .Descriptor.ParameterType;

            if (type.IsGenericType && type.GetGenericTypeDefinition().IsAssignableFrom(typeof(Data.OData.Delta<>)))
            {
                return actionContext.Request.Content
                        .ReadAsStreamAsync()
                        .ContinueWith((task) =>
                        {
                            // create instance of e.g. Delta<company>()
                            var delta = (Delta)Activator.CreateInstance(typeof(Data.OData.Delta<>).MakeGenericType(type.GetGenericArguments()));

                            var jOb = default(JToken);

                            using (var mem = new MemoryStream((int)task.Result.Length))
                            {
                                task.Result.CopyTo(mem);
                                mem.Position = 0;
                                var sr = new StreamReader(mem);

                                // deserialize twice
                                // one time as JTOken, the other time to get TEntityType
                                try
                                {
                                    var ser = new JsonSerializer()
                                    {
                                        PreserveReferencesHandling = PreserveReferencesHandling.All,
                                        MissingMemberHandling = MissingMemberHandling.Ignore//you dont want JsonRequired to cause problems here
                                    };
                                   // ser.Converters.Add(new ArrayReferencePreservngConverter()); HERE YOUR OWN solution for self referencing types

                                    jOb = JToken.ReadFrom(new JsonTextReader(sr));
                                }
                                catch (Exception ex)
                                {
                                    var msg = ex.Message;
                                    throw ex;
                                }
                            }
                            JToken token;
                            if ((token = jOb["id"]) != null)//not the primary key can be patched todo check on KeyAttribute
                            {
                                token.Parent.Remove();
                            }
                            DeltaCopyUtil.CopyEntityToDelta(delta, jOb);

                            SetValue(actionContext, delta);
                        });
            }
        

            throw new InvalidOperationException("Only Delta parameters");
        }
        /// <summary>
        /// returns blah
        /// </summary>
        public override bool WillReadBody
        {
            get
            {
                return true;
            }
        }
    }
}

DeltaCopyUtil

public static class DeltaCopyUtil {

 

static void CopyEntityToDelta(Delta delta, JToken jOb) {

           var keysGivenInJson = jOb.Children().Select(s => s.Name).ToList();

          object baseObjFromJSON = jOb.ToObject(((TypedDelta)delta).EntityType);

         var backupType = baseObjFromJSON.GetType();

             var tobeCopied = backupType.GetProperties() .Where(p => keysGivenInJson.Contains(p.Name) && (p.GetSetMethod() != null || p.PropertyType.IsCollection()) && p.GetGetMethod() != null) .Select>(p => new FastPropertyAccessor(p)) .ToDictionary(p => p.Property.Name);

      foreach (var prop in tobeCopied)

     {

      var val = prop.Value.GetValue(baseObjFromJSON);

             var result = delta.TrySetPropertyValue(prop.Key, val);

        if (result == false)

       {

         Trace.TraceWarning("CopyEntityToDelta: Cannot set property {0} on Entity type {1}", prop.Key, backupType.Name); } }

            //any keys are in JSON, while not in the EntityType as a property?, They are OData OpenType

             var keysdynamic = keysGivenInJson.Where(s => !tobeCopied.Keys.Contains(s));

             if (keysdynamic?.Any() ?? false)

            { 

                     var entityValue = ((dynamic)delta).GetEntity();

                   //copy to delta.GetEntity() the dynamic properties

                  var targetEntCollection = backupType.GetProperty("dynamic_properties", BindingFlags.SetProperty | BindingFlags.Public | BindingFlags.Instance);

                 if (targetEntCollection != null)

               {

                 targetEntCollection.SetValue(entityValue, keysdynamic.ToDictionary(k => k, jOb.Value) ) ; }

               }

          }

}

The rest of the code

I will not bother you with the rest of the code. It is a cherry pick of the code that I got from GitHub that works for OData 5.8.0 where the Patch method works it was fixed there, but not ‘here’ Glimlach 

You can download the complete code, including on this page FIX OData 6.0.0 Patch Method.

This also means, that we have var instance = delta.GetEntity(); instead of var instance = delta.GetInstance() (a typical change that OData 6.0.0 includes compared to 5.8.x)

 

I hope, this saves you from many hours, days, of frustrating research, on trying to get OData do for you what you expect it to do.