Mathean
Mathean

Reputation: 63

Mocking ODataQueryOptions on .Net Core

Recently we decided to use the package Microsoft.AspNetCore.OData for our solution in our services. The services have the ODataQueryOptions parameter and use it for filtering the data they provide. To unit test this I need to Mock ODataQueryOptions somehow. It used to be easier with System.Web.Http.OData.Query.ODataQueryOptions because you could create it with HttpRequestMessage as parameter but not any more.

I have this code

 public static ODataQueryOptions<T> Create<T>(string url = "", Action<ODataConventionModelBuilder> reconfigure = null)
               where T : class
    {
        var serviceCollection = new ServiceCollection();
        serviceCollection.AddMvcCore().AddApplicationPart(typeof(ODataFactory).Assembly);

        ODataConventionModelBuilder builder = new ODataConventionModelBuilder(serviceCollection.BuildServiceProvider());
        builder.EntitySet<T>("Entity");

        reconfigure?.Invoke(builder);

        ODataQueryContext context = new ODataQueryContext(builder.GetEdmModel(), typeof(T), new Microsoft.AspNet.OData.Routing.ODataPath());

        var httpContext = new DefaultHttpContext();

        var httpRequest = new DefaultHttpRequest(httpContext);

        // throws exception: Value cannot be null. Parameter name: provider
        return new ODataQueryOptions<T>(context, httpRequest);
    }

This code throws the next exception:

System.ArgumentNullException: Value cannot be null. Parameter name: provider at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService[T](IServiceProvider provider) at Microsoft.AspNet.OData.Extensions.HttpRequestExtensions.CreateRequestScope(HttpRequest request, String routeName) at Microsoft.AspNet.OData.Extensions.HttpRequestExtensions.CreateRequestContainer(HttpRequest request, String routeName) at Microsoft.AspNet.OData.Extensions.HttpRequestExtensions.GetRequestContainer(HttpRequest request) at Microsoft.AspNet.OData.Query.ODataQueryOptions..ctor(ODataQueryContext context, HttpRequest request) at Microsoft.AspNet.OData.Query.ODataQueryOptions1..ctor(ODataQueryContext context, HttpRequest request) at Services.Test.Internal.ODataFactory.Create[T](String url, Action1 reconfigure) in C:\Users\wboun\source\repos\Services.Test\Internal\ODataFactory.cs:line 36

Upvotes: 5

Views: 5989

Answers (3)

Mauro Bilotti
Mauro Bilotti

Reputation: 6252

According with @mccow002 answer, I just post the ODataHelper with the namespaces needed (because this gave me trouble too) that I have made for using it in Unit Tests that needs ODataQueryOptions for making them work:

using Microsoft.AspNet.OData;
using Microsoft.AspNet.OData.Builder;
using Microsoft.AspNet.OData.Extensions;
using Microsoft.AspNet.OData.Query;
using Microsoft.AspNet.OData.Query.Validators;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OData.UriParser;
using Moq;
using System;

namespace Surgent.Evaluation.Tests.xUnit.Helpers
{
    public static class ODataHelper
    {
        public static ODataQueryOptions<T> Create<T>(string uri = "")
                where T : class
        {
            var provider = GetServiceProvider();
            var routeBuilder = new RouteBuilder(Mock.Of<IApplicationBuilder>(x => x.ApplicationServices == provider));
            routeBuilder.EnableDependencyInjection();            

            var modelBuilder = new ODataConventionModelBuilder(provider);
            modelBuilder.EntitySet<T>(nameof(T));
            var model = modelBuilder.GetEdmModel();

            var http = new DefaultHttpContext();
            var uri = new Uri(uri);

            http.Request.Method = "GET";
            http.Request.Host = new HostString(uri.Host, uri.Port);
            http.Request.Path = uri.LocalPath;
            http.Request.QueryString = new QueryString(uri.Query);
            http.RequestServices = provider;
                        
            HttpRequest request = http.Request;
            var context = new ODataQueryContext(model, typeof(T), new Microsoft.AspNet.OData.Routing.ODataPath());

            return new ODataQueryOptions<T>(context, request);
        }

        private static ServiceProvider GetServiceProvider()
        {
            var collection = new ServiceCollection();

            collection.AddMvcCore();
            collection.AddOData();
            collection.AddTransient<ODataUriResolver>();
            collection.AddTransient<ODataQueryValidator>();
            collection.AddTransient<TopQueryValidator>();
            collection.AddTransient<FilterQueryValidator>();
            collection.AddTransient<SkipQueryValidator>();
            collection.AddTransient<OrderByQueryValidator>();

            return collection.BuildServiceProvider();
        }
    }
}

Upvotes: 1

mccow002
mccow002

Reputation: 6914

I figured out how to get past the Value cannot be null. Parameter name: provider error and the Cannot find the services container for the non-OData route.

First off, when you create the DefaultHttpContext object, you have to set RequestServices to collection.BuildServiceProvider().

Next, you have to EnableDependencyInjection. I was able to do that by using Moq.

var routeBuilder = new RouteBuilder(Mock.Of<IApplicationBuilder>(x => x.ApplicationServices == provider));
routeBuilder.EnableDependencyInjection();

My full code is:

var collection = new ServiceCollection();

collection.AddOData();
collection.AddODataQueryFilter();
collection.AddTransient<ODataUriResolver>();
collection.AddTransient<ODataQueryValidator>();
collection.AddTransient<TopQueryValidator>();
collection.AddTransient<FilterQueryValidator>();
collection.AddTransient<SkipQueryValidator>();
collection.AddTransient<OrderByQueryValidator>();

var provider = collection.BuildServiceProvider();

var routeBuilder = new RouteBuilder(Mock.Of<IApplicationBuilder>(x => x.ApplicationServices == provider));
routeBuilder.EnableDependencyInjection();

var modelBuilder = new ODataConventionModelBuilder(provider);
modelBuilder.EntitySet<MyType>("MyType");
var model = modelBuilder.GetEdmModel();

var uri = new Uri("http://localhost/api/mytype/12345?$select=Id");

var httpContext = new DefaultHttpContext{
    RequestServices = provider
};
HttpRequest req = new DefaultHttpRequest(httpContext)
{
    Method = "GET",
    Host = new HostString(uri.Host, uri.Port),
    Path = uri.LocalPath,
    QueryString = new QueryString(uri.Query)
};

var context = new ODataQueryContext(model, typeof(MyType), new Microsoft.AspNet.OData.Routing.ODataPath());
var options = new ODataQueryOptions<MyType>(context, req);

Upvotes: 13

Mathean
Mathean

Reputation: 63

After going trough the code for ODataQueryOptions we found the solution.

The main issue was missing initialization of needed objects in the service providers which is passed to the ODataConventionModelBuilder constructor

ServiceProvider GetServiceProvider()
    {
        var collection = new ServiceCollection();

        collection.AddMvc();
        collection.AddOData();
        collection.AddTransient<ODataUriResolver>();
        collection.AddTransient<ODataQueryValidator>();
        collection.AddTransient<TopQueryValidator>();
        collection.AddTransient<FilterQueryValidator>();
        collection.AddTransient<SkipQueryValidator>();
        collection.AddTransient<OrderByQueryValidator>();

        return collection.BuildServiceProvider();
    }

Then the HttpRequest can be mocked with

 var uri = new Uri(url);

        HttpRequest request = new DefaultHttpRequest(http) {
            Method = "GET",
            Host = new HostString(uri.Host, uri.Port),
            Path = uri.LocalPath,
            QueryString = new QueryString(uri.Query)
        };

Upvotes: 1

Related Questions