OData service with multiple routes when using unrelated functions

Does anyone know how to get OData v4 hosted in a .NET service to work with multiple routes?

I have the following:

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

Each of the GetEdmModel methods has associated objects.
I can access the service as follows (this works fine):

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

But if I try to call a function like the following (will not work):

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

It produces the following error:

The path pattern 'test1 / TestFunction1 ()' in the action 'TestFunction1' in the controller 'Testing' is not a valid OData path pattern. Resource not found for segment 'test1'.

However, if I remove the "MapODataServiceRoute" for "test2", so there is only one route, it all works.

How do I get this to work with multiple routes?

** I posted a full example of the problem on the following **
https://github.com/OData/WebApi/issues/1223

** I tried the sample OData version below with the following problems **
https://github.com/OData/ODataSamples/tree/master/WebApi/v4/ODataVersioningSample
I tried the "OData Version" example before and it did not work. It seems that unbound (unbound is the target) does not follow the same routing rules as regular service calls.

Ex. If you download the OData Version example and follow these steps.

  • In V1 -> WebApiConfig.cs add
    builder.Function(nameof(Controller.ProductsV1Controller.Test)).Returns<string>();
  • In V2 -> WebApiConfig.cs add
    builder.Function(nameof(Controller.ProductsV2Controller.Test)).Returns<string>();
  • In V1 -> Products V1Controller.cs add
    [HttpGet] [ODataRoute("Test()")] public string Test() { return "V1_Test"; }
  • In V2 -> ProductsV2Controller.cs add
    [HttpGet] [ODataRoute("Test()")] public string Test() { return "V2_Test"; }

Now call it. "/ versionbyroute / v1 / Test ()" and you will get "V2_Test"

The problem is that "GetControllerName" does not know how to get the controller when it uses unrelated functions / actions.
This is why most of the code examples that I discovered fail when trying to "output" the controller.

+5
source share
2 answers

Take a look at the OData Versioning Sample for primer.

The key to a problem is usually that DefaultHttpControllerSelector matches controllers by local name, not by name / namespace.

If your entity type and therefore controller names are unique to both EdmModels, you don’t need to do anything special, it should just work out of the box. The above example uses this concept, forcing you to enter a string value in the physical names of controller classes to make them unique, and then override ODataVersionControllerSelector GetControllerName to match the incoming route with the names of custom controllers

If the unique names of the controllers seem difficult, and you would prefer to use the full namespace (this means that the logic of the name of the controllers remains standard), you can, of course, implement your own logic to select a specific instance of the controller class when overriding DefaultHttpControllerSelector . just replace the SelectController . This method will need to return an instance of the HttpControllerDescriptor , which is slightly more involved than the sample.

To show you what I mean, I will send a solution to a request from an older project that was slightly different from yours. I have one WebAPI project that controls access to several databases, these databases have a similar scheme, many Entity names are the same, which means that these controller classes will have the same name. The controllers are structured by folders / namespaces, so there is a root folder called DB, then there is a folder for each database, and then controllers.

enter image description here

You can see that in this project there are many different schemes, they are effectively compared with versions of the evolving solution, the non-DB namespaces in this image are a combination of OData v4, v3 and standard REST-apis. It is possible that all these animals coexist;)

This override of the HttpControllerSelector checks the runtime once to cache a list of all controller classes, and then displays incoming route requests by matching the route prefix with the correct controller class.

 /// <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); } } 
+1
source

You can use Custsom MapODataServiceRoute. Below is an example from WebApiConfig.cs

Controllers are registered using CustomMapODataServiceRoute, and its kind of cumbersome should include typeof(NameOfController) for each controller. One of my endpoints has 22 separate controllers, but so far it has worked.

Registration of controllers . Display two separate OData endpoints in one project. Both contain custom features.

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

Create a custom route OData service route

 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); } } 
+1
source

Source: https://habr.com/ru/post/1274962/


All Articles