Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

OData & WebAPI routing conflict

I have a project with WebAPI controllers. I'm now adding OData controllers to it. The problem is that my OData controller has the same name as an existing WebAPI controller, and that leads to an exception:

Multiple types were found that match the controller named 'Member'. This can happen if the route that services this request ('OData/{*odataPath}') found multiple controllers defined with the same name but differing namespaces, which is not supported. The request for 'Member' has found the following matching controllers: Foo.Bar.Web.Areas.API.Controllers.MemberController Foo.Bar.Web.Odata.Controllers.MemberController

And this happens even though the controllers are in different namespaces and should have distinguishable routes. Here is a summary of the config that I have. What can I do (besides renaming the controller) to prevent this exception? I'm trying expose these endpoints as:

mysite.com/OData/Members
mysite.com/API/Members/EndPoint

It seems to me that the URLs are distinct enough that there's gotta be some way to configure routing so there's no conflict.

namespace Foo.Bar.Web.Odata.Controllers {

    public class MemberController : ODataController {
        [EnableQuery]
        public IHttpActionResult Get() {
            // ... do stuff with EF ...
        }
    }
}

namespace Foo.Bar.Web.Areas.API.Controllers {

    public class MemberController : ApiControllerBase {
        [HttpPost]
        public HttpResponseMessage EndPoint(SomeModel model) {
            // ... do stuff to check email ...
        }
    }
}

public class FooBarApp : HttpApplication {

    protected void Application_Start () {
        // ... snip ...

        GlobalConfiguration.Configure(ODataConfig.Register);
        AreaRegistration.RegisterAllAreas();
        RouteConfig.RegisterRoutes(RouteTable.Routes);

        // ... snip ...
    }
}

public static class ODataConfig {
    public static void Register(HttpConfiguration config) {
        config.MapODataServiceRoute(
            routeName: "ODataRoute",
            routePrefix: "OData",
            model: GetModel());
    }

    public static Microsoft.OData.Edm.IEdmModel GetModel() {
        // ... build edm models ...
    }
}

namespace Foo.Bar.Web.Areas.API {
    public class APIAreaRegistration : AreaRegistration {
        public override string AreaName {
            get { return "API"; }
        }

        public override void RegisterArea(AreaRegistrationContext context) {
            var route = context.Routes.MapHttpRoute(
                "API_default",
                "API/{controller}/{action}/{id}",
                new { action = RouteParameter.Optional, id = RouteParameter.Optional }
            );
        }
    }
}
like image 762
Matthew Groves Avatar asked Aug 20 '15 12:08

Matthew Groves


1 Answers

If you have two controllers with same names and different namespaces for api and OData you can use this code. First add this class:

public class ODataHttpControllerSelector : DefaultHttpControllerSelector
{
    private readonly HttpConfiguration _configuration;
    private readonly Lazy<ConcurrentDictionary<string, Type>> _apiControllerTypes;

    public ODataHttpControllerSelector(HttpConfiguration configuration)
        : base(configuration)
    {
        _configuration = configuration;
        _apiControllerTypes = new Lazy<ConcurrentDictionary<string, Type>>(GetControllerTypes);
    }

    public override HttpControllerDescriptor SelectController(HttpRequestMessage request)
    {
        return this.GetApiController(request);
    }

    private static ConcurrentDictionary<string, Type> GetControllerTypes()
    {
        var assemblies = AppDomain.CurrentDomain.GetAssemblies();

        var types = assemblies
            .SelectMany(a => a
                .GetTypes().Where(t =>
                    !t.IsAbstract &&
                    t.Name.EndsWith(ControllerSuffix, StringComparison.OrdinalIgnoreCase) &&
                    typeof(IHttpController).IsAssignableFrom(t)))
            .ToDictionary(t => t.FullName, t => t);

        return new ConcurrentDictionary<string, Type>(types);
    }

    private HttpControllerDescriptor GetApiController(HttpRequestMessage request)
    {
        var isOData = IsOData(request);
        var controllerName = GetControllerName(request);
        var type = GetControllerType(isOData, controllerName);

        return new HttpControllerDescriptor(_configuration, controllerName, type);
    }

    private static bool IsOData(HttpRequestMessage request)
    {
        var data = request.RequestUri.ToString();
        bool match = data.IndexOf("/OData/", StringComparison.OrdinalIgnoreCase) >= 0 ||
            data.EndsWith("/OData", StringComparison.OrdinalIgnoreCase);
        return match;
    }

    private Type GetControllerType(bool isOData, string controllerName)
    {
        var query = _apiControllerTypes.Value.AsEnumerable();

        if (isOData)
        {
            query = query.FromOData();
        }
        else
        {
            query = query.WithoutOData();
        }

        return query
            .ByControllerName(controllerName)
            .Select(x => x.Value)
            .Single();
    }
}

public static class ControllerTypeSpecifications
{
    public static IEnumerable<KeyValuePair<string, Type>> FromOData(this IEnumerable<KeyValuePair<string, Type>> query)
    {
        return query.Where(x => x.Key.IndexOf(".OData.", StringComparison.OrdinalIgnoreCase) >= 0);
    }

    public static IEnumerable<KeyValuePair<string, Type>> WithoutOData(this IEnumerable<KeyValuePair<string, Type>> query)
    {
        return query.Where(x => x.Key.IndexOf(".OData.", StringComparison.OrdinalIgnoreCase) < 0);
    }

    public static IEnumerable<KeyValuePair<string, Type>> ByControllerName(this IEnumerable<KeyValuePair<string, Type>> query, string controllerName)
    {
        var controllerNameToFind = string.Format(CultureInfo.InvariantCulture, ".{0}{1}", controllerName, DefaultHttpControllerSelector.ControllerSuffix);

        return query.Where(x => x.Key.EndsWith(controllerNameToFind, StringComparison.OrdinalIgnoreCase));
    }
}

It drives DefaultHttpControllerSelector and you should add this line at the end of Register method inside WebApiConfig.cs file:

config.Services.Replace(typeof(IHttpControllerSelector), new ODataHttpControllerSelector(config));

Notes:

It uses controller's namespace to determine that controller is OData or not. So you should have namespace YourProject.Controllers.OData for your OData controllers and in contrast for API controllers, it should not contains OData word in the namespace.

Thanks to Martin Devillers for his post. I used his idea and a piece of his code!

like image 69
Mahdi Ataollahi Avatar answered Oct 30 '22 01:10

Mahdi Ataollahi