System.NotSupportedException when calling OData service from NetCoreApp2.1

邮差的信 提交于 2021-02-04 14:13:16

问题


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 ?


回答1:


As mentioned in the error message, the platform only supports asynchronous fetches. Even after you use that, you will likely need to enumerate over the results multiple times -- everytime you perform a ToList(), FirstOrDefault() or other similar System.Generics.Collections operations, you are essentially getting the Enumerator of the collection and enumerating over it.

I adopted this solution: immediately after I fetch enumerable results from the OData libraries I enumerate over them and put them in another enumerable container (Dictionary<string, MyAwesomeResult> in this case) instantiated by me.

var resultsQuery = this.oDataClient.MyAwesomeResults
    .AddQueryOption("$filter", "Name eq 'MyAwesomeName'")
    .AddQueryOption("$top", "5")
    .AddQueryOption("$skip", "2");

IEnumerable<MyAwesomeResult> resultsRaw = await 
resultsQuery.ExecuteAsync();
var results = new Dictionary<string, MyAwesomeResult>();`

foreach (var resultRaw in resultsRaw)
{
    results.Add(resultRaw.Key, resultRaw);
}

Then I use the container I instantiated -- I no longer need to enumerate again over the enumerable returned by DataServiceQuery<MyAwesomeResult>.ExecuteAsync.




回答2:


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.



来源:https://stackoverflow.com/questions/53043994/system-notsupportedexception-when-calling-odata-service-from-netcoreapp2-1

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!