Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

In ASP.NET 5, how do I get the chosen route in middleware?

I am building an ASP.NET 5 (vNext) site that will host dynamic pages, static content, and a REST Web API. I have found examples of how to create middleware using the new ASP.NET way of doing things but I hit a snag.

I am trying write my own authentication middleware. I would like to create a custom attribute to attach to the controller actions (or whole controllers) that specifies that it requires authentication. Then during a request, in my middleware, I would like to cross reference the list of actions that require authentication with the action that applies to this current request. It is my understanding that I configure my middleware before the MVC middleware so that it is called first in the pipeline. I need to do this so the authentication is done before the request is handled by the MVC controller so that I can't prevent the controller from ever being called if necessary. But doesn't this also mean that the MVC router hasn't determined my route yet? It appears to me the determination of the route and the execution of that routes action happen at one step in the pipeline right?

If I want to be able to determine if a request matches a controller's action in a middleware pipeline step that happens before the request is handled by the controller, am I going to have to write my own url parser to figure that out? Is there some way to get at the routing data for the request before it is actually handled by the controller?

Edit: I'm beginning to think that the RouterMiddleware might be the answer I'm looking for. I'm assuming I can figure out how to have my router pick up the same routes that the standard MVC router is using (I use attribute routing) and have my router (really authenticator) mark the request as not handled when it succeeds authentication so that the default mvc router does the actual request handling. I really don't want to fully implement all of what the MVC middleware is doing. Working on trying to figure it out. RouterMiddleware kind of shows me what I need to do I think.

Edit 2: Here is a template for the middleware in ASP.NET 5

public class TokenAuthentication
{
        private readonly RequestDelegate _next;

        public TokenAuthentication(RequestDelegate next)
        {
            _next = next;
        }

        public async Task Invoke(HttpContext context)
        {
             //do stuff here

             //let next thing in the pipeline go  
             await _next(context);

             //do exit code
        }
}
like image 221
Greg Cobb Avatar asked Aug 15 '15 06:08

Greg Cobb


People also ask

How do I specify a route in Web API?

The default route template for Web API is "api/{controller}/{id}". In this template, "api" is a literal path segment, and {controller} and {id} are placeholder variables. When the Web API framework receives an HTTP request, it tries to match the URI against one of the route templates in the routing table.

How would you setup the default route using endpoints?

UseEndpoints(endpoints => { endpoints. MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); }); Inside the call to UseEndpoints() , we use the MapControllerRoute() method to create a route by giving the name default .

What is routing and how can you define routes in ASP.NET Core?

In ASP.NET Core MVC, this process is known as routing. Routing is the process of directing an HTTP request to a controller. Let us now understand how to route requests to different controllers. The ASP.NET Core middleware needs a way to determine if a given HTTP request should go to a controller for processing or not.


1 Answers

I ended up looking through the ASP.NET source code (because it is open source now!) and found that I could copy the UseMvc extension method from this class and swap out the default handler for my own.

public static class TokenAuthenticationExtensions
{
    public static IApplicationBuilder UseTokenAuthentication(this IApplicationBuilder app, Action<IRouteBuilder> configureRoutes)
    {
        var routes = new RouteBuilder
        {
            DefaultHandler = new TokenRouteHandler(),
            ServiceProvider = app.ApplicationServices
        };

        configureRoutes(routes);

        routes.Routes.Insert(0, AttributeRouting.CreateAttributeMegaRoute(
            routes.DefaultHandler,
            app.ApplicationServices));

        return app.UseRouter(routes.Build());
    }
}

Then you create your own version of this class. In my case I don't actually want to invoke the actions. I will let the typical Mvc middleware do that. Since that is the case I gut all the related code and kept just what I needed to get the route data which is in actionDescriptor variable. I probably can remove the code dealing with backing up the route data since I dont think what I will be doing will affect the data, but I have kept it in the example. This is the skeleton of what I will start with based on the mvc route handler.

public class TokenRouteHandler : IRouter
{
    private IActionSelector _actionSelector;

    public VirtualPathData GetVirtualPath(VirtualPathContext context)
    {
        EnsureServices(context.Context);

        context.IsBound = _actionSelector.HasValidAction(context);

        return null;
    }

    public async Task RouteAsync(RouteContext context)
    {
        var services = context.HttpContext.RequestServices;

        EnsureServices(context.HttpContext);

        var actionDescriptor = await _actionSelector.SelectAsync(context);
        if (actionDescriptor == null)
        {
            return;
        }

        var oldRouteData = context.RouteData;
        var newRouteData = new RouteData(oldRouteData);

        if (actionDescriptor.RouteValueDefaults != null)
        {
            foreach (var kvp in actionDescriptor.RouteValueDefaults)
            {
                if (!newRouteData.Values.ContainsKey(kvp.Key))
                {
                    newRouteData.Values.Add(kvp.Key, kvp.Value);
                }
            }
        }

        try
        {
            context.RouteData = newRouteData;

            //Authentication code will go here  <-----------
            var authenticated = true;

            if (!authenticated)
            {
                context.IsHandled = true;
            }
        }
        finally
        {
            if (!context.IsHandled)
            {
                context.RouteData = oldRouteData;
            }
        }
    }

    private void EnsureServices(HttpContext context)
    {
        if (_actionSelector == null)
        {
            _actionSelector = context.RequestServices.GetRequiredService<IActionSelector>();
        }            
    }
}

And finally, in the Startup.cs file's Configure method at the end of the pipeline I have it setup so that I use the same routing setup (I use attribute routing) for the both my token authentication and mvc router.

    public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
    {
        //Other middleware delcartions here <----------------

        Action<IRouteBuilder> routeBuilder = routes =>
        {
            routes.MapRoute(
                name: "default",
                template: "{controller=Home}/{action=Index}/{id?}");
        };

        app.UseTokenAuthentication(routeBuilder);

        //Middleware after this point will be blocked if authentication fails by having the TokenRouteHandler setting context.IsHandled to true

        app.UseMvc(routeBuilder);
    }

Edit 1: I should also note that at the moment I am not concerned about the extra time required to select the route twice which is what I think would happen here since both my middleware and the Mvc middleware will be doing that. If that becomes a performance problem then I will build the mvc and authentication in to one handler. That would be best idea performance-wise, but what I have shown here is the most modular approach I think.

Edit 2: In the end to get the information I needed I had to cast the ActionDescriptor to a ControllerActionDescriptor. I am not sure what other types of actions you can have in ASP.NET but I am pretty sure all my action descriptors should be ControllerActionDescriptors. Maybe the old legacy Web Api stuff needs another type of ActionDescriptor.

like image 90
Greg Cobb Avatar answered Oct 25 '22 18:10

Greg Cobb