Custom routing convention

Applies To:# OData WebApi v7 for aspnet webapi supported Green circle with a checkmark inside it. OData AspNet WebApi V7# OData Webapi for Webapi supported Green circular checkmark icon to indicate a success. OData AspNet WebApi V6

It is easy to create custom routing conventions to override the default Web API OData routing convention.

Property access 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.

Custom routing convention

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

Enable customized routing convention

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.

Add actions

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

Samples

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"
}