Friday, November 28, 2014

ASP.Net: Web API 2 + Help Pages + OData

Doing each of them separately is fairly easy and there's lots of tutorials on how to do it. But as for mixing them together, I've not seen much. Some blogs claim it's not possible and buggy and mostly lead to 404 and 406 errors. Well, out of luck, I made it work. The secret is in the ROUTING!

I don't understand all the concepts behind this and I believe most of us don't, and don't care to. Still, feel free to explain the unexplained in the comments.

So, I started with a ASP.Net Application project. I Selected Web API for the template so I could have the help pages ready. Then I added the needed stuff for OData V4 following this really nice tutorial. Last, I had to configure the ROUTES, and that is where all hell resides (well, that's what all the blog posts and obscure SO questions lead me to think). THe following has been taken from my SO answer.

The order in which the routes are configured has an impact. In my case, I also have some standard MVC controllers and help pages.

Global.asax:
protected void Application_Start()
{
 AreaRegistration.RegisterAllAreas();
 GlobalConfiguration.Configure(config =>
 {
  ODataConfig.Register(config); //this has to be before WebApi
  WebApiConfig.Register(config);
 });
 FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
 RouteConfig.RegisterRoutes(RouteTable.Routes);
}

The filter and routeTable parts weren't there when I started my project and are needed.

ODataConfig.cs:
public static void Register(HttpConfiguration config)
{
  config.MapHttpAttributeRoutes(); //This has to be called before the following OData mapping, so also before WebApi mapping
    
  ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
    
  builder.EntitySet("Sites");
  //Moar!
    
  config.MapODataServiceRoute("ODataRoute", "api", builder.GetEdmModel());
}

WebApiConfig.cs:
public static void Register(HttpConfiguration config)
{
  config.Routes.MapHttpRoute( //MapHTTPRoute for controllers inheriting ApiController
     name: "DefaultApi",
     routeTemplate: "api/{controller}/{id}",
     defaults: new { id = RouteParameter.Optional }
  );
}

RouteConfig.cs:
public static void RegisterRoutes(RouteCollection routes)
{
  routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
    
  routes.MapRoute( //MapRoute for controllers inheriting from standard Controller
    name: "Default",
    url: "{controller}/{action}/{id}",
    defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
  );
}

This has to be in that EXACT ORDER. I tried moving the calls around and ended up with either MVC, Api or Odata broken with 404 or 406 errors.

So I can call:

localhost:xxx/ -> leads to help pages (home controller, index page)

localhost:xxx/api/ -> leads to the OData $metadata

localhost:xxx/api/Sites -> leads to the Get method of my SitesController inheriting from ODataController

localhost:xxx/api/Test -> leads to the Get method of my TestController inheriting from ApiController.

7 comments:

Unknown said...

Thank you for this, so very much

Jerther said...

No problem! My pleasure :)

kiran said...

Hi Jerther, I am also facing the same issue, and it dint fix it, is there any way that you can share your sample application. Thank you.

Julio Cesar Avellaneda said...

Hi, I fixed this error changing the using System.Web.Http.OData by System.Web.OData

SoluciĆ³n al error 406 Not Acceptable en OData

Unknown said...

I recently used this to configure my Application that is using both oData and WebAPI (MVC) Controllers. After studying your solution/fix I thought that I would comment (probably obvious but none the less here goes).

Your solution worked as is for me, but one difference in mine that didn't work, but we were early on so we switched directions. In that if we have a MVC Controller and oData controller that maps to the same route, i.e. /api/testEntity for both oData and MVC the oData route would win first. Additionally, since oData was put in the solution packages after MVC it seems to me that you are essentially pushing a second route config, and while I did not try it, if you do have two controller types server the same entity then your only option would be to send webAPI down one path and oData down the other. Assuming you couldn't rename one to be testEnity and testEntiyMVC etc. None the less your solution has set the framework to do precisely that.

Very nice job. And thank you.

Jerther said...

@Andrew: You're welcome, and thanks for the nice addition!

HTalstra said...

which usings do you use in global.asax?
I cannot get the:
AreaRegistration.RegisterAllAreas();
to work