8

I have a legacy project that has a single IHttpHandler implementing class that routes all the requests using a huge switch statement etc.. I am trying to introduce Attribute Routing with ApiControllers but the first one always has the priority. Is it possible to configure the system (either code or IIS) so that Web ApiControllers have priority over my single IHttpHandler implementing class? In IIS, I put my AttributeRouting first and then there are all the aspx ones but still the Web Api Controller is not getting processed first..no matter what I do (having them under the same project). I don't want to introduce a separate project.

Edit: There is a IHttpModule that decides based on what is after api/ to route it to specific ashx file. One of them is the one described..

Edit 2: More specifically: If the uri doesn't have a list of filtered things [file,message,property ...] it is routed to Resource.aspx

so api/file, api/message, api/property would be handle from other .ashx files - otherwise the traffic goes to Resource.ashx...

As a result the requests that have api/endpoint1, api/endpoint2, api/endpoint3 will all go to Resource.aspx. The question is how to route api/endpoint3 to the API Controller described below. Thanks

Simplified Code Architecture:

 //SolutionName/Api/MyModule.cs (Legacy Code)
 //this routes based on what is after api/ to Resource.ashx or other ashx files
 public class MyModule : IHttpModule {
    //if url doesn't contain [file,message,property ...] route to Resource.ashx
 }

//SolutionName/API/Resource.ashx (Legacy Code)
//this is hit at any request solutionname/api/anything
public class DefaultHandler : IHttpHandler 
{
   public void ProcessRequest(HttpContext context) {
       String APIBranch = parse(context);
       switch(APIBranch)
       {
           case "endpoint1": methodOne(); break;
           case "endpoint2": methodTwo(); break;
           [...]
           default: throw Exception(); break;
       }
   }
}

//SolutionName/API/App_Start/AttributeRoutingHttpConfig.cs
public static class AttributeRoutingHttpConfig
{
    public static void RegisterRoutes(HttpRouteCollection routes) 
    {    
        // See http://github.com/mccalltd/AttributeRouting/wiki for more options.
        // To debug routes locally using the built in ASP.NET development server, go to /routes.axd

        routes.MapHttpAttributeRoutes();
    }

    public static void Start() 
    {
        RegisterRoutes(GlobalConfiguration.Configuration.Routes);
    }
}

//SolutionName/API/Controllers/MyController.cs
//this should have been hit for a GET on solutionname/api/endpoint3/id
[RoutePrefix("endpoint3")]
public class MyController : ApiController
{
    private IModelDao modelDao;

    MyController(IModelDao modelDao){
        this.modelDao = modelDao;
    }   

    [Route("{id}")]
    [HttpGet]
    public Model GetSomething(int id)
    {
        Model model = modelDao.GetSomething(id);
        return model;
    }
}
Michail Michailidis
  • 11,792
  • 6
  • 63
  • 106
  • 1
    `HttpHandlers` work before the api controller is hit. Handlers are used to inspect and potentially intercept or redirect ('act on') a given resource request. It doesn't work the other way around. You'll have to take the `default` out of the handler's switch statement to get this to work. You can, however, do the same thing with web api-- you can explicitly specify all the available routes in the configuration and have a default route that takes them to a route that returns an error. – ps2goat Jun 01 '15 at 22:50
  • 1
    Now that I've thought about it a bit, you may be able to inspect the configured api routes and let them through. Now I want to try it... – ps2goat Jun 01 '15 at 22:52
  • @ps2goat Could I have an HttpModule that would hit my ApiController instead - Although I don't know how that would be..I hope this won't have a performance hit.. – Michail Michailidis Jun 01 '15 at 22:55
  • At that point, the module would be acting like IIS and the built-in url routing in the web api. Is this a single problem or does the entire site have this issue? – ps2goat Jun 01 '15 at 23:04
  • The complete story is that there is a module that routes to different ashx given the url. So this is part of the site. I want to override all this and when lets say I have a call to api/endpoint3/ to reroute it to the apicontroller or something or it could have something like api/rest/endpoint3 if that helps to differentiate it... does this answer your question? – Michail Michailidis Jun 01 '15 at 23:11
  • @ps2goat as a note I don't want to manually put intercepting code in the module for each of the web api controller paths... – Michail Michailidis Jun 01 '15 at 23:56
  • If you don't want to manually handle each url in your handler, don't have this in there: `default: throw Exception(); break;` Removing this will allow the request to flow to your api controller. Your api project can return errors if a route does not exist or is not allowed. I don't think it's the handler's job to block traffic. – ps2goat Jun 03 '15 at 14:46
  • well it doesn't it just returns without hitting the api-controllers.. – Michail Michailidis Jun 03 '15 at 15:13

1 Answers1

2

I've found two solutions to this problem. The first is to modify module that rewrites urls by inserting check if Web API routing system can handle request. The second is to add another module to application, that will direct requests to Web API Handler using HttpContext.RemapHandler().

Here's code:

First solution.

If your module looks like this:

public class MyModule: IHttpModule
{
    public void Dispose(){}

    public void Init(HttpApplication context)
    {
        context.BeginRequest += (object Sender, EventArgs e) =>
        {
            HttpContext httpContext = HttpContext.Current;
            string currentUrl = httpContext.Request.Url.LocalPath.ToLower();
            if (currentUrl.StartsWith("/api/endpoint0") ||
                currentUrl.StartsWith("/api/endpoint1") || 
                currentUrl.StartsWith("/api/endpoint2"))
            {
                httpContext.RewritePath("/api/resource.ashx");
            }
        };
    }
}

Then you need to change it like this:

public void Init(HttpApplication context)
{
    context.BeginRequest += (object Sender, EventArgs e) =>
    {
        HttpContext httpContext = HttpContext.Current;
        var httpRequestMessage = new HttpRequestMessage(
            new HttpMethod(httpContext.Request.HttpMethod),
            httpContext.Request.Url);
        IHttpRouteData httpRouteData = 
            GlobalConfiguration.Configuration.Routes.GetRouteData(httpRequestMessage);
        if (httpRouteData != null) //enough if WebApiConfig.Register is empty
            return;

        string currentUrl = httpContext.Request.Url.LocalPath.ToLower();
        if (currentUrl.StartsWith("/api/endpoint0") ||
            currentUrl.StartsWith("/api/endpoint1") ||
            currentUrl.StartsWith("/api/endpoint2"))
        {
            httpContext.RewritePath("/api/resource.ashx");
        }
    };
}

Second solution.

Module for remapping handlers:

public class RemappingModule: IHttpModule
{
    public void Dispose() { }

    public void Init(HttpApplication context)
    {
        context.PostResolveRequestCache += (src, args) =>
        {
            HttpContext httpContext = HttpContext.Current;
            string currentUrl = httpContext.Request.FilePath;
            if (!string.IsNullOrEmpty(httpContext.Request.QueryString.ToString()))
                currentUrl += "?" + httpContext.Request.QueryString;
            //checking if url was rewritten
            if (httpContext.Request.RawUrl != currentUrl) 
            {
                //getting original url
                string url = string.Format("{0}://{1}{2}",
                    httpContext.Request.Url.Scheme,
                    httpContext.Request.Url.Authority,
                    httpContext.Request.RawUrl);
                var httpRequestMessage = new HttpRequestMessage(
                    new HttpMethod(httpContext.Request.HttpMethod), url);
                //checking if Web API routing system can find route for specified url
                IHttpRouteData httpRouteData = 
                    GlobalConfiguration.Configuration.Routes.GetRouteData(httpRequestMessage);
                if (httpRouteData != null)
                {
                    //to be honest, I found out by experiments, that 
                    //context route data should be filled that way
                    var routeData = httpContext.Request.RequestContext.RouteData;
                    foreach (var value in httpRouteData.Values)
                        routeData.Values.Add(value.Key, value.Value);
                    //rewriting back url
                    httpContext.RewritePath(httpContext.Request.RawUrl);
                    //remapping to Web API handler
                    httpContext.RemapHandler(
                        new HttpControllerHandler(httpContext.Request.RequestContext.RouteData));
                }
            }
        };
    }
}

These solutions work when method WebApiConfig.Register is empty, but if there were routes with templates like "api/{controller}" then any path with two segments starting with "api" would pass the check, even if there're no controllers with specified name and your module can do something userfull for this path. In this case you can, for example, use method from this answer to check if controller exists.

Also Web API routing system will accept route even if found controller don't handle requests for current http method. You can use descendant of RouteFactoryAttribute and HttpMethodConstraint to avoid this.

UPD Tested on this controllers:

[RoutePrefix("api/endpoint1")]
public class DefaultController : ApiController
{
    [Route("{value:int}")]
    public string Get(int value)
    {
        return "TestController.Get: value=" + value;
    }
}

[RoutePrefix("api/endpoint2")]
public class Endpoint2Controller : ApiController
{
    [Route("segment/segment")]
    public string Post()
    {
        return "Endpoint2:Post";
    }
}
Community
  • 1
  • 1
Valyok26
  • 501
  • 3
  • 12
  • Thanks for your effort! I think you missed the point a bit - but I will read the answer thoroughly later.. I have 3 modules one of them redirects to /api/ and it handles all the situation that it supports through Resourse.aspx (HttpHandler) ... But I have 3 web api controllers that I want them to handle the traffic before it hits the Resource.aspx.. In your code all those endpoints1,2,3 you direct the to the Resource.aspx which already happens and I don't want it to.. – Michail Michailidis Jun 06 '15 at 19:37
  • @MichailMichailidis I still don't understand. You can either prevent redirect to /api/ in your module or roll back this redirect before HttpHandler in Resource.aspx is executed. Isn't it what you need? – Valyok26 Jun 06 '15 at 19:58
  • I editted the the question to give more clarity - and I added a comment in the httpmodule.. let me know if it is still not clear - thanks - the problem is that any redirect to /api/... will hit again the modules and the process starts again.. – Michail Michailidis Jun 06 '15 at 20:06
  • @MichailMichailidis Sorry my stubborness, I still don't get it. If on PostResolveRequestCache event HttpContext.RewritePath (just rewrites some context variables) and HttpContext.RemapHandler (with Web API Handler as parameter) are called, then request is just processed by Web API framework. It won't go to BeginRequest event in modules again. – Valyok26 Jun 06 '15 at 20:33
  • I see what you are trying to do in the second solution. It wasn't clear when I read the first solution. So you were able to test it and make it work when you have conflicting /api/endpoint0,1,2 and api/endpoint3 being handled by httphandler and web api controller respectively? – Michail Michailidis Jun 06 '15 at 20:46
  • @MichailMichailidis Yes, api/endpoint1 and api/endpoint2 are routed to controllers, I edited the answer. – Valyok26 Jun 06 '15 at 20:51
  • and the rest are going to the general HttpHandler right? – Michail Michailidis Jun 06 '15 at 20:59
  • @MichailMichailidis Yes. Web API Handler get only such requests, that Web API routing system knows how to process (from attributes). All other requests stays as they are, processed by handlers in aspx file for example. – Valyok26 Jun 06 '15 at 21:02
  • I will accept it when I will have time to test this approach or if any of the users that upvoted the question upvote the answer :) Thanks! – Michail Michailidis Jun 06 '15 at 21:27
  • @MichailMichailidis Nice ) Still, I don't get how requests get to resource.aspx without RewritePath in modules. And if it's actually modules, then you can make RewritePath conditional just like in second solution. First solution looks easier and better for me, though requires editing of original modules. – Valyok26 Jun 06 '15 at 21:47
  • In the http modules of course RewritePath is being used so that requests go to different ashx files – Michail Michailidis Jun 06 '15 at 22:17
  • @MichailMichailidis The idea of first solution is just to return from module before RewritePath is called if Web API can handle request. – Valyok26 Jun 06 '15 at 22:30
  • ok I see I will try the first solution and will tell you when possible! Thanks :) – Michail Michailidis Jun 06 '15 at 22:38