Search code examples
c#asp.netasp.net-mvcasp.net-web-api2odata

How to prevent HTTP 404 for a custom action in an odata controller?


I have an ASP.Net WebApi2 project hosting odata both ApiController and ODataController.

And I want to add a custom action in an ODataController.

I saw this seems to be achievable by either adding [HttpPost] attribute on the desired action, or by configuring the ODataConventionModelBuilder with a specific FunctionConfiguration when using the MapODataServiceRoute.

To distinguish between odata routes and webapi routes we use the following scheme :

I tried both these solution without success which all led to get an HTTP 404 result.

My custom action is defined as following:

public class SomeModelsController : ODataController
{
    //...

    [EnableQuery]
    public IHttpActionResult Get()
    {
        //...
        return Ok(data);
    }

    public IHttpActionResult MyCustomAction(int parameterA, int parameterB)
    {
        //...
        return Json(data);
    }

    //...
}

So as you guessed it, the Get call on the controller perfectly work with odata. However the MyCustomAction is a bit more difficult to setup properly.

Here is what I have tried :

  1. Setting an [HttpPost] attribute on MyCustomAction

    [HttpPost]
    public IHttpActionResult MyCustomAction(int parameterA, int parameterB)
    {
        //...
        return Json(data);
    }
    

    I also tried decorating MyCustomAction with the [EnableQuery] attribute.
    Also, I tried adding the [AcceptVerbs("GET", "POST")] attribute on the method without changes.

  2. Configuring the ODataConventionModelBuilder

      private static IEdmModel GetEdmModel()
      {
          var builder = new ODataConventionModelBuilder
          {
              Namespace = "MyApp",
              ContainerName = "DefaultContainer"
          };
          // List of entities exposed and their controller name
          // ...
          FunctionConfiguration function = builder.Function("MyCustomAction ").ReturnsFromEntitySet<MyModel>("SomeModels");
          function.Parameter<int>("parameterA");
          function.Parameter<int>("parameterB");
          function.Returns<MyModel>();
    
          return builder.GetEdmModel();
      }
    

    Also tried decoration of MyCustomAction with [EnableQuery], HttpPost and [AcceptVerbs("GET", "POST")] attributes.

I still get HTTP 404 result.

My query url is as follow:
http://localhost:9292/myProject/odata/SomeModels/MyCustomAction?parameterA=123&parameterB=123

I also tried to POST parameters on http://localhost:9292/myProject/odata/SomeModels/MyCustomAction with the same result. Actually with or without parameters I get HTTP 404 status.


Solution

  • I've created a working example from scratch with Visual Studio 2017. If you want more info you can read this tutorial:

    https://learn.microsoft.com/en-us/aspnet/web-api/overview/odata-support-in-aspnet-web-api/odata-v4/odata-actions-and-functions

    • Create a new ASP.Net Web Application (no .Net Core)

    • Choose WebApi Template

    • Install from NuGet the package Microsoft.AspNet.OData (I have used v. 6.0.0)

    • Create a simple model class into Models folder

    TestModel.cs

    namespace DemoOdataFunction.Models
    {
        public class TestModel
        {
            public int Id { get; set; }
    
            public int MyProperty { get; set; }
    
            public string MyString { get; set; }
        }
    }
    
    • Configure WebApiConfig

    WebApiConfig.cs

    using DemoOdataFunction.Models;
    using System.Web.Http;
    using System.Web.OData.Builder;
    using System.Web.OData.Extensions;
    
    
    namespace DemoOdataFunction
    {
        public static class WebApiConfig
        {
            public static void Register(HttpConfiguration config)
            {
                // Web API configuration and services
    
                // Web API routes
                config.MapHttpAttributeRoutes();
    
                config.Routes.MapHttpRoute(
                    name: "DefaultApi",
                    routeTemplate: "api/{controller}/{id}",
                    defaults: new { id = RouteParameter.Optional }
                );
    
                ODataModelBuilder builder = new ODataConventionModelBuilder();
                builder.Namespace = "MyNamespace";
    
                builder.EntitySet<TestModel>("TestModels");
    
                ActionConfiguration myAction = builder.EntityType<TestModel>().Action("MyAction");
                myAction.Parameter<string>("stringPar");
    
    
                FunctionConfiguration myFunction = builder.EntityType<TestModel>().Collection.Function("MyFunction");
                myFunction.Parameter<int>("parA");
                myFunction.Parameter<int>("parB");
                myFunction.ReturnsFromEntitySet<TestModel>("TestModels");
    
    
                config.MapODataServiceRoute(
                    routeName: "ODataRoute",
                    routePrefix: "odata",
                    model: builder.GetEdmModel()
                    );
            }
        }
    }
    
    • Create the controller TestModelsController into Controllers folder

    TestModelsController.cs

    using DemoOdataFunction.Models;
    using System.Collections.Generic;
    using System.Linq;
    using System.Web.Http;
    using System.Web.OData;
    using System.Web.OData.Query;
    
    namespace DemoOdataFunction.Controllers
    {
        public class TestModelsController : ODataController
        {
            IQueryable<TestModel> testModelList = new List<TestModel>()
                {
                    new TestModel{
                    MyProperty = 1,
                    MyString = "Hello"
                    }
                }.AsQueryable();
    
            [EnableQuery]
            public IQueryable<TestModel> Get()
            {
                return testModelList;
            }
    
            [EnableQuery]
            public SingleResult<TestModel> Get([FromODataUri] int key)
            {
    
                IQueryable<TestModel> result = testModelList.Where(t => t.MyProperty == 1);
                return SingleResult.Create(result);
            }
    
            [HttpPost]
            public IHttpActionResult MyAction([FromODataUri] int key, ODataActionParameters parameters)
            {
                string stringPar = parameters["stringPar"] as string;
    
                return Ok();
            }
    
            [HttpGet]
            [EnableQuery(AllowedQueryOptions = AllowedQueryOptions.All, MaxExpansionDepth = 2)]
            public  IHttpActionResult MyFunction(int parA, int parB)
            {
                return Ok(testModelList);
            }
        }
    }
    
    • Edit Web.config changing the handlers section in system.webServer

    web.config

    <system.webServer>
        <handlers>
          <clear/>
          <add name="ExtensionlessUrlHandler-Integrated-4.0" path="/*" 
              verb="*" type="System.Web.Handlers.TransferRequestHandler" 
              preCondition="integratedMode,runtimeVersionv4.0" />
        </handlers>
        [...]
    </system.webServer>
    

    That's all.

    This is the request for MyAction:

    POST
    http://localhost:xxxx/odata/TestModels(1)/MyNamespace.MyAction
    {
      "stringPar":"hello"
    }
    

    This is the request for MyFunction:

    GET
    http://localhost:xxxx/odata/TestModels/MyNamespace.MyFunction(parA=1,parB=2)