An Alternative Style of Routing for ASP.Net Core

Jasper started as an idea to build a far better, next generation version of an older framework called FubuMVC. FubuMVC was primarily a web framework meant to be an alternative to ASP.Net MVC that later got a service bus feature bolted on the side. My previous employer mostly wanted and needed the asynchronous messaging support, so that’s what most of the focus has been within Jasper so far. However, it does have some preliminary support for building HTTP APIs as well and that’s the subject of this post.

From the Carter (“Carter” is to NancyFx as Jasper is to FubuMVC) website:

Carter is a library that allows Nancy-esque routing for use with ASP.Net Core.

Cool, so in the ASP.Net Core space you at least have the main MVC Core framework and its style of implementing HTTP routes and the inevitable Sinatra inspired alternative. In the same vein, Jasper provides yet another alternative to implement routing on ASP.Net Core with a much improved version of what we did years ago with FubuMVC.

A Quick Example of Jasper Routing

Before I get into too many details, here’s a concrete example of a Jasper route handler from its testing library that allows you to post a JSON message to an endpoint and get a response back as JSON:

    public class NumbersEndpoint
    {
        // This action would respond to the url "POST: /sum"
        public static SumValue post_sum(SomeNumbers input)
        {
            return new SumValue{Sum = input.X + input.Y};
        }
    }

So right off the bat, you can probably see that Jasper still uses (by default) convention over configuration to derive the Url pattern attached to this method. I think most folks would react to that code and approach in one of a couple ways:

  1. OMG, that’s too much magic, and magic in code is evil! All code must be “simple” and explicit, no matter how much repetitive cruft and code ceremony I have to type and then later wade through to understand the code. You probably won’t care for Jasper, and probably don’t mind the repetitive base class declarations, attribute usage, and IActionResult code noise that bugs me when I use or have to read MVC Core code. There’s room in the world for both of us;-)
  2. That looks simple, clean, and I bet it’s easy to test. Tell me more!

So if you fell into the second group, or are at least open minded enough to learn a little more, let’s move on to some of the mechanics of defining routes and implementing handlers.

 

Discovering Routes within an Application

While Sinatra-inspired web frameworks like like Express.js tend to want you to explicitly register routes and their handlers, most .Net web frameworks I’m familiar with use some kind of type scanning to find candidate routes from the public types and methods exposed in your application’s main assembly. Jasper is no different in that it looks inside your application’s assembly (either the assembly containing your JasperRegistry or the main console application that bootstrapped Jasper) for concrete, public classes that are named with the suffix “Endpoint” or “Endpoints.” Within those types, Jasper looks for any public method on those types whose name begins with a supported HTTP method name (GET, POST, PUT, DELETE, or HEAD for the moment).

Do note that the endpoint methods can be either instance or static methods, as long as they are public and match the naming criteria.

Here’s an example:

    public class SomeEndpoints
    {
        // Responds to GET: /something
        public string get_something()
        {
            return "Something";
        }

        // Responds to POST: /something
        public string post_something()
        {
            return "You posted something";
        }
    }

Jasper does the type discovery using Lamar’s type scanning, which in turn is pretty well taken straight up from StructureMap 4.*’s type scanning, which in its turn was specifically designed to improve the cold start time in the last version of FubuMVC and provides the same benefit for Jasper (it does some pre-sorting of types and some parallelization that helps quite a bit when you have multiple type scanning conventions happening in the same application). This isn’t my first rodeo.

This discovery is somewhat customizable, but this time around I’m asking users to just use the minimal default conventions instead of making Jasper crazy configurable like FubuMVC was to its and its user’s detriment.

Defining Route Patterns

I didn’t realize this is missing until writing this post, but FubuMVC also supported attributes for explicitly defining or overriding route patterns. This just hasn’t quite made it to Jasper yet, but would have to for the inevitable exception cases.

First off, Jasper has a special naming convention for the root (“/”) url of your application. If an endpoint class is called HomeEndpoint or ServiceEndpoint (it’s up to your preference, but I’d advise you to only use one or the other), the route methods are just derived by the matching HTTP method names, like this:

    public class HomeEndpoint
    {
        // Responds to GET: /
        public string Index()
        {
            return "Hello, world";
        }

        // Responds to GET: /
        public string Get()
        {
            return "Hello, world";
        }

        // Responds to PUT: /
        public void Put()
        {
        }

        // Responds to DELETE: /
        public void Delete()
        {
        }
    }

The Index() method is a synonym for “GET” that was a convention in FubuMVC that I kept in Jasper.

For other endpoint action methods, the route is completely derived from the method name, where the method name would follow a pattern like this:

[http method name]_[segment1]_[segment2]_[segment3]

Roughly speaking, the underscore characters denote a “/” forward slash that separates segments in your route. The first segment is the HTTP method — and each action can only legally respond to a single HTTP method.

So a method with the signature post_api_v1_invoice() would respond to the route “POST: /api/v1/invoice.”

Cool, but now you’re probably asking, how do you pass in route arguments? That’s also a naming convention. Consider the method signature get_api_v1_invoice_id(Guid id). Jasper is looking for segments that match an argument to the method and it knows that those segments are really route parameters. By that logic, that method above responds to the route pattern “GET /api/v1/invoice/{id}.” At runtime when this route is matched, the last segment would be parsed to a Guid and the value passed to the method argument.

As of now, route arguments can be most of the primitive types you’d expect:

  • string
  • Guid
  • int/long/double
  • bool
  • enumerations
  • DateTime/DateTimeOffset

Jasper’s routing forces some limitations on you compared to ASP.Net Core’s Routing module. There’s no kind of route constraint other than full string segments and HTTP methods. I’m making a conscious tradeoff here in favor of performance versus the greater flexibility of the ASP.Net Core routing, with the additional upside that Jasper’s routing selection logic is far, far simpler than what the ASP.Net team did for MVC.

So what do I think are the advantages of this approach over some of the other alternatives in .Net land?

  • It’s really easy to navigate to the route handler from a Url. It’s not terribly difficult to go from a Url in some kind of exception report to using a keyboard shortcut in VS.Net or Rider that lets you navigate quickly to the actual method that handles that url.
  • Even though there’s some repetitiveness in defining all the segments of a route, I like that you can completely derive the route for a method by just the method name. From experience, it was a bit of cognitive load having to remember how to combine a controller type name with attributes and the method name to come up with the route pattern.
  • While using an attribute to define the route is mechanically easy, that’s a little extra code noise in my opinion, and you lose some of the code navigability via the method name when you do that.
  • The code is clean. By this I mean there doesn’t have to be any extraneous code noise from attributes or special return types or base classes getting in your way. Some people hate the magic, but I appreciate the terseness

Moreover, this approach is proven in large FubuMVC applications over a matter of years. I’m happily standing by this approach, even knowing that it’s not going to be for everyone.

Asynchronous Actions

All of the method signatures shown above are very simple and synchronous, but it’s a complicated world and many if not most HTTP endpoints will involve some kind of asynchronous behavior, so let’s look at more advanced usages.

For asynchronous behavior, just return Task or Task like this:

        public Task get_greetings(HttpResponse response)
        {
            response.ContentType = "text/plain";
            return response.WriteAsync("Greetings and salutations!");
        }

Injecting Services as Arguments

You’ll frequently need to get direct access to the HttpContext inside your action methods, and to do that, just take that in as a method argument like this:

        public Task post_values(HttpContext context)
        {
            // do stuff
        }

You can also take in arguments for any property of an HttpContext like HttpRequest or HttpResponse just to slim down your own code like this shown below:

        public Task post_values(HttpRequest request, HttpResponse response)
        {
            // do stuff
        }

Like MVC Core, Jasper supports “method injection” of registered service dependencies to the HTTP methods, but Jasper doesn’t require any kind of explicit attributes. Here’s an elided example from the load testing harness projects in Jasper:

        [MartenTransaction]
        public static async Task post_one(IMessageContext context, IDocumentSession session)
        {
            // Do stuff with the context and session
        }

In the case above, the IMessageContext and IDocumentSession are known service registrations, so they will be passed into the method by Jasper by resolving with Lamar either through generating “poor man’s DI” code if it can, or service location if it has to. See Jasper’s special sauce for a little more description of what Jasper is doing differently here.

Reading and Writing Content

To write content, Jasper keys off the return type of your endpoint action. If your method returns:

  • int or Task — the return value is written to the response status code
  • string or Task — the return value is written to the response with the content type “text/plain”
  • Any other T or Task — the return value is written to the response using Jasper’s support for content negotiation. Out of the box though, the only known content for a given type is JSON serialization using Newtonsoft.Json, so if you do nothing to customize or override that, it’s JSON in and JSON else. I feel like that’s a good default and useful in many cases, so it stays.

To post information Jasper let’s you work directly with strong typed objects while it handles the work of deserializing an HTTP body into your declared input body. Putting that altogether, this method used earlier:

    public class NumbersEndpoint
    {
        public static SumValue post_sum(SomeNumbers input)
        {
            return new SumValue{Sum = input.X + input.Y};
        }
    }

At runtime, Jasper will try to deserialize the posted body of the HTTP request into a SomeNumbers model object that will be passed into the post_sum method above. Likewise, the SumValue object coming out of the method will be serialized and written to the HTTP response as the content type “application/json.”

How Does Jasper Select and Execute the Route at Runtime?

I’m not going to get into too many details, but Jasper uses its own routing mechanism based on the Trie algorithm. I was too intimidated to try something like this in the FubuMVC days and we just used the old ASP.Net routing that’s effectively a table scan search. The new Trie algorithm searching is far more efficient for the way that Jasper uses routing.

Once Jasper’s routing matches a route to the Url of the incoming HTTP request, it can immediately call the corresponding RouteHandler method with this signature:

    public abstract class RouteHandler
    {
        public abstract Task Handle(HttpContext httpContext);

        // other methods
    }

As I explained in my previous post on the Roslyn-powered code weaving, Jasper generates a class at runtime that mediates between the incoming HttpContext and your HTTP endpoint, along with any necessary middleware, content negotiation, route arguments, or service resolution.

Potential Advantages of the Jasper Routing Style

I’m biased here, but I think that the Jasper style of routing and the runtime pipeline has some potentially significant advantages over MVC Core:

  • Cleaner code, and for me, “clean” means the absence of extraneous attributes, marker interfaces, base types, and other custom framework types
  • Less mechanical overhead in the runtime pipeline
  • Better performance
  • Less memory usage through fewer object allocations
  • Cleaner stack traces
  • The generated code will hopefully make Jasper’s behavior much more self-revealing when users start heavily using middleware

Most of this is probably a rehash of my previous post, Roslyn Powered Code Weaving Middleware.

 

 

What’s Already in the Box

Keep in mind this work is in flight, and I honestly haven’t worked on it much in quite awhile. So far Jasper’s HTTP support includes:

  • The routing engine (kind of necessary to make anything else go;))
  • Reverse Url lookup
  • Action discovery
  • Middleware support, both Jasper’s idiomatic version or ASP.Net Core middleware (Jasper’s routing is itself ASP.Net Core middleware)
  • The ability to apply middleware conventionally for cases like “put a transactional middleware on any HTTP action that is a POST, PUT, or DELETE”
  • Some basic content conventions like “an endpoint action that returns a string will be rendered as text/plain at runtime.
  • Basic content negotiation (which shares a lot of code with the messaging side of things)

What’s Missing or Where does This Go Next?

The first question is “whether this goes on?” I haven’t poured a ton of effort into the HTTP handling in Jasper. If there’s not much interest in this or my ambition level just isn’t there, it wouldn’t hurt that much to throw it all away and say Jasper is just an asynchronous messaging framework that can be used with or without ASP.Net Core.

Assuming the HTTP side of Jasper goes on, Jasper gets retagged as a “framework for accelerating microservice development in .Net” and then I think this stuff is next up:

  • Documentation. A conventional framework has a weird tendency of being useless if nobody knows what the conventions are.
  • A modicum of middleware extensions that integrate common tools into Jasper HTTP actions. Offhand, I’ve thought about integrations for IdentityServer and Fluent Validation that add some idiomatic Jasper middleware to be just a little bit more efficient than you’d get from ASP.Net Core middleware.
  • Optimization of the routing. There’s some open issues and known performance “fat” in the routing I’ve just never gotten around to doing
  • Some kind of idiomatic approach in Jasper for branching route handling, i.e. return this status code if the entity exists or redirect to this resource if some condition or return a 403 if the user doesn’t have permission. All of that is perfectly possible today with middleware, but it needs to be something easier for one-off cases.
  • Some level of integration of MVC Core elements within Jasper. I’m thinking you’d definitely want the ability to use IActionResult return types within Jasper in some cases. Even though idiomatic Jasper middleware would be more efficient at runtime, you’d probably want to reuse existing ActionFilters too. It might be valuable to even allow you to use MVC Core Controller classes within Jasper, but still use Jasper’s routing, middleware, and the more efficient IoC integration to achieve what I think will be better performance than by using MVC Core by itself.
  • OpenAPI (Swagger) support. I don’t think this would be too big, and I think I have some ideas about how to allow for general customization of the Swagger documents without forcing users to spray attributes all over the endpoint classes until you can barely see the real code.

What about Razor?

I have zero interest in ever supporting any kind of custom integration of Razor (the view engine support sucked to work on in FubuMVC), and I have it in mind that Razor is pretty tightly coupled to MVC Core’s internals anyway. My thought is that either the IActionResult support gives Jasper Razor support for free, or we just say that you use MVC Core for those pages — and it’s perfectly possible to use both Jasper and MVC Core in the same application. My focus with Jasper is on headless services anyway.

6 thoughts on “An Alternative Style of Routing for ASP.Net Core

  1. My opinion about the convention: looks good but I do agree that it’s a bit “magic”.
    Problem with this is that obfuscation (unfortunately for my team and many that I know, obfuscation is still a must, business requirement) will very easily destroy any magic available. I like how NancyFX register routes in the constructor.
    I think something like having a AutoRoutes method call in the class’ constructor to apply the convention will help make it easier for most devs to use the convention, while others with more need can apply their own convention, similar to StructureMap’s scanning calls. Better yet, Roslyn code generation at compile time will be great, so that the convention will survive obfuscation.

    1. A couple points:

      * MVC and Nancy also have some level of conventions, so it’s not really new

      * The traceability of the Jasper/FubuMVC style is far better IMO than either MVC or Nancy

      * The ability to see the generated code in Jasper will do a lot to explain the “magic” and that’s something you have neither of the other tools

      * I didn’t get into it, but Jasper also has a strong reverse url lookup capability that’s very handy for ReST endpoints you won’t get from Nancy’s Sinatra style routing

Leave a comment