Skip to content

Controllers with same name in different namespace #37

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
ra-design opened this issue Nov 26, 2020 · 5 comments
Closed

Controllers with same name in different namespace #37

ra-design opened this issue Nov 26, 2020 · 5 comments

Comments

@ra-design
Copy link

Hi, I'm trying to update our current webapi to odata, where I have controllers with the same name in different namespaces.
The underlying entity types are in different DbContexts (different Databases), the DbContexts are in separate IEdmModels with different odata prefixes.

  • route odata/erp/customers (prefix: odata/erp) should resolve to controller WebService.Erp.Controllers.CustomersController
  • route odata/crm/customers (prefix: odata/crm) should resolve to controller WebService.Crm.Controllers.CustomersController

I'm getting an AmbiguousMatchException: The request matched multiple endpoints. when calling one endpoint and a 404 reply when requesting the other, so both requests route to the same controller.

Could someone point me in the right direction or how to tackle this problem? Am I missing something here? I tried to set a custom routing convention (https://devblogs.microsoft.com/odata/routing-in-asp-net-core-8-0-preview/), but to no effect.

Btw, i noticed that Order in EntityRoutingConvention is not virtual - don't know if for a reason, but seems odd as it's virtual in the others:

@ra-design
Copy link
Author

Finally I managed do solve this on my own. Anybody having the same issue, I'll quickly describe my solution:

1.: I introduced an interface IODataEndpointModelDeclaration, connecting the OData-prefix with the required namespace and the IEdmModel generation. I created a singleton implementing the interface for every namespace I need, create the IEdmModel by using every instance in services.AddOData(...) and add them to IoC as singletons of type IODataEndpointModelDeclaration.

    public interface IODataEndpointModelDeclaration
    {
        string ODataRoutePrefix { get; } //e.g "odata/erp"
        string ODataControllerNamespace { get; } //e.g "WebService.Erp.Controllers"
        IEdmModel GetEdmModel();
        //...
    }

2.: I introduced an IODataRoutingPrefixMatcher, that has one method Match(ODataControllerActionContext context) which verifies that the context's prefix, model and and controller's namespace match one of the declared IODataEndpointModelDeclarations

    public interface IODataRoutingPrefixMatcher
    {
        bool Match(ODataControllerActionContext context);
    }

    public class ODataRoutingPrefixMatcher : IODataRoutingPrefixMatcher
    {
        private readonly ILogger<ODataRoutingPrefixMatcher> logger;
        private readonly List<IODataEndpointModelDeclaration> modelDeclarations;
        public ODataRoutingPrefixMatcher(IServiceProvider serviceProvider,
            ILogger<ODataRoutingPrefixMatcher> logger)
        {
            this.logger = logger;
            modelDeclarations = serviceProvider.GetServices<IODataEndpointModelDeclaration>().ToList();
        }

        public bool Match(ODataControllerActionContext context)
        {
            if (context == null) return true; // do not interfere with the built-in functionality
            var currentDeclaration = modelDeclarations.FirstOrDefault(x => 
                x.GetEdmModel() == context.Model && 
                x.ODataRoutePrefix == context.Prefix);
            if (currentDeclaration == null) return true; // no declaration found, continue with built-in functionality
            var match = context.Controller.ControllerType.Namespace == currentDeclaration.ODataControllerNamespace; // all that matters
            if (match)
            {
                var msg = $"Matched convention for prefix: {context.Prefix}({context.Controller.DisplayName}) - target: {currentDeclaration.ODataControllerNamespace}";
                logger.LogInformation(msg);
            }
            return match;
        }
    }

3.: I replaced every built-in routing convention (see https://devblogs.microsoft.com/odata/routing-in-asp-net-core-8-0-preview/#built-in-conventional-routings) with a new one that uses the IODataRoutingPrefixMatcher in AppliesToController(...). E.g the EntitySetRoutingConvention:

    public class CustomEntitySetRoutingConvention : EntitySetRoutingConvention
    {
        private readonly IODataRoutingPrefixMatcher prefixMatcher;
        public CustomEntitySetRoutingConvention(IODataRoutingPrefixMatcher prefixMatcher)
        {
            this.prefixMatcher = prefixMatcher;
        }

        /// <inheritdoc />
        public override bool AppliesToController(ODataControllerActionContext context)
        {
            return prefixMatcher.Match(context) && base.AppliesToController(context);
        }
    }

So my services.AddOData call now looks like this:

services.AddOData(options =>
                    {
                        options.Select()
                            .Expand()
                            .Filter()
                            .OrderBy()
                            .Count()
                            .SkipToken()
                            .AddModel(erpEndpointDeclaration.ODataRoutePrefix, erpEndpointDeclaration.GetEdmModel())
                            .AddModel(crmEndpointDeclaration.ODataRoutePrefix, crmEndpointDeclaration.GetEdmModel());
                    })
                    .RemoveDefaultConventions() // extension method removing every IODataControllerActionConvention in IODataBuilder.Services
                    .AddConvention<CustomEntitySetRoutingConvention>()
                    .AddConvention<CustomSingletonRoutingConvention>()
                    .AddConvention<CustomEntityRoutingConvention>()
                    .AddConvention<CustomPropertyRoutingConvention>()
                    .AddConvention<CustomNavigationRoutingConvention>()
                    .AddConvention<CustomFunctionRoutingConvention>()
                    .AddConvention<CustomActionRoutingConvention>()
                    .AddConvention<CustomOperationImportRoutingConvention>()
                    .AddConvention<CustomRefRoutingConvention>();

I'm leaving the issue open, as my note that the Order-Property in EntityRoutingConvention is not virtual should be considered.

@fabiocbr75
Copy link

fabiocbr75 commented Nov 30, 2020

I had the same errors (AmbiguousMatchException) but in my case, the solution is very simple:

Add models:

 services.AddOData(opt => opt.Count().Filter().Expand().Select().OrderBy().SetMaxTop(5)
                .AddModel("v1", model1)
                .AddModel("v2", model2)

And in the controllers (in different namespaces) you need to use the attribute ODataModel.

    [ODataModel("v1")]
    public class CustomersController : ControllerBase

    [ODataModel("v2")]
    public class CustomersController : ControllerBase

And now you can call the API:

/v1/customers
/v2/customers

@xuzhg
Copy link
Member

xuzhg commented Dec 1, 2020

@ra-design Thanks for your question and workaround. We do appreciate.

Actually, just like @fabiocbr75 's comment, [ODataModelAttribute] is designed to resolve the scenario like yours. it's more easy.

For the virtual Order, i will fix it. Thanks for it and looking forward more feedback from you.

@xuzhg xuzhg added the followup label Dec 1, 2020
@xuzhg
Copy link
Member

xuzhg commented Dec 2, 2020

See "virtual Order" at 002f78c. Thanks.

@xuzhg xuzhg added the Fixed label Dec 2, 2020
@ra-design
Copy link
Author

@fabiocbr75, @xuzhg: Thank you, couldn't find this solution somehow, thanks for pointing this out!

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