简体   繁体   English

OData和WebAPI路由冲突

[英]OData & WebAPI routing conflict

I have a project with WebAPI controllers. 我有一个带有WebAPI控制器的项目。 I'm now adding OData controllers to it. 我现在正在添加OData控制器。 The problem is that my OData controller has the same name as an existing WebAPI controller, and that leads to an exception: 问题是我的OData控制器与现有的WebAPI控制器具有相同的名称,这导致异常:

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. 在我看来,URL足够明显,必须有一些方法来配置路由,所以没有冲突。

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 }
            );
        }
    }
}

You'll want to include namespace constraint on your WebAPI: 您需要在WebAPI上包含名称空间约束:

var route = context.Routes.MapHttpRoute(
            name: "API_default",
            routeTemplate: "API/{controller}/{action}/{id}",
            defaults:new { action = RouteParameter.Optional, id = RouteParameter.Optional },
        );
route.DataTokens["Namespaces"] = new string[] {"Foo.Bar.Web.Areas.API.Controllers"];

If you are getting conflicts for view controllers, you should be able to include a similar namespace constraint as: 如果您遇到视图控制器的冲突,您应该能够包含类似的命名空间约束:

routes.MapRoute(
            name: "ViewControllers_Default",
            url: "{controller}/{action}/{id}",
            defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional, area = "" },
            namespaces: new[]{"Foo.Bar.Web.Controllers"}
        );

If you have two controllers with same names and different namespaces for api and OData you can use this code. 如果有两个控制器具有相同的名称和apiOData不同命名空间,则可以使用此代码。 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: 它驱动DefaultHttpControllerSelector ,你应该在WebApiConfig.cs文件中的Register方法的末尾添加这一行:

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

Notes: 笔记:

It uses controller's namespace to determine that controller is OData or not. 它使用控制器的命名空间来确定控制器是否为OData。 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. 因此,您应该为OData控制器设置namespace YourProject.Controllers.OData ,而对于API控制器,它不应该包含命名空间中的OData字。

Thanks to Martin Devillers for his post. 感谢Martin Devillers的发帖。 I used his idea and a piece of his code! 我用他的想法和他的代码!

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM