Tinyweb Series, Part 3: Dependency Injection and Filters
By now our little framework can route requests and return JSON or XML. This part, number three in the Tinyweb series, tackles two features that separate a toy from something pleasant to build applications on: dependency injection and filters. If you have just arrived, start with part 1.
The dependency problem
The dispatcher currently creates controllers with Activator.CreateInstance, which only works for classes with a parameterless constructor. A real controller needs a repository, a clock, a mailer. We want to write:
public class TodoController
{
private readonly ITodoStore _store;
public TodoController(ITodoStore store)
{
_store = store;
}
}
and have the framework supply the store.
A controller factory
The first step is to stop creating controllers directly. Introduce a seam:
public interface IControllerFactory
{
object Create(Type controllerType);
}
The dispatcher asks the factory for instances, and applications decide how to implement it. That one interface is the entire hook into any dependency injection container.
A tiny container
You do not need a library to see how this works. A minimal container maps a requested type to a way of creating it, and resolves constructor parameters recursively:
public class TinyContainer : IControllerFactory
{
private readonly Dictionary<Type, Func<object>> _registrations =
new Dictionary<Type, Func<object>>();
public void Register<TService>(Func<TService> factory)
{
_registrations[typeof(TService)] = () => factory();
}
public object Create(Type type)
{
Func<object> factory;
if (_registrations.TryGetValue(type, out factory))
return factory();
var ctor = type.GetConstructors()
.OrderByDescending(c => c.GetParameters().Length)
.First();
var args = ctor.GetParameters()
.Select(p => Create(p.ParameterType))
.ToArray();
return ctor.Invoke(args);
}
}
Registration happens once at startup, in a place often called the composition root:
var container = new TinyContainer();
container.Register<ITodoStore>(() => new SqlTodoStore(connectionString));
Tinyweb.UseControllerFactory(container);
Lifetimes are the obvious missing piece. A registration that returns a shared instance behaves like a singleton, one that builds a new object each time behaves like a transient. Real containers add per-request scopes, which matter for anything holding a database connection. Our version leaves that as an exercise, because the point is to see the mechanism.
Where a legacy dependency cannot be injected at all, isolating code with Moles and Fakes is the fallback.
Why bother?
Constructor injection documents dependencies, makes classes testable with simple fakes, and keeps wiring in a single place. For tiny dependencies you can even inject delegates; testing with Func and Action shows how. And if you want pluggable discovery rather than explicit registration, the Managed Extensibility Framework offers a declarative alternative.
Filters: code around the action
Some behaviour belongs around actions, not inside them: logging, authorization, timing, error handling. Copying that into every action is how bugs breed. A filter is a hook that runs before and after an action.
public interface IActionFilter
{
TinyResult Before(TinyRequest request); // return a result to short-circuit
void After(TinyRequest request, TinyResult result);
}
A filter that returns a non-null result from Before stops the pipeline and sends that result immediately. That is how authorization works.
Writing a few filters
public class LogFilter : IActionFilter
{
public TinyResult Before(TinyRequest request)
{
Trace.WriteLine("Start " + request.Method + " " + request.Path);
return null;
}
public void After(TinyRequest request, TinyResult result)
{
Trace.WriteLine("Finished " + request.Path);
}
}
public class RequireLoginFilter : IActionFilter
{
public TinyResult Before(TinyRequest request)
{
return request.IsAuthenticated ? null : new StatusResult(401, "Please sign in");
}
public void After(TinyRequest request, TinyResult result) { }
}
Running the pipeline
The dispatcher now wraps invocation:
public TinyResult Handle(TinyRequest request)
{
var (controller, method) = Resolve(request);
var filters = _filters.ToList();
foreach (var filter in filters)
{
var early = filter.Before(request);
if (early != null) return early;
}
var result = Invoke(controller, method, request);
for (int i = filters.Count - 1; i >= 0; i--)
filters[i].After(request, result);
return result;
}
Running the After methods in reverse order mirrors how nested wrappers behave: the first filter to start is the last to finish. (The tuple syntax above is a newer C# feature; on older compilers return a small class instead.)
Attaching filters to specific actions
Global filters are a start. Often you want them on just one controller or action, which attributes express neatly:
[RequireLogin]
public class AccountController { /* ... */ }
The dispatcher reads the attributes with reflection, the same way it found the controller. An attribute can implement the filter interface directly, or merely name the filter type to create via the container, so the filter can have its own dependencies.
Error handling as a filter
Exceptions deserve a dedicated place in the pipeline. Wrap invocation in a try/catch, convert the exception to a safe result, and log the details. Never show stack traces to visitors. A friendly error page or a small JSON error object is enough.
Summary
- A factory interface turns controller creation into a seam.
- A small container resolves constructors recursively.
- Filters give you before and after hooks and a way to short-circuit.
- Attributes target filters at the right actions.
Next, in part 4, we render HTML views and bind posted forms to real objects.
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 1: Getting Started
- Tinyweb Series, Part 2: Building APIs
- 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).