1

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.

John-Philip
  • 3,392
  • 2
  • 23
  • 52

2 Answers2

2

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)
Francesco Bozzi
  • 271
  • 2
  • 6
  • I can't get it to work. I tried both with and without namespace in url – John-Philip Jul 24 '17 at 13:05
  • Do you still get HTTP 404 result? – Francesco Bozzi Jul 24 '17 at 13:07
  • Yes, still HTTP 404 – John-Philip Jul 24 '17 at 13:08
  • Do you tried a Get Request with the function decorated with [HttpGet]? – Francesco Bozzi Jul 24 '17 at 13:15
  • Yes, this is still HTTP 404 – John-Philip Jul 24 '17 at 13:22
  • I had a few days off and only now I could test the code. You have to change the request in [http://localhost:9292/myProject/odata/SomeModels/MyApp.MyCustomAction(parameterA=123,parameterB=123)](http://localhost:9292/myProject/odata/SomeModels/MyApp.MyCustomAction(parameterA=123,parameterB=123)). I edited my answer. – Francesco Bozzi Jul 26 '17 at 15:35
  • I have carefully followed your example but I can't get it to work. Could you please elaborate a bit more your answer with a full working example, and the http url to use ? – John-Philip Jul 28 '17 at 13:53
  • Until today I've test in a my working project, but today I've create a working example from scratch and I've realized that I was missing a web.config's modify. I've edit my post with a step by step procedure. – Francesco Bozzi Aug 01 '17 at 23:04
  • Thank you so much for your time and your quality answer. This helped me to spot some problems in my configuration and code. – John-Philip Aug 03 '17 at 08:22
  • 1
    If this work well for custom OData function, it appears that setting path of `System.Web.Handlers.TransferRequestHandler` in `web.config` file to `/*` instead of `*.` does prevent reaching the WebApiController. [I described my problem here](https://stackoverflow.com/q/45586813/4306452), if you have any idea it will be of much help ! – John-Philip Aug 09 '17 at 09:35
0

I am using HTTP POST with route on the controller functions like below:

        [HttpPost]
        [Route("{application}/{envName}/date/{offset}")]
        [ResponseType(typeof(DateInfo))]
        public async Task<IHttpActionResult> SetDateOffsetForEnvironmentName(string application, string envName, string offset)
        {
        }

can you try setting the route on the function and then call the post method on it like this:

POST /status/environments/ATOOnline/PTH/date/0

Also try and capture a request through Fiddler and see what is being passed.

Arun Kumar
  • 907
  • 13
  • 33