使用未绑定功能时具有多个路由的OData服务

gor*_*oth 5 odata asp.net-web-api-routing asp.net-web-api2 odata-v4

有谁知道如何在.NET服务中托管OData v4以使用多个路由?

我有以下内容:

config.MapODataServiceRoute("test1", "test1", GetEdmModelTest1());
config.MapODataServiceRoute("test2", "test2", GetEdmModelTest2());
Run Code Online (Sandbox Code Playgroud)

每个GetEdmModel方法都有映射对象.
我可以按照以下方式访问该服务(这很好):

http://testing.com/test1/objects1()
http://testing.com/test2/objects2()

但是,如果我尝试调用以下函数(将无法工作):

[HttpGet]
[ODataRoute("test1/TestFunction1()")]
public int TestFunction1()
{ return 1; }
Run Code Online (Sandbox Code Playgroud)

它会抛出以下错误:

在控制器"测试"动作"TestFunction1"的路径模板"TEST1/TestFunction1()"不是一个有效的OData路径模板.找不到段'test1'的资源.

然而,如果我删除"MapODataServiceRoute"为"测试2",所以只有一个路由,它所有的作品.

如何使用多个路由?

**我已在以下**发布了该问题的完整示例**
https://github.com/OData/WebApi/issues/1223

**我已经尝试了下面列出的OData版本示例以及以下问题**
https://github.com/OData/ODataSamples/tree/master/WebApi/v4/ODataVersioningSample
我之前尝试过"OData版本"示例不工作.似乎未绑定(未绑定是目标)不遵循相同的路由规则是正常的服务调用.

防爆.如果您下载"OData版本"示例并执行以下操作.

  1. 在V1 - > WebApiConfig.cs中添加
    builder.Function(nameof(Controller.ProductsV1Controller.Test)).Returns<string>();
  2. 在V2 - > WebApiConfig.cs中添加
    builder.Function(nameof(Controller.ProductsV2Controller.Test)).Returns<string>();
  3. 在V1 - > ProductsV1Controller.cs中添加
    [HttpGet] [ODataRoute("Test()")] public string Test() { return "V1_Test"; }
  4. 在V2 - > ProductsV2Controller.cs中添加
    [HttpGet] [ODataRoute("Test()")] public string Test() { return "V2_Test"; }

现在打电话给它."/ versionbyroute/v1/Test()"你会得到"V2_Test"

问题是"GetControllerName"在使用未绑定的函数/操作时不知道如何获取控制器.
这就是我尝试"推断"控制器时发现的大多数示例代码失败的原因.

Chr*_*ler 1

查看OData 版本控制示例作为入门知识。

问题的关键点通常是DefaultHttpControllerSelector按本地名称映射控制器,而不是全名/命名空间。

如果您的实体类型和控制器名称在两个 EdmModel 中都是唯一的,您无需执行任何特殊操作,它应该开箱即用。上面的示例利用了这个概念,强制您将字符串值注入控制器类的物理名称中,使它们唯一,然后重写以ODataVersionControllerSelector GetControllerName将传入路由映射到自定义控制器名称

如果控制器的唯一名称似乎很难,并且您更愿意使用完整的命名空间(意味着您的控制器名称逻辑仍然是标准的),那么您当然可以实现自己的逻辑来在覆盖时选择特定的控制器类实例DefaultHttpControllerSelector。只需覆盖SelectController即可。此方法需要返回一个HttpControllerDescriptor比示例更复杂的实例。

为了向您展示我的意思,我将发布旧项目需求的解决方案,该项目与您的项目有点不同。我有一个 WebAPI 项目管理对多个数据库的访问,这些数据库具有相似的架构,许多实体名称相同,这意味着这些控制器类将具有相同的名称。控制器由文件夹/命名空间构成,其中有一个名为 DB 的根文件夹,然后每个数据库都有一个文件夹,然后控制器就在其中。

在此输入图像描述

您可以看到该项目有许多不同的模式,它们有效地映射到不断发展的解决方案的版本,该图像中的非数据库命名空间是 OData v4、v3 和标准 REST api 的混合。让所有这些野兽共存是可能的;)

HttpControllerSelector 的此重写会检查运行时一次以缓存所有控制器类的列表,然后通过将路由前缀匹配到正确的控制器类来映射传入的路由请求。

/// <summary>
/// Customised controller for intercepting traffic for the DB Odata feeds.
/// Any route that is not prefixed with ~/DB/ will not be intercepted or processed via this controller
/// <remarks>Will instead be directed to the base class</remarks>
/// </summary>
public class DBODataHttpControllerSelector : DefaultHttpControllerSelector
{
    private readonly HttpConfiguration _configuration;

    public DBODataHttpControllerSelector(HttpConfiguration config)
        : base(config)
    {
        _configuration = config;
    }

    // From: http://www.codeproject.com/Articles/741326/Introduction-to-Web-API-Versioning
    private Dictionary<string, HttpControllerDescriptor> _controllerMap = null;
    private List<string> _duplicates = new List<string>();
    /// <summary>
    /// Because we are interested in supporting nested namespaces similar to MVC "Area"s we need to
    /// Index our available controller classes by the potential url segments that might be passed in
    /// </summary>
    /// <returns></returns>
    private Dictionary<string, HttpControllerDescriptor> InitializeControllerDictionary()
    {
        if(_controllerMap != null)
            return _controllerMap;

        _controllerMap = new Dictionary<string, HttpControllerDescriptor>(StringComparer.OrdinalIgnoreCase);

        // Create a lookup table where key is "namespace.controller". The value of "namespace" is the last
        // segment of the full namespace. For example:
        // MyApplication.Controllers.V1.ProductsController => "V1.Products"
        IAssembliesResolver assembliesResolver = _configuration.Services.GetAssembliesResolver();
        IHttpControllerTypeResolver controllersResolver = _configuration.Services.GetHttpControllerTypeResolver();

        ICollection<Type> controllerTypes = controllersResolver.GetControllerTypes(assembliesResolver);

        foreach (Type t in controllerTypes)
        {
            var segments = t.Namespace.Split(Type.Delimiter);

            // For the dictionary key, strip "Controller" from the end of the type name.
            // This matches the behavior of DefaultHttpControllerSelector.
            var controllerName = t.Name.Remove(t.Name.Length - DefaultHttpControllerSelector.ControllerSuffix.Length);

            var key = String.Format(CultureInfo.InvariantCulture, "{0}.{1}.{2}", segments[segments.Length - 2], segments[segments.Length - 1], controllerName);

            // Check for duplicate keys.
            if (_controllerMap.Keys.Contains(key))
            {
                _duplicates.Add(key);
            }
            else
            {
                _controllerMap[key] = new HttpControllerDescriptor(_configuration, t.Name, t);  
            }
        }

        // Remove any duplicates from the dictionary, because these create ambiguous matches. 
        // For example, "Foo.V1.ProductsController" and "Bar.V1.ProductsController" both map to "v1.products".
        // CS: Ahem... thats why I've opted to go 3 levels of depth to key name, but this still applies if the duplicates are there again
        foreach (string s in _duplicates)
        {
            _controllerMap.Remove(s);
        }
        return _controllerMap;
    }
    /// <summary>
    /// Because we are interested in supporting nested namespaces we want the full route
    /// to match to the full namespace (or at least the right part of it)
    /// </summary>
    /// <returns></returns>
    private Dictionary<string, HttpControllerDescriptor> _fullControllerMap = null;
    private Dictionary<string, HttpControllerDescriptor> InitializeFullControllerDictionary()
    {
        if(_fullControllerMap != null)
            return _fullControllerMap;

        _fullControllerMap = new Dictionary<string, HttpControllerDescriptor>(StringComparer.OrdinalIgnoreCase);

        // Create a lookup table where key is "namespace.controller". The value of "namespace" is the last
        // segment of the full namespace. For example:
        // MyApplication.Controllers.V1.ProductsController => "V1.Products"
        IAssembliesResolver assembliesResolver = _configuration.Services.GetAssembliesResolver();
        IHttpControllerTypeResolver controllersResolver = _configuration.Services.GetHttpControllerTypeResolver();

        ICollection<Type> controllerTypes = controllersResolver.GetControllerTypes(assembliesResolver);

        foreach (Type t in controllerTypes)
        {
            var segments = t.Namespace.Split(Type.Delimiter);

            // For the dictionary key, strip "Controller" from the end of the type name.
            // This matches the behavior of DefaultHttpControllerSelector.
            var controllerName = t.Name.Remove(t.Name.Length - DefaultHttpControllerSelector.ControllerSuffix.Length);

            var key = t.FullName;// t.Namespace + "." + controllerName;
            _fullControllerMap[key] = new HttpControllerDescriptor(_configuration, t.Name, t);  
        }

        return _fullControllerMap;
    }

    /// <summary>
    /// Select the controllers with a simulated MVC area sort of functionality, but only for the ~/DB/ route
    /// </summary>
    /// <param name="request"></param>
    /// <returns></returns>
    public override System.Web.Http.Controllers.HttpControllerDescriptor SelectController(System.Net.Http.HttpRequestMessage request)
    {
        string rootPath = "db";
        IHttpRouteData routeData = request.GetRouteData();
        string[] uriSegments = request.RequestUri.LocalPath.Split('/');
        if (uriSegments.First().ToLower() == rootPath || uriSegments[1].ToLower() == rootPath)
        {
            #region DB Route Selector
            // If we can find a known api and a controller, then redirect to the correct controller
            // Otherwise allow the standard select to work
            string[] knownApis = new string[] { "tms", "srg", "cumulus" };


            // Get variables from the route data.
            /* support version like this:
             * config.Routes.MapODataRoute(
                routeName: "ODataDefault",
                routePrefix: "{version}/{area}/{controller}",     
                model: model);
            object versionName = null;
            routeData.Values.TryGetValue("version", out versionName);

            object apiName = null;
            routeData.Values.TryGetValue("api", out apiName);

            object controllerName = null;
            routeData.Values.TryGetValue("controller", out controllerName);
             * */

            // CS: we'll just use the local path AFTER the root path
            // db/tms/contact
            // db/srg/contact
            // Implicity parse this as
            // db/{api}/{controller}
            // so [0] = ""
            // so [1] = "api"
            // so [2] = "version" (optional)
            // so [2 or 3] = "controller"

            if (uriSegments.Length > 3)
            {
                string apiName = uriSegments[2];
                if (knownApis.Contains(string.Format("{0}", apiName).ToLower()))
                {
                    string version = "";
                    string controllerName = uriSegments[3];
                    if (controllerName.ToLower().StartsWith("v")
                        // and the rest of the name is numeric
                        && !controllerName.Skip(1).Any(c => !Char.IsNumber(c))
                        )
                    {
                        version = controllerName;
                        controllerName = uriSegments[4];
                    }

                    // if the route has an OData item selector (#) then this needs to be trimmed from the end.
                    if (controllerName.Contains('('))
                        controllerName = controllerName.Substring(0, controllerName.IndexOf('('));

                    string fullName = string.Format(CultureInfo.InvariantCulture, "{0}.{1}.{2}", apiName, version, controllerName).Replace("..", ".");


                    // Search for the controller.
                    // _controllerTypes is a list of HttpControllerDescriptors
                    var descriptors = InitializeControllerDictionary().Where(t => t.Key.EndsWith(fullName, StringComparison.OrdinalIgnoreCase)).ToList();
                    if (descriptors.Any())
                    {
                        var descriptor = descriptors.First().Value;
                        if (descriptors.Count > 1)
                        {
                            descriptor = null;
                            // Assume that the version was missing, and we have implemented versioning for that controller
                            // If there is a row with no versioning, so no v1, v2... then use that
                            // if all rows are versioned, use the highest version
                            if (descriptors.Count(d => d.Key.Split('.').Length == 2) == 1)
                                descriptor = descriptors.First(d => d.Key.Split('.').Length == 2).Value;
                            else if (descriptors.Count(d => d.Key.Split('.').Length > 2) == descriptors.Count())
                                descriptor = descriptors
                                    .Where(d => d.Key.Split('.').Length > 2)
                                    .OrderByDescending(d => d.Key.Split('.')[1])
                                    .First().Value;
                            if (descriptor == null)
                                throw new HttpResponseException(
                                                    request.CreateErrorResponse(HttpStatusCode.InternalServerError,
                                                    "Multiple controllers were found that match this un-versioned request."));
                        }
                        if (descriptor != null)
                            return descriptor;
                    }

                    if (_duplicates.Any(d => d.ToLower() == fullName.ToLower()))
                        throw new HttpResponseException(
                                            request.CreateErrorResponse(HttpStatusCode.InternalServerError,
                                            "Multiple controllers were found that match this request."));
                }
            }
            #endregion DB Route Selector
        }
        else
        {
            // match on class names that match the route.
            // So if the route is odata.tms.testController
            // Then the class name must also match
            // Add in an option to doing a string mapping, so that
            // route otms can mapp to odata.tms

            // TODO: add any other custom logic for selecting the controller that you want, alternatively try this style syntax in your route config:
            //routes.MapRoute(
            //    name: "Default",
            //    url: "{controller}/{action}/{id}",
            //    defaults: new { controller = "Home", action = "RegisterNow", id = UrlParameter.Optional },
            //    namespaces: new[] { "YourCompany.Controllers" }
            //);

            // Because controller path mapping might be controller/navigationproperty/action
            // We need to check for the following matches:
            // controller.navigationproperty.actionController
            // controller.navigationpropertyController
            // controllerController

            string searchPath = string.Join(".", uriSegments).ToLower().Split('(')[0] + "controller";
            var descriptors = InitializeFullControllerDictionary().Where(t => t.Key.ToLower().Contains(searchPath)).ToList();
            if (descriptors.Any())
            {
                var descriptor = descriptors.First().Value;
                if (descriptors.Count > 1)
                {
                    descriptor = null;
                    // In this mode, I think we should only ever have a single match, ready to prove me wrong?
                    if (descriptor == null)
                        throw new HttpResponseException(
                                            request.CreateErrorResponse(HttpStatusCode.InternalServerError,
                                            "Multiple controllers were found that match this namespace request."));
                }
                if (descriptor != null)
                    return descriptor;
            }

        }
        return base.SelectController(request);
    }

}
Run Code Online (Sandbox Code Playgroud)