Search code examples
c#asp.net-web-apicontrollerodata

Adding a new OData controller fails existing controller


I am building a sample from two Web API OData samples, each of them works fine as a separate project. But when I add second ODataController class, then the site no longer works complaining about OData path templates that worked previously. Here are more details:

The following action works fine as long as its controller (ProductsController) is the only controller:

[HttpGet]
[ODataRoute("GetSalesTaxRate(state={state})")]
public IHttpActionResult GetSalesTaxRate([FromODataUri] string state)
{
    return Ok(GetRate(state));
}

Now I add a new controller (MoviesController) with a few actions.

I extend Owin Startup class so it looks like this:

public void Configuration(IAppBuilder builder)
{
    var config = new HttpConfiguration();

    config.MapODataServiceRoute(routeName: "functions route", routePrefix: "functions", model: FunctionStartup.GetEdmModel());
    config.MapODataServiceRoute(routeName: "actions route", routePrefix: "actions", model: ActionStartup.GetEdmModel());

    builder.UseWebApi(config);
}

However, when I try to execute a Web request (URLBASE/functions/$metadata), I get the following error:

System.InvalidOperationExceptionThe path template 'GetSalesTaxRate(state={state})' on the action 'GetSalesTaxRate' in controller 'Products' is not a valid OData path template. Resource not found for the segment 'GetSalesTaxRate'.

Controllers are mapped to different routes ("functions" and "actions"). Can be that the problem is that each route is mapped to its own EdmModel?

UPDATE. I checked that I can add more controllers as long as they refer to the same EDM model. But once I introduce a second model (and reference it from MapODataServiceRoute), then the whole service breaks. Is there any workaround to support multiple models?

UPDATE 2. If I subclass DefaultHttpControllerTypeResolver and only enable single controller (any of them), then is also works fine. But I am still puzzled why multiple controllers using different models fail.


Solution

  • By default, when map OData attribute route conventions, the default logic of HTTP controller selector IHttpControllerSelector uses HttpConfiguration's DefaultAssembloesResolver, which will look up all controller types in an app domain. The scope could be reduced to controllers belong to a model.

    We can customize the MapODataServiceRoute extension methods. Some code snippet:

    public class Startup
    {
        public void Configuration(IAppBuilder builder)
        {
            var config = new HttpConfiguration();
    
            config.CustomMapODataServiceRoute(routeName: "functions route", routePrefix: "functions",
                model: FunctionStartup.GetEdmModel(),
                controllers: new[] { typeof(ProductsController) });
            config.CustomMapODataServiceRoute(routeName: "actions route", routePrefix: "actions",
                model: ActionStartup.GetEdmModel(),
                controllers: new[] { typeof(MoviesController) });
    
            config.EnsureInitialized();
    
            builder.UseWebApi(config);
        }
    }
    
    public class CustomAttributeRoutingConvention : AttributeRoutingConvention
    {
        private readonly List<Type> _controllers = new List<Type> { typeof(MetadataController) };
    
        public CustomAttributeRoutingConvention(IEdmModel model, HttpConfiguration configuration, IEnumerable<Type> controllers)
            : base(model, configuration)
        {
            _controllers.AddRange(controllers);
        }
    
        public override bool ShouldMapController(HttpControllerDescriptor controller)
        {
    
            return _controllers.Contains(controller.ControllerType);
        }
    }
    
    public static class HttpConfigExt
    {
        public static ODataRoute CustomMapODataServiceRoute(this HttpConfiguration configuration, string routeName,
            string routePrefix, IEdmModel model, IEnumerable<Type> controllers)
        {
            var routingConventions = ODataRoutingConventions.CreateDefault();
            routingConventions.Insert(0, new CustomAttributeRoutingConvention(model, configuration, controllers));
            return configuration.MapODataServiceRoute(routeName, routePrefix, model, new DefaultODataPathHandler(),
                routingConventions);
        }
    }