-
Notifications
You must be signed in to change notification settings - Fork 177
EmbedIO and IoC containers
- Introduction
- Objectives
- Creating and disposing an IoC scope
- Using IoC in your modules
- Putting it all together
Unlike ASP.NET, EmbedIO (v3.4.3 at the time of writing) does not come with built-in dependency injection. This means that if you need to instantiate request-scoped services you're pretty much on your own. It would be nice if we could automatically create and use an IoC scope bound to every HTTP context; fortunately, there are some extension points in EmbedIO that can be exploited to do just that.
I will use Autofac in this article, but the code can be easily adapted to other IoC containers. As a matter of fact, it will need a bit of adaptation even if you use Autofac. By adapting the code to your application's specific needs, you will understand the underlying concepts much better than by blindly copy-pasting.
Here's what we're going to do:
- create an IoC scope for each HTTP context;
- dispose the scope just before the context is disposed, so as to bind the scope's lifetime to the lifetime of the HTTP context;
- find out how we can use the scope to obtain request-scoped services from modules.
If you've been using an IoC container for more than a couple days, you already know how to create a scope (also known as a "child container" or "scoped container"). What you need to know is:
- how do you create the scope before any module is called to serve a request?
- where do you put a reference to the scope to be able to retrieve and use it in your modules?
- how do you tell EmbedIO to dispose the scope when it disposes the related HTTP context?
I'm glad you asked. 😁 The answer to the first question is pretty simple: create a module (a class derived from WebModuleBase
) whose OnRequestAsync
method creates the scope. You will need to pass an IoC container to the module's constructor, so it can be used to create child scopes.
IHttpContext
's Items
collection provides a place to store a reference to the scope for later use.
Finally, to dispose the created scope we can use a callback passed to the context's OnClose
method.
using System;
using System.Threading.Tasks;
using Autofac;
using EmbedIO;
using EmbedIO.Utilities;
namespace EmbedIO.DependencyInjection
{
public class IoCModule : WebModuleBase
{
// Unique key for storing scopes in HTTP context items
internal static readonly object ScopeKey = new object();
private readonly IContainer _container;
private readonly Action<ContainerBuilder>? _configurationAction;
public IoCModule(IContainer container, Action<ContainerBuilder>? configurationAction = null)
: base(UrlPath.Root)
{
// We will need the container to create child scopes.
_container = container ?? throw new ArgumentNullException(nameof(container));
// We can optionally use a configuration action
// to register additional services for each HTTP context.
_configurationAction = configurationAction;
}
// Tell EmbedIO that this module does not handle requests completely.
public override bool IsFinalHandler => false;
protected override Task OnRequestAsync(IHttpContext context)
{
// Create a scope for the HTTP context, tagging it with the context.
// Use the configuration action if one has been specified.
var scope = _configurationAction == null
? _container.BeginLifetimeScope(context)
: _container.BeginLifetimeScope(context, _configurationAction);
// Store the scope for later retrieval.
context.Items.Add(ScopeKey, scope);
// Ensure that the scope is disposed when EmbedIO is done processing the request.
context.OnClose(OnContextClose);
return Task.CompletedTask;
}
private void OnContextClose(IHttpContext context)
{
// Retrieve the scope and dispose it
var scope = context.Items[ScopeKey] as ILifetimeScope;
context.Items.Remove(ScopeKey);
scope!.Dispose();
}
}
public static class WebServerExtensions
{
// Fluent extension method to add an IoC module to a web server.
public static TServer WithIoC<TServer>(this TServer @this, IContainer container, Action<ContainerBuilder>? configurationAction = null)
where TServer : WebServer
{
@this.Modules.Add(new IoCModule(container, configurationAction));
return @this;
}
}
public static class HttpContextExtensions
{
// Shortcut method to retrieve an IoC scope from a context.
public static ILifetimeScope GetIoCScope(this IHttpContext @this) =>
@this.Items[IoCModule.ScopeKey] as ILifetimeScope ?? throw new ApplicationException("IoC scope not initialized for HTTP context");
}
}
You will need to add an IoCModule
to your WebServer
before any module that needs to use the per-request IoC scope.
Now that we have an IoC scope (or child container, or scoped container, or whatever your favorite IoC container library likes to call it) associated with every HTTP context, using it to obtain a service is pretty straightforward. Wherever you have a HTTP context available, you can do this:
// ...
var service = context.GetIoCScope().Resolve<IMyService>();
// ...
Since you can initialize a WebApiModule
with a factory function for every controller type, you may be tempted to register your web API controller classes in the IoC container and retrieve them in a factory function. Do not do that! The factory functions you can pass to WebApiModuleBase.RegisterControllerType
have no parameters, hence your factory function would have no idea where to retrieve the HTTP context of the request and, by consequence, the IoC scope to use.
To overcome the above limitation, you may register your controller classes as transient, using InstancePerDependency
as explained here. However, this will only work if your controllers do not depend on any request-scoped service, as they will always be resolved by the root container.
One additional note, in case you are in doubt: never, never, absolutely never register a web API controller class as a singleton! It may seem to work at first, but it will manifest subtle bugs (or even crash spectacularly) as soon as two or more concurrent requests are served with the same controller.(*)
There are two ways to correctly use IoC in a web API controller:
- you can resolve services as needed, using
HttpContext.GetIoCScope()
in your controller methods, or - you can override
WebApiController.OnBeforeHandler
to retrieve services you know you will need and store them in instance properties, like this:
using System;
using System.Collections.Generic;
using System.Data;
using System.Threading.Tasks;
using Autofac;
using EmbedIO;
using EmbedIO.DependencyInjection;
using EmbedIO.Routing;
using EmbedIO.WebApi;
namespace EmbedIO.Samples.DependencyInjection
{
public class MyController : WebApiController
{
private IDbConnection _connection = null!;
protected override void OnBeforeHandler()
{
base.OnBeforeHandler();
_connection = HttpContext.GetIoCScope().Resolve<IDbConnection>();
}
[Route(HttpVerbs.Get, "/items")]
public Task<List<Item>> GetItems()
{
var items = ...; // Use _connection to retrieve items.
return items;
}
}
}
The code below, adapted from EmbedIO.Samples
and slightly simplified, shows how you can use Autofac in your web server.
using System;
using Autofac;
using EmbedIO;
using EmbedIO.DependencyInjection;
namespace EmbedIO.Samples.DependencyInjection
{
internal class Program
{
private static void Main()
{
var builder = new ContainerBuilder();
// Register your services here
using (var container = builder.Build())
using (var ctSource = new CancellationTokenSource())
{
Task.WaitAll(
RunWebServerAsync("http://*:8877", container, ctSource.Token),
WaitForUserBreakAsync(ctSource.Cancel));
}
Console.WriteLine("Press any key to exit.");
WaitForKeypress();
}
// Create and run a web server.
private static async Task RunWebServerAsync(
string url,
IContainer container,
CancellationToken cancellationToken)
{
using var server = CreateWebServer(url, container);
await server.RunAsync(cancellationToken).ConfigureAwait(false);
}
// Prompt the user to press any key; when a key is next pressed,
// call the specified action to cancel operations.
private static async Task WaitForUserBreakAsync(Action cancel)
{
// Be sure to run in parallel.
await Task.Yield();
// Clear the console input buffer and wait for a keypress
while (Console.KeyAvailable)
Console.ReadKey(true);
Console.ReadKey(true);
cancel();
}
// Create and configure our web server.
private static WebServer CreateWebServer(string url, IContainer container) =>
new WebServer(o => o
.WithUrlPrefix(url)
.WithMode(HttpListenerMode.EmbedIO))
.WithIoC(container)
// Add other modules
;
}
}
* OK, I lied a little bit: it will work if you controller never uses either of its HttpContext
, Route
, CancellationToken
, Request
, Response
, User
, and Session
properties. Kind of an edge case, as you can see. Plus, it's bad practice anyway.