Search code examples
.net-coreodata

System.NotSupportedException when calling OData service from NetCoreApp2.1


I have set up a multi targetting (net4.5.2/netstandard2) class library allowing to consume one of our enterprise OData services. To access this OData service we use a proxy class generated with the OData v4 Client Code Generator (v7.5.0)

Unfortunately, when trying to use my library in a Netcoreapp2.1 application I encounter an issue as soon as I try to enumerate a collection.

Container.MyDataSet.ToList(); produces the following exception :

"System.NotSupportedException : This target framework does not enable you to directly enumerate over a data service query. This is because enumeration automatically sends a synchronous request to the data service. Because this framework only supports asynchronous operations, you must instead call the BeginExecute and EndExecute methods to obtain a query result that supports enumeration."

I do not encounter this issue when using this same multitarget library in a .Net 4.5.2 application.

Having a look at the Microsoft.OData.Client v7.5.0 source code, this behaviour seems to be by design with specific handling of the .Net Core case.

Did I miss something ?

The following code prevents the issue, but it is barely usable :

var query = (DataServiceQuery<MyData>)Container.MyDataSet;
var taskFactory = new TaskFactory<IEnumerable<MyData>>();
var t = taskFactory.FromAsync(query.BeginExecute(null, null), data => query.EndExecute(data));
t.ConfigureAwait(false);
IEnumerable<MyData> result = t.Result;

How can I use an OData IQueryable in .Net Core application without adding specific code ?


Solution

  • As said by @PanagiotisKanavos DataServiceQuery.ToString() will return the uri of the OData query. Based on this, I wrote my own IQueryable :

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Linq;
    using System.Linq.Expressions;
    
    using Microsoft.OData.Client;
    
    public class ODataLinqQuery<T> : IOrderedQueryable<T>
    {
        public IQueryProvider Provider { get; }
    
        private DataServiceQuery<T> DataServiceQuery { get; }
    
        public ODataLinqQuery(DataServiceQuery<T> dataServiceQuery, MyClient client, Type finalType)
        {
            this.DataServiceQuery = dataServiceQuery;
            this.Provider = new ODataLinqQueryProvider<T>(dataServiceQuery, client, finalType);
        }
    
        public IEnumerator<T> GetEnumerator()
        {
            return this.Provider.Execute<IEnumerable<T>>(this.Expression).GetEnumerator();
        }
    
        IEnumerator IEnumerable.GetEnumerator()
        {
            return this.Provider.Execute<System.Collections.IEnumerable>(this.Expression).GetEnumerator();
        }
    
        public Expression Expression => this.DataServiceQuery.Expression;
    
        public Type ElementType => typeof(T);
    }
    

    Where MyClient is an utility class which wraps an HttpClient, handles authentication token, and performs result deserialization. FinalType is to keep track on the type I want to obtain and deserialize, as I am handling IQueryables over interfaces. Then I wrote my own IQueryProvider :

    using System;
    using System.Collections;
    using System.Linq;
    using System.Linq.Expressions;
    using System.Net.Http;
    
    using Microsoft.OData.Client;
    
    public class ODataLinqQueryProvider<T> : IQueryProvider
    {
        private MyClient Client { get; set; }
    
        private DataServiceQuery<T> DataServiceQuery { get; set; }
    
        private Type FinalType { get; }
    
        public ODataLinqQueryProvider(
            DataServiceQuery<T> dsq,
            MyClient client,
            Type finalType)
        {
            this.DataServiceQuery = dsq;
            this.Client = client;
            this.FinalType = finalType;
        }
    
        public IQueryable CreateQuery(Expression expression)
        {
            return new ODataLinqQuery<T>(this.DataServiceQuery, this.Client, this.FinalType);
        }
    
        public IQueryable<TElement> CreateQuery<TElement>(Expression expression)
        {
            var pro = new DataServiceQuery<TElement>(expression, this.DataServiceQuery.Provider as DataServiceQueryProvider);
            return new ODataLinqQuery<TElement>(pro, this.Client, this.FinalType);
        }
    
        public object Execute(Expression expression)
        {
            this.DataServiceQuery = new DataServiceQuery<T>(expression, this.DataServiceQuery.Provider as DataServiceQueryProvider);
            return this.Execute();
        }
    
        public TResult Execute<TResult>(Expression expression)
        {
            this.DataServiceQuery = new DataServiceQuery<T>(expression, this.DataServiceQuery.Provider as DataServiceQueryProvider);
            var res = this.Execute();
            if (typeof(IEnumerable).IsAssignableFrom(typeof(TResult)))
            {
                return (TResult)res;
            }
            else
            {
                return ((IEnumerable)res).Cast<TResult>().FirstOrDefault();
            }
        }
    
        private object Execute()
        {
            var result = Client.GetResult(typeof(OData<>).MakeGenericType(this.FinalType), HttpMethod.Get, new Uri(this.DataServiceQuery.ToString())) as OData;
            return result.Objects;
        }
    }
    

    Where Odata<> class is just for deserialization of the OData result and GetResult "just" invokes the GetAsync method of its underlying HttpClient with the correct authentication headers, wait for and deserializes the result :

    using System.Collections.Generic;
    
    using Newtonsoft.Json;
    
    public class OData<T> : OData where T : class
    {
        public override IEnumerable<object> Objects => this.Value;
    
        public List<T> Value { get; set; }
    }
    
    public class OData
    {
        [JsonProperty("@odata.context")]
        public string Metadata { get; set; }
    
        public virtual IEnumerable<object> Objects { get; set; }
    }
    

    Finally I expose my IQueryable as follows :

    var myQueryable = new ODataLinqQuery<MyData>(this.Container.MyDataSet, myclient, typeof(MyData));
    

    I can then apply filters, orderby, top and skip and get the results as with a standard IQueryable. I know that this implementation is not complete, and IQueryable to OData is not as complete as most IQueryable to SQL, but it achieves the minimum I need.