Following the very cool series of ODATA articles on the ASP.NET site by Mike Wasson you will see that it is possible to define actions not only on entities directly, but also (unbound) on entity sets and the service root itself. While it is fairly straightforward to implement entity set action (the same as an entity action but without the key parameter) the same is not so obvious for unbound actions at the service itself. So following the standard procedure of defining a service route, service root action and its implementation, it could look similar to this:
// WebApiConfig.cs public static class WebApiConfig { public static void Register(HttpConfiguration config) { config.Routes.MapODataServiceRoute( routeName: "Utilities.svc" , routePrefix: "Utilities.svc" , model: GetModel("Utilities") , batchHandler: new DefaultODataBatchHandler(GlobalConfiguration.DefaultServer) ); config.MapHttpAttributeRoutes(); } private static IEdmModel GetModel(string ContainerName) { ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); builder.ContainerName = ContainerName; var builderAction = builder.Action("ServiceRootAction"); builderAction.Parameter<string>("inputParam1"); builderAction.Returns<string>(); return builder.GetEdmModel(); } } // NonbindableActionsController.cs with service root action [HttpPost] public async Task<IHttpActionResult> ServiceRootAction(ODataActionParameters parameters) { string fn = string.Format("{0}:{1}.{2}", this.GetType().Namespace, this.ControllerContext.ControllerDescriptor.ControllerName, this.ControllerContext.Request.GetActionDescriptor().ActionName); Debug.WriteLine(fn); if (!ModelState.IsValid) { return BadRequest(ModelState); } try { if(null == parameters) { return BadRequest("Body cannot be null."); } var inputParam1 = parameters.Where(p => p.Key.Equals("param1")).Select(p => p.Value).FirstOrDefault(); if (null == inputParam1) { return BadRequest("inputParam1: Parameter cannot be null."); } return Ok<string>(string.Format("{0}: Seng!", inputParam1)); } catch (Exception ex) { Debug.WriteLine(String.Format("{0}: {1}\r\n{2}", ex.Source, ex.Message, ex.StackTrace)); throw; } }
However when invoking the action you end up either with a plain HTTP 404 telling that no controller could be found.
=== REQUEST === POST http://www.example.com/Utilities.svc/ServiceRootAction HTTP/1.1 Content-Length: 25 Content-Type: application/json {"inputParam1":"tralala"} === RESPONSE === HTTP/1.1 404 Not Found { "Message": "No HTTP resource was found that matches the request URI 'http://www.example.com/Utilities.svc/ServiceRootAction'.", "MessageDetail": "No route providing a controller name was found to match request URI 'http://www.example.com/Utilities.svc/ServiceRootAction'" }
This seems strange as the service root action is not bound to any entity set, and so it is not clear which controller should be used for that. The solution to this: it actually does not matter in which controller the method/action Is defined. As WebAPI does not support NonBindable actions out of the box anyway, we have to manually add support for this in the call to “MapODataServiceRoute()”.
So when having a look at the ASP.NET code samples on their Codeplex Git one can see that we have to implement a class for the “IODataRoutingConvention” interface that acts a helper to select the right controller. Quoting the example code from Mike Wasson Supporting OData Actions in ASP.NET Web API 2 this will end up looking like this:
// WebApiConfig.cs public static class WebApiConfig { public static void Register(HttpConfiguration config) { var conventions = ODataRoutingConventions.CreateDefault(); conventions.Insert(0, new NonBindableActionRoutingConvention("NonBindableActions")); config.Routes.MapODataServiceRoute( routeName: "Utilities.svc" , routePrefix: "Utilities.svc" , model: GetModel("Utilities") , pathHandler: new DefaultODataPathHandler() , routingConventions: conventions , batchHandler: new DefaultODataBatchHandler(GlobalConfiguration.DefaultServer) ); config.MapHttpAttributeRoutes(); } private static IEdmModel GetModel(string ContainerName) { ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); builder.ContainerName = ContainerName; var builderAction = builder.Action("ServiceRootAction"); builderAction.Parameter<string>("inputParam1"); builderAction.Returns<string>(); return builder.GetEdmModel(); } } // NonBindableActionRoutingConvention.cs using System.Linq; using System.Net.Http; using System.Web.Http.Controllers; using System.Web.Http.OData.Routing; using System.Web.Http.OData.Routing.Conventions; using Microsoft.Data.Edm; namespace LightSwitchApplication { // Implements a routing convention for non-bindable actions. // The convention maps "MyAction" to Controller:MyAction() method, where the name of the controller // is specified in the constructor. public class NonBindableActionRoutingConvention : IODataRoutingConvention { private string _controllerName; public NonBindableActionRoutingConvention(string controllerName) { _controllerName = controllerName; } // Route all non-bindable actions to a single controller. public string SelectController(ODataPath odataPath, System.Net.Http.HttpRequestMessage request) { if (odataPath.PathTemplate == "~/action") { return _controllerName; } return null; } // Route the action to a method with the same name as the action. public string SelectAction(ODataPath odataPath, HttpControllerContext controllerContext, ILookup<string, HttpActionDescriptor> actionMap) { // OData actions must be invoked with HTTP POST. if (controllerContext.Request.Method == HttpMethod.Post) { if (odataPath.PathTemplate == "~/action") { ActionPathSegment actionSegment = odataPath.Segments.First() as ActionPathSegment; IEdmFunctionImport action = actionSegment.Action; if (!action.IsBindable && actionMap.Contains(action.Name)) { return action.Name; } } } return null; } } } // taken from ASP.NET Codeplex ODATA v3 code samples, http://aspnet.codeplex.com/sourcecontrol/latest#Samples/WebApi/OData/v3/ODataActionsSample/ODataActionsSample/
You add a routing convention on how to route NonBindableActions and place these actions into a “fake” controller “NonBindableActionsController” that only holds these actions. You could certainly also place them into any other controller, but as they do not really belong to any entity set it is preferable to have them separate.
The information on this topic is relatively sparse and you really have to look at the code of the samples to find this out. This is a little bit unfortunate and time consuming. A short answer to this can actually be found at StackOverflow: http://stackoverflow.com/questions/15798915/asp-net-web-api-odata-action-on-the-edm-model-root (when looking for the right keyword).
Very interesting. I’m having the same problem with OData 4. However the odata 4 samples include an example of an unbound action without apparently using a custom convention.
Hi Martin, thanks for your information. To what sample are you referring? As for v4 I cannot find an example on CodePlex for unbound actions. If you sent me the link I would like to take a look into that. Thanks, Ronald
Hi Ronald,
Click the link labelled Download at this url https://aspnet.codeplex.com/SourceControl/latest
The zip is pretty large, but contains loads of samples, including odata v3 and v4 (Samples\WebApi\OData\v4)
Cheers
Martin
FYI, I have found that adding custom conventions to the odata4 custom actions sample breaks the custom actions in the sample. In fact having
config.MapODataServiceRoute(“OData”, “odata”, GetEdmModel()
,pathHandler: new DefaultODataPathHandler()
, routingConventions: ODataRoutingConventions.CreateDefault()
);
breaks the unbound actions in the sample.
So the question is, how to add custom conventions for CompositeKeyRouting, and CreateNavigationPropertyRouting, without breaking the custom actions?
Martin
http://forums.asp.net/p/2011361/5785499.aspx?p=True&t=635477405415609901&pagenum=1 contains the resolution
Thanks for solving the problem and sharing the link!