Tinyweb Series, Part 1: Getting Started
This is the first part of a short series about Tinyweb, a deliberately small web micro-framework for .NET that we will build step by step. It is a teaching device, not a product. The goal is to understand how web frameworks work by building one whose entire source fits in your head.
The series covers getting started (this part), building APIs, dependency injection and filters, views and model binding, and finally a complete to-do demo.
Design goals
Before writing code, decide what Tinyweb should be. Our rules:
- Small. A handful of classes, each doing one job.
- Convention first. A class ending in
Controllerhandles requests; a public method is an action. - Testable. Actions are plain methods that return a result object. Nothing in them touches
HttpContextdirectly. - Built on ASP.NET. We borrow the hosting and the HTTP pipeline and add only what we need.
If the idea of building such a thing seems odd, why building a web framework is the new black makes the case for it as a learning exercise.
Project setup
Create an empty ASP.NET web application project. Add a reference to System.Web if it is not there already, and a new class library or folder named Tinyweb to hold the framework code. Keeping the framework in a separate folder, or better a separate project, stops you from mixing framework code with application code, which is a habit worth building from day one.
Then tell ASP.NET to hand every request to our front handler. In web.config, register an HTTP handler for all paths (the exact section depends on whether you run the integrated or classic pipeline, so follow the documentation for your hosting mode).
The front handler
Everything starts with IHttpHandler:
public class TinywebHandler : IHttpHandler
{
public bool IsReusable { get { return true; } }
public void ProcessRequest(HttpContext context)
{
var request = new TinyRequest(context);
var result = Tinyweb.Dispatcher.Handle(request);
result.Execute(context);
}
}
We wrap the context in our own TinyRequest so that the rest of the framework depends on a small type that we control and can fake in tests.
The request and the result
public class TinyRequest
{
public string Method { get; private set; }
public string Path { get; private set; }
public NameValueCollection Query { get; private set; }
public NameValueCollection Form { get; private set; }
public TinyRequest(HttpContext context)
{
Method = context.Request.HttpMethod;
Path = context.Request.AppRelativeCurrentExecutionFilePath.TrimStart('~');
Query = context.Request.QueryString;
Form = context.Request.Form;
}
}
public abstract class TinyResult
{
public abstract void Execute(HttpContext context);
}
public class TextResult : TinyResult
{
private readonly string _text;
public TextResult(string text) { _text = text; }
public override void Execute(HttpContext context)
{
context.Response.ContentType = "text/plain";
context.Response.Write(_text);
}
}
The first controller
With the plumbing in place, an application controller is refreshingly ordinary:
public class HomeController
{
public TinyResult Index()
{
return new TextResult("Hello from Tinyweb");
}
public TinyResult Greet(string name)
{
return new TextResult("Hello, " + name);
}
}
Notice that neither action mentions HTTP. You can call new HomeController().Greet("Ann") in a unit test and check the returned object.
The dispatcher
The dispatcher maps the path to a controller and action using the convention: /home/greet becomes HomeController.Greet.
public class Dispatcher
{
public TinyResult Handle(TinyRequest request)
{
var parts = request.Path.Trim('/').Split('/');
var controllerName = (parts.Length > 0 && parts[0] != "" ? parts[0] : "home");
var actionName = parts.Length > 1 ? parts[1] : "index";
var type = FindController(controllerName);
if (type == null) return new NotFoundResult();
var method = type.GetMethods()
.FirstOrDefault(m => string.Equals(m.Name, actionName, StringComparison.OrdinalIgnoreCase));
if (method == null) return new NotFoundResult();
var args = BindArguments(method, request);
return (TinyResult)method.Invoke(Activator.CreateInstance(type), args);
}
}
Here FindController scans the assembly for a type named {name}Controller (cache the result at startup), and BindArguments pulls values for each parameter from the query string. We will replace that naive binder in part 4.
Running it
Start the site and browse to the root. You should see the greeting from Index. Then try the greeting action with a query string such as /home/greet?name=Ann. If you see "Hello, Ann", the whole loop works: request, convention, reflection, result.
What we have and what is missing
- A single entry point, a dispatcher, controllers and results.
- No JSON or XML yet. That is the subject of part 2.
- Controllers are created with
Activator.CreateInstance, so no dependencies. Part 3 fixes that. - No views and a very weak binder. Part 4.
Continue with part 2, building APIs. If you want the broader testing story for code like this, take a look at isolating code with Moles and Fakes for the cases where you cannot avoid static dependencies.
More .NET guides
- Moles and Pex: Isolating .NET Code and Generating Tests (Now Fakes and IntelliTest)
- Building a Web Framework Is the New Black
- Funcy Testing: Using Func and Action Instead of Interfaces
- MEF: Making Extensibility Fun
- A Gentle Introduction to WCF
- Tinyweb Series, Part 2: Building APIs
- Tinyweb Series, Part 3: Dependency Injection and Filters
- Tinyweb Series, Part 4: Views, View Models and Model Binding
- Tinyweb Series, Part 5: The To-Do Demo
- The Ordered Jobs Kata: Dependency Sorting with TDD in C#
- JavaScript: It's Up Your Sleeve
- C# News from the Future: What If...?
- What's After Unit Testing?
- The "If It Isn't Broken, Don't Fix It" Mentality
- Are You Failing Enough?
- Don't Bother Rewriting It: The Case for Gradual Refactoring
- Was Dave a Genius? Lessons from a Legacy Codebase
- Preston Tweetup: Why Developer Meetups Matter
- How Developers Really Learn
- Sustainable Side Projects: Keeping Personal Code Alive
- Parallel.For and Silver Bullets
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).