annuaire tech France

Building a Web Framework Is the New Black

Every few years a fashion sweeps through the developer world. Once it was writing your own blog engine. Then it was writing your own ORM. Right now, apparently, the new black is writing your own web framework. I have done it, and I recommend it, with one big caveat that I will get to at the end.

Why bother?

Frameworks hide the plumbing, which is wonderful until something goes wrong and you realise you do not know how a request becomes a method call. Building a small one forces you to answer questions you would otherwise never ask. How does a URL find a piece of code? Who creates the objects? What happens between the browser and your method?

You do not need to compete with anything. The goal is understanding, and a few hundred lines of C# are enough.

Step one: take the request

ASP.NET already gives you the hook you need: an IHttpHandler. It has one method and one property, and it is the lowest-level way to say "send me the request".

public class FrontHandler : IHttpHandler
{
    public bool IsReusable { get { return true; } }

    public void ProcessRequest(HttpContext context)
    {
        var path = context.Request.AppRelativeCurrentExecutionFilePath;
        context.Response.ContentType = "text/plain";
        context.Response.Write("You asked for " + path);
    }
}

Register it in web.config for every path, or for a prefix, and you have a front controller. Everything else in this article is what you build on top of this one method.

Step two: routing

A router maps a URL pattern to something that can handle it. The simplest version is a list of patterns with named segments:

public class Route
{
    public string Pattern;   // e.g. "/products/{id}"
    public Func<RouteData, string> Handler;
}

public class Router
{
    private readonly List<Route> _routes = new List<Route>();

    public void Add(string pattern, Func<RouteData, string> handler)
    {
        _routes.Add(new Route { Pattern = pattern, Handler = handler });
    }

    public string Dispatch(string path)
    {
        foreach (var route in _routes)
        {
            var data = RouteData.TryMatch(route.Pattern, path);
            if (data != null) return route.Handler(data);
        }
        return null; // 404
    }
}

Matching means splitting both strings on /, comparing literal segments and capturing the ones in braces. It is tedious rather than difficult, and writing it once makes the routing tables in bigger frameworks feel much less magical.

Step three: convention over configuration

Hand-registering every route gets old quickly. Conventions fix that. Decide that a class named ProductsController handles /products, and that a public method named Show handles /products/show. Reflection does the rest:

var controllers = Assembly.GetExecutingAssembly()
    .GetTypes()
    .Where(t => t.Name.EndsWith("Controller"));

foreach (var type in controllers)
{
    var prefix = type.Name.Replace("Controller", "").ToLowerInvariant();
    foreach (var method in type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly))
    {
        router.Add("/" + prefix + "/" + method.Name.ToLowerInvariant(),
                   data => (string)method.Invoke(Activator.CreateInstance(type), null));
    }
}

Now adding a feature means adding a method. No registration, no XML, no ceremony. That is the entire pitch of convention-based frameworks, and you have just built it.

Step four: results instead of strings

Returning a raw string works for a demo, but real handlers need to return HTML, JSON, redirects and error codes. Introduce a small result type:

public abstract class ActionResult
{
    public abstract void Execute(HttpContext context);
}

public class ContentResult : ActionResult
{
    public string Body;
    public override void Execute(HttpContext context)
    {
        context.Response.Write(Body);
    }
}

public class RedirectResult : ActionResult
{
    public string Url;
    public override void Execute(HttpContext context)
    {
        context.Response.Redirect(Url);
    }
}

Handlers return a result, and the framework executes it. That separation makes handlers easy to unit test, because you assert on the returned object instead of on an HTTP response.

What you learn along the way

  • Routing is just string matching plus a priority order. Order matters, and the first match wins.
  • Conventions are reflection with opinions. The convention is the framework's real API.
  • Object creation is a design decision. The moment a controller needs a dependency, you will want a container, which is where dependency injection and filters come in.
  • Testability is a feature you have to design, not a side effect. The same lesson shows up when you try isolating code with Moles and Fakes: the code you cannot swap out is the code you cannot test.

The caveat

Please do not run your company's customer-facing site on the framework you wrote last weekend. Established frameworks have years of work behind them on security, encoding, request validation, caching and edge cases you have not met yet. A homegrown framework is a superb teacher and a poor foundation.

What you should do is keep the knowledge. The next time a stack trace passes through a router or a model binder, you will recognise the shape of it. That alone repays the weekend.

A weekend plan

  1. Write the IHttpHandler and make it print the requested path.
  2. Add a router with two routes and one captured segment.
  3. Add a controller convention using reflection, scanned once at startup.
  4. Introduce result types and a view of your own.
  5. Write tests for the router first next time. It is the easiest part to get wrong.

It is fashionable, it is educational, and it is a lot of fun. Just remember that the point of wearing black is how it makes you look, not how long you keep wearing it.

More .NET guides

Looking for a software partner? Browse the independent directory of French tech companies (web agencies, IT consultancies, software publishers; the directory itself is in French).