Skip to content
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

Restier cannot handle more than one API registered per app. #527

Closed
robertmclaws opened this issue Sep 27, 2016 · 4 comments
Closed

Restier cannot handle more than one API registered per app. #527

robertmclaws opened this issue Sep 27, 2016 · 4 comments

Comments

@robertmclaws
Copy link
Collaborator

It appears that the design for Restier did not take into account the possibility that more than one API could be registered with inside a single project.

Assemblies affected

RESTier 1.0.0-beta

Reproduce steps

Register 2 APIs in a single project using MapRestierRoute.

Expected result

I would expect a request to the service to return normally.

Actual result

The Dependency Injection system is throwing an error because there is more than one model registered, but the call is apparently calling the LINQ .Single() function, which only expects one element. It should likely be calling .FirstOrDefault() instead and checking for a null result.

{
  "Message": "An error has occurred.",
  "ExceptionMessage": "One or more errors occurred.",
  "ExceptionType": "System.AggregateException",
  "StackTrace": "   at System.Threading.Tasks.Task.ThrowIfExceptional(Boolean includeTaskCanceledExceptions)\r\n   at System.Threading.Tasks.Task`1.GetResultCore(Boolean waitCompletionNotification)\r\n   at System.Threading.Tasks.Task`1.get_Result()\r\n   at Microsoft.Restier.Core.RestierContainerBuilder.<AddRestierService>b__0(IServiceProvider sp)\r\n   at Microsoft.Extensions.DependencyInjection.ServiceLookup.FactoryService.Invoke(ServiceProvider provider)\r\n   at Microsoft.Extensions.DependencyInjection.ServiceProvider.ScopedCallSite.Invoke(ServiceProvider provider)\r\n   at Microsoft.Extensions.DependencyInjection.ServiceProvider.SingletonCallSite.Invoke(ServiceProvider provider)\r\n   at Microsoft.Extensions.DependencyInjection.ServiceProvider.<>c__DisplayClass12_0.<RealizeService>b__0(ServiceProvider provider)\r\n   at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetService(Type serviceType)\r\n   at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(IServiceProvider provider, Type serviceType)\r\n   at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService[T](IServiceProvider provider)\r\n   at System.Web.OData.Routing.DefaultODataPathHandler.Parse(String serviceRoot, String odataPath, IServiceProvider requestContainer, Boolean template)\r\n   at System.Web.OData.Routing.DefaultODataPathHandler.Parse(String serviceRoot, String odataPath, IServiceProvider requestContainer)\r\n   at System.Web.OData.Routing.ODataPathRouteConstraint.Match(HttpRequestMessage request, IHttpRoute route, String parameterName, IDictionary`2 values, HttpRouteDirection routeDirection)\r\n   at System.Web.Http.Routing.HttpRoute.ProcessConstraint(HttpRequestMessage request, Object constraint, String parameterName, HttpRouteValueDictionary values, HttpRouteDirection routeDirection)\r\n   at System.Web.Http.Routing.HttpRoute.ProcessConstraints(HttpRequestMessage request, HttpRouteValueDictionary values, HttpRouteDirection routeDirection)\r\n   at System.Web.Http.Routing.HttpRoute.GetRouteData(String virtualPathRoot, HttpRequestMessage request)\r\n   at System.Web.Http.WebHost.Routing.HttpWebRoute.GetRouteData(HttpContextBase httpContext)",
  "InnerException": {
    "Message": "An error has occurred.",
    "ExceptionMessage": "Sequence contains more than one element",
    "ExceptionType": "System.InvalidOperationException",
    "StackTrace": "   at System.Linq.Enumerable.Single[TSource](IEnumerable`1 source)\r\n   at Microsoft.Restier.Providers.EntityFramework.ModelProducer.GetModelAsync(ModelContext context, CancellationToken cancellationToken)\r\n   at Microsoft.Restier.Publishers.OData.Model.RestierModelBuilder.<GetModelAsync>d__0.MoveNext()\r\n--- End of stack trace from previous location where exception was thrown ---\r\n   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)\r\n   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)\r\n   at Microsoft.Restier.Publishers.OData.Model.RestierModelExtender.ModelBuilder.<GetModelReturnedByInnerHandlerAsync>d__17.MoveNext()\r\n--- End of stack trace from previous location where exception was thrown ---\r\n   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)\r\n   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)\r\n   at Microsoft.Restier.Publishers.OData.Model.RestierModelExtender.ModelBuilder.<GetModelAsync>d__12.MoveNext()\r\n--- End of stack trace from previous location where exception was thrown ---\r\n   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)\r\n   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)\r\n   at Microsoft.Restier.Publishers.OData.Model.RestierOperationModelBuilder.<GetModelAsync>d__4.MoveNext()\r\n--- End of stack trace from previous location where exception was thrown ---\r\n   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)\r\n   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)\r\n   at Microsoft.Restier.Core.ApiBaseExtensions.<GetModelAsync>d__0.MoveNext()"
  }
}
@chinadragon0515
Copy link
Contributor

chinadragon0515 commented Sep 28, 2016

I checked the source code, it is not directly two API (In fact, we create one container per API, it should not cause issue).
But for a single DBContext, its model contains more than one EntityContainer (concept and class from entityframework). Can I know how your DbContext is defined? Do you manually registered it into DI? Can you write some test case for your DbContext like newing one, then get the EntityContainer to see how many it will return?

BTW, we will have China National Holiday from 10/1-10/7, and I will take three additional leave (9/30, 10/8-10/9), the response will be slow.

We are welcome and appreciated any contributions.

For the code which throws exception per call stack you paste.

        var dbContext = context.GetApiService<DbContext>();

        var efModel = (dbContext as IObjectContextAdapter).ObjectContext.MetadataWorkspace;
        var efEntityContainer = efModel.GetItems<EntityContainer>(DataSpace.CSpace).Single();

@robertmclaws
Copy link
Collaborator Author

I am not using more than one EntityContainer, and I'm only using one DbContext per API. I tried registering custom ModelBuilders as per the 1.0.0 beta after the error was thrown the first time, but that had no effect.

Technically, this code should probably be looping through each Container, shouldn't it? At the very least, it should be calling .FirstOrDefault(), not .Single(). Also, if I'm passing in my own ModelBuilder, I'm not sure why this code is even being called in the first place.

@chinadragon0515
Copy link
Contributor

  1. The code expect exact one container for one DbContext, I have not digged into whether it will have more than one container for one DbContext, will do some investigation when I have time and see whether we should improve here. Can you help do the test with your DbContext to see whether it do have more than one container? And what does each container have?
  2. About why the code is called first. In RESTier DI part, a chain service is introduced and ModelBuilder uses the chain service. (You can refer to the document model builder part how we do the chain, and in the meanwhile, we do plan to simplify the end user experience here via redesign the model extension mechanism)
    In the model producer code, you can see it will call its own logic, then call inner handler get model method. So this mean the service you register is called first, then call configureAPi method.

If you do not want any restier model builder code been called. You need to call configureApi method first, then register your service. This means your service will be the end of the chain and it will be called first, and in your service, you can decide whether to call services registered before your service.

@robertmclaws
Copy link
Collaborator Author

So, we've had the discussion about how what you described is not how DI is supposed to work, I won't re-hash it. When I changed the code to .FirstOrDefault() and tried to execute run my API project again, both API endpoints work, but they are returning the same configuration. So the query that was introduced into ModelProducer.GetModelAsync() is incorrect somehow. I'll dig into it tomorrow and see if I can find a fix, but I have a feeling this is the wrong DI implementation coming back to bite us.

Once I find the problem, the unit tests will need to be updated to check that more than one API pointing to more than one DbContext can be registered properly and return the proper metadata.

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

No branches or pull requests

2 participants