title | description | ms.date | author |
---|---|---|---|
Custom routing convention |
Learn how to override the default Web API OData routing convention with the OData custom routing convention. |
7/1/2019 |
madansr7 |
Applies To:[!INCLUDEappliesto-webapi][!INCLUDEappliesto-webapi]
It is easy to create custom routing conventions to override the default Web API OData routing convention.
From built-in routing convention section, we know that users should add many actions for every property access.
For example, if the client issues the following property access request URIs:
~/odata/Customers(1)/Orders
~/odata/Customers(1)/Address
~/odata/Customers(1)/Name
...
Service should have the following actions in CustomersController
to handle:
public class CustomersController : ODataController
{
public string GetOrders([FromODataUri]int key)
{
......
}
public string GetAddress([FromODataUri]int key)
{
......
}
public string GetName([FromODataUri]int key)
{
......
}
}
If Customer
has hundreds of properties, a developer should add hundreds of similar functions in CustomersController
. This is tedious, but with a custom routing convention we can override this behavior.
We can create our own routing convention class by implementing the IODataRoutingConvention
. However, if you don't want to change the behaviour to find the controller, the new added routing convention class can derive from `NavigationSourceRoutingConvention'.
Let's build a sample property access routing convention class derived from NavigationSourceRoutingConvention
.
public class CustomPropertyRoutingConvention : NavigationSourceRoutingConvention
{
private const string ActionName = "GetProperty";
public override string SelectAction(ODataPath odataPath, HttpControllerContext controllerContext, ILookup<string, HttpActionDescriptor> actionMap)
{
if (odataPath == null || controllerContext == null || actionMap == null)
{
return null;
}
if (odataPath.PathTemplate == "~/entityset/key/property" ||
odataPath.PathTemplate == "~/entityset/key/cast/property" ||
odataPath.PathTemplate == "~/singleton/property" ||
odataPath.PathTemplate == "~/singleton/cast/property")
{
var segment = odataPath.Segments[odataPath.Segments.Count - 1] as PropertyAccessPathSegment;
if (segment != null)
{
string actionName = FindMatchingAction(actionMap, ActionName);
if (actionName != null)
{
if (odataPath.PathTemplate.StartsWith("~/entityset/key", StringComparison.Ordinal))
{
KeyValuePathSegment keyValueSegment = odataPath.Segments[1] as KeyValuePathSegment;
controllerContext.RouteData.Values[ODataRouteConstants.Key] = keyValueSegment.Value;
}
controllerContext.RouteData.Values["propertyName"] = segment.PropertyName;
return actionName;
}
}
}
return null;
}
public static string FindMatchingAction(ILookup<string, HttpActionDescriptor> actionMap, params string[] targetActionNames)
{
foreach (string targetActionName in targetActionNames)
{
if (actionMap.Contains(targetActionName))
{
return targetActionName;
}
}
return null;
}
}
Where, we route the following path templates to a certain action named GetProperty
.
~/entityset/key/property
~/entityset/key/cast/property
~/singleton/property
~/singleton/cast/property
The following sample code is used to enable the customized routing convention:
HttpConfiguration configuration = ......
IEdmModel model = GetEdmModel();
IList<IODataRoutingConvention> conventions = ODataRoutingConventions.CreateDefaultWithAttributeRouting(configuration, model);
conventions.Insert(0, new CustomPropertyRoutingConvention());
configuration.MapODataServiceRoute("odata", "odata", model, new DefaultODataPathHandler(), conventions);
Where, we insert our own routing convention at the starting position to override the default Web API OData property access routing convention.
In the CustomersController
, only one method named GetProperty
should be added.
public class CustomersController : ODataController
{
[HttpGet]
public IHttpActionResult GetProperty(int key, string propertyName)
{
Customer customer = _customers.FirstOrDefault(c => c.CustomerId == key);
if (customer == null)
{
return NotFound();
}
PropertyInfo info = typeof(Customer).GetProperty(propertyName);
object value = info.GetValue(customer);
return Ok(value, value.GetType());
}
private IHttpActionResult Ok(object content, Type type)
{
var resultType = typeof(OkNegotiatedContentResult<>).MakeGenericType(type);
return Activator.CreateInstance(resultType, content, this) as IHttpActionResult;
}
}
Below are some request Uri samples to test:
a)
GET https://localhost/odata/Customers(2)/Name
The result is:
{
"@odata.context":"https://localhost/odata/$metadata#Customers(2)/Name","value": "Mike"
}
b)
GET https://localhost/odata/Customers(2)/Location
The result is:
{
"@odata.context":"https://localhost/odata/$metadata#Customers(2)/Salary","value ":2000.0
}
c)
GET https://localhost/odata/Customers(2)/Location
The result is:
{
"@odata.context":"https://localhost/odata/$metadata#Customers(2)/Location","Country":"The United States","City":"Redmond"
}