ASP.NET Core 1.0 was released today, and those of you who haven't already, I really urge you to check it out and have a play. There are a whole raft of features that make it a very enjoyable development experience compared to the venerable ASP.NET 4.x. Plus, upgrading from RTM to RC2 is a breeze.
Among those feature, is the first class support for dependency injection (DI) and inversion of control (IoC) containers. While this was possible in ASP.NET 4.x, it often felt like a bit of an after thought, with many different extensibility points that you had to hook into to get complete control. In this post I will show to integrate StructureMap into your ASP.NET Core apps to use as your dependency injection container.
Why choose a different container?
With ASP.NET Core, Microsoft have designed the framework to use dependency injection as standard everywhere. ASP.NET Core apps use a minimal feature set container by default, and require you to register all your services for injection throughout your app. Pretty much every Hello World app you see shows how to configure a services container.
This works great for small apps and demos, but as anyone who has a built a web app of a reasonable size knows, your container can end up being somewhat of a code smell. Each new class/interface also requires a tweak to your container configuration, in what often feels like a redundant step. If you find yourself writing a lot of code like this:
services.AddTransient<IMyService, MyService>();
then it may be time to start thinking about a more fully featured IoC container.
There are many possibilities (e.g. Autofac or Ninject) but my personal container of choice is StructureMap. StructureMap strongly emphasises convention over configuration, which helps to minimise a lot of the repeated mappings, allowing you to DRY up your container configuration.
To give a flavour of the benefits this can bring I'll show an example service configuration which includes a variety of different mappings. We'll then update the configuration to use StructureMap which will hopefully make the advantages evident. You can find the code for the project using the built in container here and for the project using StructureMap here.
As an aside, for those of you who may have looked at StructureMap a while back but were discouraged by the lack and/or age of the documentation - you have nothing to fear, it is really awesome now! Plus it's kept permanently in sync with the code base thanks to StoryTeller.
The test project using the built in container
First of all, I'll present the Startup
configuration for the example app, using the built in ASP.NET Core container. All of the interfaces and classes are just for example, so they don't have any actual members, but that's not really important in this case. However, the inter-dependencies are such that an instance of every configured class is required when constructing the default ValuesController
.
public class Startup
{
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables();
Configuration = builder.Build();
}
public IConfigurationRoot Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddMvc();
// Configure the IoC container
ConfigureIoC(services);
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
app.UseMvc();
}
public void ConfigureIoC(IServiceCollection services)
{
services.AddTransient<IPurchasingService, PurchasingService>();
services.AddTransient<ConcreteService, ConcreteService>();
services.AddTransient<IGamingService, CrosswordService>();
services.AddTransient<IGamingService, SudokuService>();
services.AddScoped<IUnitOfWork, UnitOfWork>(provider => new UnitOfWork(priority: 3));
services.Add(ServiceDescriptor.Transient(typeof(ILeaderboard<>), typeof(Leaderboard<>)));
services.Add(ServiceDescriptor.Transient(typeof(IValidator<>), typeof(DefaultValidator<>)));
services.AddTransient<IValidator<UserModel>, UserModelValidator>();
}
}
The Configure
call and the constructor are just the default from the ASP.NET Core template. In ConfigureServices
we first call AddMvc()
, as required to register all our MVC services in the container, and then call out to a method ConfigureIoC
, which encapsulates configuring all our app-specific services. I'll run through each of these registrations briefly to explain their intent.
services.AddTransient<IPurchasingService, PurchasingService>();
services.AddTransient<ConcreteService, ConcreteService>();
These are the simplest registrations - whenever an IPurchasingService
is requested, a new PurchasingService
should be created and used. Similarly for the specific concrete class ConcreteService
, a new instance should be created and used whenever it is requested.
services.AddTransient<IGamingService, CrosswordService>();
services.AddTransient<IGamingService, SudokuService>();
These next two calls are registering all of our instances of IGamingService
. These will both be injected when we have a constructor that requires IEnumerable<IGamingService>
for example.
services.AddScoped<IUnitOfWork, UnitOfWork>(provider => new UnitOfWork(priority: 3));
This is our first registration with a different lifetime - in this case, we are explicitly creating a new UnitOfWork
for each http request that is made (instead of every time an IUnitOfWork
is required, which may be more than once per Http request for Transient lifetimes.)
services.Add(ServiceDescriptor.Transient(typeof(ILeaderboard<>), typeof(Leaderboard<>)));
This next registration is our first use of generics and allows us to do some pretty handy things using open generics. For example, if I request a type of ILeaderboard<UserModel>
in my ValuesController
constructor, the container knows that it should inject a concrete type of Leaderboard<UserModel>
, without having to somehow register each and every generic mapping.
services.Add(ServiceDescriptor.Transient(typeof(IValidator<>), typeof(DefaultValidator<>)));
services.AddTransient<IValidator<UserModel>, UserModelValidator>();
Finally, we have another aspect of generic registrations. We have a slightly different situation here however - we have a specific UserModelValidator
that we want to use whenever an IValidator<UserModel>
is requested, and an open DefaultValidator<T>
that we want to use for every other IValidator<T>
request. We can specify the default IValidator<T>
cleanly, but we must also explicitly register every specific implementation that we need - a list which will no doubt get longer as our app grows.
With our services all registered what are the key things we note? Well the main thing is that pretty much every concrete service we want to use has to be registered somewhere in the container. Every new class we add will probably result in another line in our Startup
file, which could easily get out of hand. Which brings us to…
The test project using StructureMap
In order to use StructureMap in your ASP.NET Core app, you'll need to install the StructureMap.DNX library into your project.json, which as of writing is at version 0.5.1-rc2-final:
{
"dependencies": {
"StructureMap.Dnx": "0.5.1-rc2-final"
}
}
I'll present the Startup
configuration for the same app, but this time using StructureMap in place of the built-in container. Those functions which are unchanged are elided for brevity:
public class Startup
{
public Startup(IHostingEnvironment env) { /* Unchanged */}
public IConfigurationRoot Configuration { get; }
public IServiceProvider ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddMvc()
.AddControllersAsServices();
return ConfigureIoC(services);
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { /* Unchanged */}
public IServiceProvider ConfigureIoC(IServiceCollection services)
{
var container = new Container();
container.Configure(config =>
{
// Register stuff in container, using the StructureMap APIs...
config.Scan(_ =>
{
_.AssemblyContainingType(typeof(Startup));
_.WithDefaultConventions();
_.AddAllTypesOf<IGamingService>();
_.ConnectImplementationsToTypesClosing(typeof(IValidator<>));
});
config.For(typeof(IValidator<>)).Add(typeof(DefaultValidator<>));
config.For(typeof(ILeaderboard<>)).Use(typeof(Leaderboard<>));
config.For<IUnitOfWork>().Use(_ => new UnitOfWork(3)).ContainerScoped();
//Populate the container using the service collection
config.Populate(services);
});
return container.GetInstance<IServiceProvider>();
}
}
On first glance it may seem like there is more configuration, not less! However, this configuration is far more generalised, emphasises convention over explicit class registrations, and will require less modification as the app grows. I'll run through each of the steps again and then we can compare the two approaches.
First of all, you should note that the ConfigureServices
(and ConfigureIoC
) call returns an instance of an IServiceProvider
. This is the extensibility point that allows the built in container to be swapped out in place of StructureMap, and is implemented by the StructureMap.DNX library.
Within the ConfigureIoC
method we create our StructureMap Container
- this is the DI container against which mappings are registered - and configure it using a number of different approaches.
config.Scan(_ =>
{
_.AssemblyContainingType(typeof(Startup));
_.WithDefaultConventions();
// ...remainder of scan method
}
The first technique we use is the assembly scanner using Scan
. This is one of the most powerful features of StructureMap as it allows you to automatically register classes against interfaces without having to configure them explicitly. In this method we have asked StructureMap to scan our assembly (the assembly containing our Startup
class) and to look for candidate classes WithDefaultConventions
. The default convention will register concrete classes to interfaces where the names match, for example IMyService
and MyService
. From personal experience, the number of these cases will inevitably grow with the size of your application, so the ability to have the simple cases like this automatically handled is invaluable.
_.AddAllTypesOf<IGamingService>();
Within the scanner we also automatically register all our implementations of IGamingService
using AddAllTypesOf
. This will automatically find and register CrosswordService
and SudokuService
against the IGamingService
interface. If later we add WordSearchService
as an additional implementation of IGamingService
, we don't have to remember to head back to our Startup
class and configure it - StructureMap will seamlessly handle it.
_.ConnectImplementationsToTypesClosing(typeof(IValidator<>));
The final auto-registration calls ConnectImplementationsToTypesClosing
. This method looks for any concrete IValidator<T>
implementations that close the interface, and registers them. For our app, we just have the one - UserModelValidator
. However if you add new ones to your app, an AvatarModelValidator
for example, StructureMap will automatically pick them up.
config.For(typeof(IValidator<>)).Add(typeof(DefaultValidator<>));
For those IValidator<T>
without an explicit implementation, we register the open generic DefaultValidator<T>
, which will be used when there is no concrete class that closes the generic for T
. So requests to IValidator<Something>
will be resolved with DefaultValidator<Something>
.
config.For(typeof(ILeaderboard<>)).Use(typeof(Leaderboard<>));
config.For<IUnitOfWork>().Use(_ => new UnitOfWork(3)).ContainerScoped();
The next two registrations work very similarly to their equivalents using the built in DI container. The first call registers the Leaderboard<T>
type to be used wherever an ILeaderboard<T>
is requested. The final call describes an expression that can be used to create a new UnitOfWork
, and specifies that it should be ContainerScoped
, i.e. per Http request.
config.Populate(services);
The final call in our StructureMap configuration comes from the StructureMap.DNX library. This call takes all those services which were previously registered in the container (e.g. all the MVC services etc registered by the framework itself), and registers them with StructureMap.
Note: I won't go in to why here, (this issue covers it pretty well), but if you run into problems with your MVC controllers not being created correctly it is probably because you need to call
AddControllersAsServices
after callingAddMvc
inConfigureServices
. This ensures that StructureMap is used to create the instances of your controllers instead of an internalDefaultControllerActivator
, which will bypass a lot of your StructureMap configuration. In this example app,AddControllersAsServices
is required forConcreteService
to be automatically resolved correctly.
What's the point?
Given that we wrote just as much configuration code for this small app using StructureMap as we did for the built in container, you may be thinking "why bother?" And for very small apps you may well be right, the extra dependency may not be worthwhile. The real value appears when your app starts to grow. Just consider how many of our concrete services were automatically registered without us needing to explicitly configure them:
PurchasingService
- default conventionCrosswordService
- registered as implementsIGamingService
SudokuService
- registered as implementsIGamingService
ConcreteService
- concrete services are automatically registeredUserModelValidator
- closes theIValidator<T>
generic
That's a whopping 5 services out of the 8 we registered using the built in service that we didn't need to mention with StructureMap. As your app grows and more services are added, you'll find you have to touch your ConfigureIoC
method far less than if you had stuck with the built in container.
If you're still not convinced there are a whole host of other benefits and features StructureMap can provide, some of which are:
- Creating child/nested containers e.g. for multi tenancy support
- Multiple profiles, similarly for tenancy support
- Setter Injection
- Constructor selection
- Conventional "Auto" Registration
- Automatic
Lazy<T>
/Func<T>
resolution - Auto resolution of concrete types
- Interception and Configurable Decorators
- Amazing debugging/testing tools for viewing inside your container
- Configurable assembly scanning
Conclusion
In this post I tried to highlight the benefits of using StructureMap as your dependency injection container in your ASP.NET Core applications. While the built-in container is a great start, using a more fully featured container will really simplify configuration as your app grows. I highly recommend checking out the documentation at http://structuremap.github.io to learn more. Give it a try in your ASP.NET Core applications using StructureMap.DNX.