【问题标题】:OData service with multiple routes while using unbound functions使用未绑定函数时具有多个路由的 OData 服务
【发布时间】:2018-07-05 09:51:05
【问题描述】:

有谁知道如何让 .NET 服务中托管的 OData v4 与多个路由一起工作?

我有以下:

config.MapODataServiceRoute("test1", "test1", GetEdmModelTest1());
config.MapODataServiceRoute("test2", "test2", GetEdmModelTest2());

每个 GetEdmModel 方法都有映射对象。
我可以按以下方式使用服务(这工作正常):

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

但是如果我尝试调用如下函数(将不起作用):

[HttpGet]
[ODataRoute("test1/TestFunction1()")]
public int TestFunction1()
{ return 1; }

它会抛出以下错误:

控制器“Testing”中操作“TestFunction1”的路径模板“test1/TestFunction1()”不是有效的 OData 路径模板。未找到段“test1”的资源。

但是,如果我删除“test2”的“MapODataServiceRoute”,那么只有一条路线,一切正常。

我如何让它适用于多条路线?

** 我已在以下位置发布了该问题的完整示例 **
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”在使用未绑定的函数/动作时不知道如何获取控制器。
这就是我发现的大多数示例代码在尝试“推断”控制器时失败的原因。

【问题讨论】:

    标签: asp.net-web-api2 odata asp.net-web-api-routing odata-v4


    【解决方案1】:

    查看OData Versioning Sample 以获取入门知识。

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

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

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

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

    您可以看到该项目有许多不同的架构,它们有效地映射到不断发展的解决方案的版本,该图像中的非 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);
        }
    
    }
    

    【讨论】:

    • 与 OData 版本控制示例相比,这是如何实现的?我下载了示例项目,将“ODataVersionControllerSelector”与您的“DBODataHttpControllerSelector”交换,并立即得到“找到与此命名空间请求匹配的多个控制器。”。是否需要为每个“WebApiConfig.cs”文件添加命名空间?
    • 无论如何你可以给我你的WebApiConfig.cs的sn-p吗?您是否还向控制器添加了任何特殊属性?从您的 cmets 看来,您可以拥有多个具有相同名称的控制器,只要它们位于不同的命名空间中即可。
    • 我也不确定如何调用服务 url。我创建了一个文件夹结构,如您的“Controllers\DB”+“\SRG”和“\TMS”,我将“ProductsController.cs”文件放在具有匹配名称空间的那些结构下。然后我尝试了“/DB/SRG/Products”和“/DB/TMS/Products”,但这些网址都不起作用。
    【解决方案2】:

    您可以使用自定义 MapODataServiceRoute。 以下是 WebApiConfig.cs 中的示例

    控制器使用 CustomMapODataServiceRoute 注册,并且必须为每个控制器包含 typeof(NameOfController) 有点麻烦。我的一个端点有 22 个独立的控制器,但到目前为止它已经工作了。

    注册控制器 - 在同一个项目中显示两个单独的 OData 端点,都包含自定义函数

            // Continuing Education
            ODataConventionModelBuilder continuingEdBuilder = new ODataConventionModelBuilder();
            continuingEdBuilder.Namespace = "db_api.Models";
            var continuingEdGetCourse = continuingEdBuilder.Function("GetCourse");
            continuingEdGetCourse.Parameter<string>("term_code");
            continuingEdGetCourse.Parameter<string>("ssts_code");
            continuingEdGetCourse.Parameter<string>("ptrm_code");
            continuingEdGetCourse.Parameter<string>("subj_code_prefix");
            continuingEdGetCourse.Parameter<string>("crn");
            continuingEdGetCourse.ReturnsCollectionFromEntitySet<ContinuingEducationCoursesDTO>("ContinuingEducationCourseDTO");
            config.CustomMapODataServiceRoute(
                routeName: "odata - Continuing Education",
                routePrefix: "contEd",
                model: continuingEdBuilder.GetEdmModel(),
                controllers: new[] { typeof(ContinuingEducationController) }
                );
    
        // Active Directory OData Endpoint
        ODataConventionModelBuilder adBuilder = new ODataConventionModelBuilder();
            adBuilder.Namespace = "db_api.Models";
            // CMS Groups
            var cmsGroupFunc = adBuilder.Function("GetCMSGroups");
            cmsGroupFunc.Parameter<string>("user");
            cmsGroupFunc.ReturnsCollectionFromEntitySet<GenericValue>("GenericValue");
            // Departments
            var deptUsersFunc = adBuilder.Function("GetADDepartmentUsers");
            deptUsersFunc.Parameter<string>("department");
            deptUsersFunc.ReturnsCollectionFromEntitySet<ADUser>("ADUser");
            var adUsersFunc = adBuilder.Function("GetADUser");
            adUsersFunc.Parameter<string>("name");
            adUsersFunc.ReturnsCollectionFromEntitySet<ADUser>("ADUser");
            var deptFunc = adBuilder.Function("GetADDepartments");
            deptFunc.ReturnsCollectionFromEntitySet<GenericValue>("GenericValue");
            var instDeptFunc = adBuilder.Function("GetADInstructorDepartments");
            instDeptFunc.ReturnsCollectionFromEntitySet<GenericValue>("GenericValue");
            var adTitleFunc = adBuilder.Function("GetADTitles");
            adTitleFunc.ReturnsCollectionFromEntitySet<GenericValue>("GenericValue");
            var adOfficeFunc = adBuilder.Function("GetADOffices");
            adOfficeFunc.ReturnsCollectionFromEntitySet<GenericValue>("GenericValue");
            var adDistListFunc = adBuilder.Function("GetADDistributionLists");
            adDistListFunc.ReturnsCollectionFromEntitySet<GenericValue>("GenericValue");
            config.CustomMapODataServiceRoute(
                routeName: "odata - Active Directory",
                routePrefix: "ad",
                model: adBuilder.GetEdmModel(),
                controllers: new[] { typeof(DepartmentsController), typeof(CMSGroupsController)
                });
    

    创建自定义地图 OData 服务路线

    public static class HttpConfigExt
    {
        public static System.Web.OData.Routing.ODataRoute CustomMapODataServiceRoute(this HttpConfiguration configuration, string routeName,
            string routePrefix, Microsoft.OData.Edm.IEdmModel model, IEnumerable<Type> controllers)
        {            
            var routingConventions = ODataRoutingConventions.CreateDefault();
    
            // Multiple Controllers with Multiple Custom Functions
            routingConventions.Insert(0, new CustomAttributeRoutingConvention(routeName, configuration, controllers));
    
            // Custom Composite Key Convention
            //routingConventions.Insert(1, new CompositeKeyRoutingConvention());
    
            return configuration.MapODataServiceRoute(routeName, 
                                                      routePrefix, 
                                                      model, 
                                                      new System.Web.OData.Routing.DefaultODataPathHandler(), 
                                                      routingConventions,
                                                      defaultHandler: System.Net.Http.HttpClientFactory.CreatePipeline( innerHandler: new System.Web.Http.Dispatcher.HttpControllerDispatcher(configuration), 
                                                                                                                        handlers: new[] { new System.Web.OData.ODataNullValueMessageHandler() }));
        }
    }
    
    public class CustomAttributeRoutingConvention : AttributeRoutingConvention
    {
        private readonly List<Type> _controllers = new List<Type> { typeof(System.Web.OData.MetadataController) };
    
        public CustomAttributeRoutingConvention(string routeName, HttpConfiguration configuration, IEnumerable<Type> controllers)
            : base(routeName, configuration)
        {
            _controllers.AddRange(controllers);
        }
    
        public override bool ShouldMapController(System.Web.Http.Controllers.HttpControllerDescriptor controller)
        {
            return _controllers.Contains(controller.ControllerType);
        }
    }
    

    【讨论】:

    • 这个选项看起来很有希望,但不幸的是,在我们的情况下它不会“轻松”工作。我们正在尝试将几个“WCF”服务合并到 OData 中,并且我们想要公开的不同命名空间中有 100 多个控制器。如果我删除所有操作/功能,那么一切都可以开箱即用。令人失望的是,MS 尚未通过操作/功能解决此问题,但他们确实修复了使用 GET、PUT、POST 等绑定到 EF 的标准控制器的路由...如果没有其他答案显示更清洁的东西,我可能不得不退回到您的解决方案.
    • 如果有多个控制器具有相同的名称但在不同的命名空间中,这也将不起作用。前任。 contEd.TestsController 和 ad.TestsController 将抛出“发现多个类型与名为 'Tests' 的控制器匹配。如果为该请求提供服务的路由 ('contEd/(*odataPath)') 发现多个控制器定义了相同的名称但不同的命名空间,这是不受支持的。”
    • @goroth 正确,我的解决方案是解决所述问题。允许多个控制器中的未绑定功能。我还没有找到在 OData 的不同命名空间中具有相同控制器名称的解决方案,但重写 HttpControllerSelector 非常适合 WebApi 调用中的命名空间问题,正如 Chris Schaller 在另一个答案中所展示的那样。我试过合并两者,但到目前为止没有成功。
    猜你喜欢
    • 2017-05-20
    • 1970-01-01
    • 2017-03-13
    • 2021-08-13
    • 2013-08-23
    • 2010-11-14
    • 1970-01-01
    • 2021-01-05
    • 2020-10-15
    相关资源
    最近更新 更多