OData / odata.net

ODataLib: Open Data Protocol - .NET Libraries and Frameworks
https://docs.microsoft.com/odata
Other
687 stars 349 forks source link

.NET Client: Async queries require manual composition of odata query options #2839

Open Mek7 opened 9 months ago

Mek7 commented 9 months ago

Calling odata query synchronously is possible by LINQ .Where and .FirstOrDefault call on the entity set IQueryable. Calling the same query with async/await is not possible using LINQ, but you have to call AddODataQueryOption and then write odata filter syntax as a string. Or is there a better way to convert sync queries to async queries? What is the recommended way to do it?

Assemblies affected

Microsoft.OData.Client 7.20

Reproduce steps

For the context of following steps, there is ApplicationMessage entity set that has a Get operation exposed on the server. The purpose of these queries is to filter on Severity property that is of type enum. Container is the proxy class generated from server metadata.

This works (sync call):

            var result = container.ApplicationMessage
                .Where(a => a.Severity == ApplicationMessageSeverity.None)
                .ToList();

This does not work (async call) - probably because ToListAsync is an extension method of Entity Framework. There is an exception - The source IQueryable doesn't implement IAsyncEnumerable.

            var result = await container.ApplicationMessage
                .Where(a => a.Severity == ApplicationMessageSeverity.None)
                .ToListAsync();

This works:

            var result = await container.ApplicationMessage
                .AddQueryOption("$filter", "Severity eq 'None'")
                .ExecuteAsync();

But it is very ugly having to compose OData query in a string like this. It would be best to leverage existing LINQ possibilities on the IQueryable and OData client would translate the query to OData query options.

Expected result

Something like this should work to make async query as simple as sync query:

            var result = await container.ApplicationMessage
                .Where(a => a.Severity == ApplicationMessageSeverity.None)
                .ToListAsync();

Actual result

See above in Reproduce steps.

corranrogue9 commented 9 months ago

Hi, since the LINQ methods in IQueryable for OData are only constructing the query string on the client side, there's no real benefit to leverage asynchronous operations until Execute is called. Because of this, we suggest to use the LINQ methods, and then to create a new instance of DataServiceQuery<T> calling ExecuteAsync on that instance. We will add a helper method for this adapter, but until then, you can do something like this:

var queryable = container.ApplicationMessage.Where(a => a.Severity == ApplicationMessageSeverity.None);
var query = new DataServiceQuery<ApplicationMessage>(queryable.Expression, queryable.Provider as DataServiceQueryProvider);
var result = await query.ExecuteAsync();
Mek7 commented 9 months ago

Thank you, this really helped. To make it easier to use, I implemented an extension method. Maybe it helps someone else.

    using Microsoft.OData.Client;

    /// <summary>
    /// Extension of IQueryable with custom methods
    /// </summary>
    public static class QueryableExtensions
    {
        /// <summary>
        /// Executes OData query that has been composed via LINQ
        /// https://github.com/OData/odata.net/issues/2839
        /// </summary>
        /// <typeparam name="TSource">Type of current IQueryable instance</typeparam>
        /// <param name="originalQuery"></param>
        /// <returns></returns>
        public static async Task<IEnumerable<TSource>> ExecuteODataQueryAsync<TSource>(this IQueryable<TSource> originalQuery)
        {
            var query = new DataServiceQuery<TSource>(originalQuery.Expression, originalQuery.Provider as DataServiceQueryProvider);
            return await query.ExecuteAsync();
        }
    }