Skip to content

Error: "Cannot find the services container for route" when using MapVersionedODataRoute #553

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
aniliht opened this issue Oct 6, 2019 · 14 comments
Assignees
Milestone

Comments

@aniliht
Copy link

aniliht commented Oct 6, 2019

Hello, I'm using the ASP.NET Core 2.2 and OData. I'm using the following setup in which I create the EdmModel dynamically by looking at the user's claims. Based on the claims, I add a EntitySet A or EntitySet B for example.

When I browse to the controller for EntitySet A and the user doesn't have the claim for it,
I get the following exception when I use the MapVersionedODataRoute. I don't get the exception if I use the OData routebuilder method MapODataServiceRoute(...); which returns 404 as expected instead.

Perhaps my setup is not supported or I misconfigured something? Thanks for your time.

Exception Message:

System.InvalidOperationException: 'Cannot find the services container for route 'odataV1-Unversioned'. This should not happen and represents a bug.'

StackTrace

at Microsoft.AspNet.OData.PerRouteContainerBase.GetODataRootContainer(String routeName)
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.Routing.ODataPathRouteConstraint.GetODataPath(String oDataPathString, String uriPathString, String queryString, Func`1 requestContainerFactory)
at Microsoft.AspNet.OData.Routing.ODataPathRouteConstraint.Match(HttpContext httpContext, IRouter route, String routeKey, RouteValueDictionary values, RouteDirection routeDirection)
at Microsoft.AspNet.OData.Routing.UnversionedODataPathRouteConstraint.Match(HttpContext httpContext, IRouter route, String routeKey, RouteValueDictionary values, RouteDirection routeDirection) in E:\BA\56\s\src\Microsoft.AspNetCore.OData.Versioning\AspNet.OData\Routing\UnversionedODataPathRouteConstraint.cs:line 79
at Microsoft.AspNetCore.Routing.RouteConstraintMatcher.Match(IDictionary`2 constraints, RouteValueDictionary routeValues, HttpContext httpContext, IRouter route, RouteDirection routeDirection, ILogger logger)
at Microsoft.AspNetCore.Routing.RouteBase.RouteAsync(RouteContext context)
at Microsoft.AspNetCore.Routing.RouteCollection.RouteAsync(RouteContext context)
at Microsoft.AspNetCore.Builder.RouterMiddleware.Invoke(HttpContext httpContext)
at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)

Startup.cs

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddApplicationInsightsTelemetry();
            services.Configure<AuthSettings>(Configuration.GetSection(nameof(AuthSettings)));
            var provider = services.BuildServiceProvider();
            var authSettings = provider.GetRequiredService<IOptions<AuthSettings>>();

            services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                .AddJwtBearer(options =>
                {
                    options.SaveToken = true;
                    options.Audience = authSettings.Value.Audience;
                    options.Authority = authSettings.Value.Authority;
                    options.TokenValidationParameters = new TokenValidationParameters
                    {
                        ValidateAudience = true,
                        ValidateIssuer = true,
                        ValidateLifetime = true,
                        ValidateIssuerSigningKey = true
                    };
                });

            services.AddHttpContextAccessor();
            services.AddApiVersioning();
            services.AddOData().EnableApiVersioning();

            services.AddMvc(options =>
            {
                options.EnableEndpointRouting = false;

                var policy = new AuthorizationPolicyBuilder()
                    .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
                    .RequireAuthenticatedUser()                    
                    .Build();

                options.Filters.Add(new AuthorizeFilter(policy));

            }).SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

            ConfigureApplicationServices(services);
        }        

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
#if DEBUG
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
#endif
            app.UseUnhandledExceptionMiddleware();
            app.UseAuthentication();
            app.UseHttpsRedirection();

            var routeNameV1 = "odataV1";
            app.UseMvc(routeBuilder =>
            {
                routeBuilder.MapVersionedODataRoute(routeNameV1, string.Empty, new ApiVersion(1, 0), GetODataContainerBuilder(routeNameV1, routeBuilder));
            });
        }

        private Action<IContainerBuilder> GetODataContainerBuilder(string routeName, IRouteBuilder routeBuilder)
        {
            return new Action<IContainerBuilder>(containerBuilder =>
            {
                containerBuilder.AddService<IEdmModel>(Microsoft.OData.ServiceLifetime.Scoped, sp =>
                {
                    var httpRequestScope = sp.GetRequiredService<HttpRequestScope>();
                    var claims = httpRequestScope?.HttpRequest?.HttpContext?.User?.Claims;
                    
                    return EdmModelFactory.GetEdmModel(claims);  // based on the claims, will select EntitySet to add to the model.

                });

                containerBuilder.AddService<IEnumerable<IODataRoutingConvention>>(Microsoft.OData.ServiceLifetime.Singleton, sp =>
                               ODataRoutingConventions.CreateDefaultWithAttributeRouting(routeName, routeBuilder));
            });

        }

Nuget package versions:

    <PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.8.0" />
    <PackageReference Include="Microsoft.AspNetCore.App" />
    <PackageReference Include="Microsoft.AspNetCore.Http.Extensions" Version="2.2.0" />
    <PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning" Version="3.1.6" />
    <PackageReference Include="Microsoft.AspNetCore.OData" Version="7.2.1" />
    <PackageReference Include="Microsoft.AspNetCore.OData.Versioning" Version="3.2.4" />
    <PackageReference Include="Microsoft.AspNetCore.Razor.Design" Version="2.2.0" PrivateAssets="All" />
    <PackageReference Include="Microsoft.Extensions.Configuration.AzureKeyVault" Version="2.2.0" />
@commonsensesoftware
Copy link
Collaborator

Apologies for the delayed response. This type of configuration will not work with API Versioning and OData. You cannot use the default OData routing conventions. API Versioning has to replace or extend the default routing conventions to make things work. There are, however, hooks to extend or add additional routing conventions via a callback in the route registration. You don't appear to need this, so I would just remove it. Based on the configuration you've shared, it appears that if you remove that, the rest of it should work.

I hope that helps.

@aniliht
Copy link
Author

aniliht commented Oct 28, 2019

Hello commonsensesoftware,

Thanks for your support! Can you specify which section you propose that I remove from the above configuration?

@commonsensesoftware
Copy link
Collaborator

Should definitely remove this:

containerBuilder.AddService<IEnumerable<IODataRoutingConvention>>(
  Microsoft.OData.ServiceLifetime.Singleton,
  sp => ODataRoutingConventions.CreateDefaultWithAttributeRouting(routeName, routeBuilder));

Start with that and if it still doesn't work, we can move on to what else needs to change.

@aniliht
Copy link
Author

aniliht commented Oct 28, 2019

OK, with those lines removed, it looks like the behavior is the same. I can still reach routes/EntitySets that are in the EdmModel. But when I try to browse to a non-existent route/EntitySet I get the exception from above: "Cannot find the services container for route 'odataV1-Unversioned'. This should not happen and represents a bug."

Seems like any bogus route will cause that exception. For example:
https://localhost:44395/ssss?api-version=1.0

@aniliht
Copy link
Author

aniliht commented Oct 28, 2019

I can try to build a reproducing sample project without my company's specifics if you think that would help.

@commonsensesoftware
Copy link
Collaborator

Repros are always helpful. The simpler the better.

@aniliht
Copy link
Author

aniliht commented Oct 28, 2019

Here is a simplified repro. There are 2 launch settings.

  1. "IIS Express: Bug Repro" - launches a route that doesn't exist and causes the above exception.
  2. "IIS Express: Route 'Persons'" - launches a valid route with an entity set that does exist.

Thank you for your time looking into this. Best Regards.

aspnet-api-versioning-issue553.zip

@nfgallimore
Copy link

nfgallimore commented Jan 23, 2020

@commonsensesoftware I am getting this same error as well and I have no idea what I am doing wrong or what the exception means. I wrote a detailed Stack Overflow question on it so I will delegate to that here:

I am trying to setup a new .NET Core 3.1 API with OData.

I have a working .NET Core API in 2.2 with OData, so I am going off of that one.

I am using three OData specific nuget packages: Microsoft.AspNetCore.OData v7.3.0, Microsoft.AspNetCore.OData.Versioning v4.1.1 and Microsoft.AspNetCore.OData.Versioning.ApiExplorer v4.1.1.

I keep getting the following response when I send a GET request to my controller:

System.InvalidOperationException: Cannot find the services container for route 'odata-Unversioned'. This should not happen and represents a bug.
  at Microsoft.AspNet.OData.PerRouteContainerBase.GetODataRootContainer(String routeName)
  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.Routing.ODataPathRouteConstraint.<>c__DisplayClass0_0.<Match>b__0()
  at Microsoft.AspNet.OData.Routing.ODataPathRouteConstraint.GetODataPath(String oDataPathString, String uriPathString, String queryString, Func`1 requestContainerFactory)
  at Microsoft.AspNet.OData.Routing.ODataPathRouteConstraint.Match(HttpContext httpContext, IRouter route, String routeKey, RouteValueDictionary values, RouteDirection routeDirection)
  at Microsoft.AspNet.OData.Routing.UnversionedODataPathRouteConstraint.Match(HttpContext httpContext, IRouter route, String routeKey, RouteValueDictionary values, RouteDirection routeDirection)
  at Microsoft.AspNetCore.Routing.RouteConstraintMatcher.Match(IDictionary`2 constraints, RouteValueDictionary routeValues, HttpContext httpContext, IRouter route, RouteDirection routeDirection, ILogger logger)
  at Microsoft.AspNetCore.Routing.RouteBase.RouteAsync(RouteContext context)
  at Microsoft.AspNetCore.Routing.RouteCollection.RouteAsync(RouteContext context)
  at Microsoft.AspNetCore.Builder.RouterMiddleware.Invoke(HttpContext httpContext)
  at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
  at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
  at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)

Note I have the requirement that the API needs to be versioned. I am not sure if versioning has something to do with this error, but the old .NET Core 2.2 API that I have uses OData versioning as well. Does anyone know what the services container is for a route?

I setup my project similar to https://devblogs.microsoft.com/odata/experimenting-with-odata-in-asp-net-core-3-1/.
I also even tried setting it up exactly as is done in the blog post, however I could not even get OData to work without versioning. When I tried removing versioning, I kept getting a 404 and it couldn't hit the route. Is my routing setup correctly?

Here is the ConfigureServices

public void ConfigureServices(IServiceCollection services)
{
    // Add API versioning
    services.AddApiVersioning(options => options.ReportApiVersions = true);

    // Add OData
    services.AddOData().EnableApiVersioning();
    services.AddODataApiExplorer(options =>
    {
        options.GroupNameFormat = "'v'VVV";
        options.SubstituteApiVersionInUrl = true;
    });

    // Add support for controllers and API-related features
    services.AddControllers(mvcOptions => mvcOptions.EnableEndpointRouting = false);
}

Here is the Configure

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseHttpsRedirection();
    app.UseRouting();
    app.UseAuthorization();

    var apiVersion = new ApiVersion(1, 0);
    app.UseMvc(routeBuilder =>
    {
        routeBuilder.Count().Filter().OrderBy().Expand().Select().MaxTop(null);
        app.UseMvc(routes =>
            routes.MapVersionedODataRoute("odata", "api/v{version:apiVersion}", app.GetEdmModel(), apiVersion));
    });
}

Here is the extension method GetEdmModel() I am using above

public static class ODataExtensions
{
    public static IEdmModel GetEdmModel(this IApplicationBuilder app)
    {
        var model = GetEdmModel(app.ApplicationServices);
        return model;
    }

    private static IEdmModel GetEdmModel(IServiceProvider serviceProvider)
    {
        var builder = new ODataConventionModelBuilder(serviceProvider);

        // Entities
        builder.EntityType<Object>().HasKey(x => x.Id);

        // Controllers
        builder.EntitySet<Object>("Object");

        return builder.GetEdmModel();
    }
}

Here is the controller

[Route("odata")]
[ApiVersion("1")] 
public class can : ODataController
{
    private readonly ILogicService _logicService;

    public AlternateIdsController(ILogicService logicService)
    {
        _logicService = logicService;
    }

    [HttpGet]
    [EnableQuery]
    public IActionResult Get()
    {
        return Ok(_logicService.Browse());
    }
}

https://stackoverflow.com/questions/59881062/odata-returning-cannot-find-the-services-container-for-route-odata-unversioned

@commonsensesoftware
Copy link
Collaborator

@nfgallimore it doesn't look like the OData mappings match any supported configuration. Your EDM has a MyObject entity set, but that's not the controller you've shown (and I would have expected it to be MyObjects). The AlternateIdsController isn't for an entity set nor seems to provide any unbound functions or actions. Finally, you can thank the OData team for continuing to have their own attributes which are completely different in meaning and use from the standard routing attributes. You need to use [ODataRoutePrefix] and [ODataRoute] respectively. It's fine to use [HttpGet], but OData doesn't really care about it. I suspect with those few changes, you should make some progress.

The Swagger/OpenAPI example here for OData has a number of different scenarios covered, including:

  • Routing with attributes
  • Routing by convention
  • Entity sets
  • Unbound functions
  • Bound functions
  • Bound actions
  • Containment
  • Entity references

Start with that and then we can move on to other potential issues.

@nfgallimore
Copy link

I named it ObjectController and setup the EdmModel to map to Objects

"Objects" in GetEdmModel() vs "ObjectController" in Controller class name.

I just figured that out, thank you sir.

@aniliht
Copy link
Author

aniliht commented Jan 23, 2020

Regarding the repro I posted, pls let me know in case it does not work. If there's anything else I could do lmk as well. Thanks.

@commonsensesoftware
Copy link
Collaborator

I can confirm that this is, in fact, a bug. The OData team occasionally changes the behavior of the internal mechanics and it isn't always hard to catch. API Versioning uses a a special catch-all route for OData to handle the scenario when a requested version doesn't match anything. This behavior will result in the built-in ODataPathRouteConstraint attempting to create a request container. No root container is configured for this route since it doesn't actually do anything. It will result in the appropriate 400 or 404 instead of a 500. Since the container is now required, but not registered, it blows up with 500. As far as I can tell, this only happens when you request an entity that doesn't exist.

The fix is forthcoming and will go into the next path. Thanks.

@commonsensesoftware
Copy link
Collaborator

One more question on this thread. It's been a year (yikes - sorry for that) and I noticed this issue is mentioning ASP.NET Core 2.2. Has everyone since moved to 3.1? I hope most people have moved forward. There's a massive set of changes for OData coming to address all the fixes, including Endpoint Routing. If I have break out specific changes for for 2.2, I'll have do it separately. It probably won't come fast either - unfortunately. Multiple, parallel branches are difficult to maintain. If anyone says the really need 2.2 support, I'll track it separately. Thanks.

@commonsensesoftware commonsensesoftware added this to the 5.0.0 milestone Oct 4, 2020
@aniliht
Copy link
Author

aniliht commented Oct 5, 2020

Hi, I will move onto 3.1 sometime. From my perspective, please do what's best for the long term. Thank you greatly for your support!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants