Dependency Injection in ASP.NET vNext
Dependency
Injection (DI) is a software design pattern, a particular case of the
Inversion of Control pattern, in which one or more dependencies are
injected into dependent objects. The pattern is used to create program
designs that are loosely coupled and testable.
This article assumes that you are already familiar with DI. If not, you can read this article for a brief introduction.
DI in ASP.NET vNext
In ASP.NET vNext, dependency injection is a first
class citizen. While in the previous versions of the framework, DI was
partially supported, in ASP.NET vNext it is available throughout the
entire stack. A minimalistic DI container is provided out of the box but
we are leaving the door open to BYOC (Bring Your Own Container). The
default container is useful in the cases when you don’t need any
advanced injection capabilities (see the known limitations section at
the end of the post).
BYOC is possible because of an abstraction over the
actual DI container implementation. The abstraction is the
IServiceProvider interface and it represents the least set of container
behavior our components are limited to assuming are present. All the
framework components (MVC, Routing, SignalR, Entity Framework, etc.)
rely only on the capabilities of IServiceProvider, but your own
application code can use any feature that your chosen DI container has.
When you BYOC, you can replace the default implementation of
IServiceProvider with a wrapper around your own container. Once that
happens, all the dependency resolution calls will be routed to your own
container. In the case when you want to use your own container strictly
for your own custom types, we support fallback to our default container.
Because all framework components use the same
container to register services, we can now flow dependencies across the
stack. This opens the door to new scenarios that were not possible
before, like injecting a SignalR broadcaster into an MVC controller
action. As we walk up the stack, there are different layers of
dependency resolvers. All dependencies are added to a single container
and everyone can see everybody else’s services. The single container
also addresses the cross-cutting concern customization story. It is
trivial in the new stack to change cross-cutting concerns (e.g. logging)
via a single entry point.
The out of the box container supports the following lifestyles:
Lifestyle | Description |
Instance | A specific instance is given all the time. You are responsible for its initial creation |
Transient | A new instance is created every time |
Singleton | A single instance is created and it acts like a singleton |
Scoped | A single instance is created inside the current scope. It is equivalent to Singleton in the current scope |
Per Request Scope
A popular feature for DI in web applications is to
create objects that have a single instance per web request. This means
that the objects acts as a singleton inside that request but two
distinct requests will have different instances of the objects.
In ASP.NET vNext, the Per Request Scope is achieved
using a middleware and a scoped lifestyle. The middleware, when invoked,
will create a new scoped container which will replace the container for
the current request. All the subsequent middleware in the pipeline will
then utilize the scoped container. After the request flows through the
pipeline and the container middleware is signaled to complete, the scope
is destroyed and all the objects created inside it are disposed.
The source code for the ContainerMiddleware, is available on GitHub.
In the rare case in which you need to create your own
container scope, you can use the IServiceScopeFactory to do this. When
the default implementation of the IServiceProvider is created, the
IServiceScopeFactory is one of the services that are registered by
default.
New vs Old
For the purpose of showing the differences between DI
in the old and new stack, we are going to use an MVC controller that
writes a string provided through an injected dependency:
Code Snippet
- public interface IMessageGenerator
- {
- string GenerateMessage();
- }
- public class HelloMessageGenerator : IMessageGenerator
- {
- public string GenerateMessage()
- {
- return"Hello DI!";
- }
- }
- public class MessageController : Controller
- {
- private readonly IMessageGenerator messageGenerator;
- public MessageController(IMessageGenerator generator)
- {
- if (generator == null)
- {
- throw new ArgumentNullException("generator", "The generator dependecy is mandatory");
- }
- this.messageGenerator = generator;
- }
- public string GetMessage()
- {
- return this.messageGenerator.GenerateMessage();
- }
- }
None of the code above is changed between the old and
the new stack. The only difference is where and how the dependency are
registered and resolved.
In the old MVC stack, controller dependencies are
resolved through a custom controller factory. For the purpose of this
demo, we are going to implement the Poor Man’s DI and manually compose
the dependencies:
Code Snippet
- public class DIControllerFactory : DefaultControllerFactory
- {
- public override IController CreateController(RequestContext requestContext, string controllerName)
- {
- // If a message controller is requested...
- if (controllerName == "Message")
- {
- // ... then create a new controller and set up the dependency
- return new MessageController(new HelloMessageGenerator());
- }
- // Otherwise, fallback to the default implementation
- return base.CreateController(requestContext, controllerName);
- }
- }
- public class MvcApplication : HttpApplication
- {
- protected void Application_Start()
- {
- ...
- // Register the controller factor
- ControllerBuilder.Current.SetControllerFactory(new DIControllerFactory());
- ...
- }
- }
The controller factory will inject a concrete
implementation (HelloMessageGenerator) of the interface
(IMessageGenerator) in the Message controller.
The same code, ported to ASP.NET vNext, doesn’t need a
Poor Man’s DI implementation. As mentioned before, in this new stack, a
DI container is available out of the box. Thus, all we need to do is
tell it what is the mapping between the interface and the concrete
implementation.
Code Snippet
- public class Startup
- {
- public void Configure(IBuilder app)
- {
- ...
- app.UseServices(services =>
- {
- ...
- // Set up the dependencies
- services.AddTransient<IMessageGenerator, HelloMessageGenerator>();
- ...
- });
- ...
- }
- }
The default implementation of the controller factory
in ASP.NET vNext uses the IServiceProvider to resolve the dependencies.
If you BYOC, none of the code above will change. You would only have to
tell the application to use a different implementation of
IServiceProvider.
Replacing the default DI container
The code below shows how the previous sample can be rewritten to use Autofac instead of the out of the box container.
The code uses compiler directives to use Autofac code
only for the full .NET 4.5 Framework because Autofac is not available
for the .NET Core Framework 4.5. In Visual Studio “14” CTP you can
change the target framework of a project by right clicking on it and
going to Properties -> Active Target Framework.
Code Snippet
- using System;
- using Microsoft.AspNet.Builder;
- using Microsoft.AspNet.Routing;
- using Microsoft.Framework.DependencyInjection;
- using Microsoft.Framework.DependencyInjection.Fallback;
- using Microsoft.AspNet.Mvc;
- using Microsoft.AspNet.RequestContainer;
- using DISample.Models;
- #if NET45
- using Autofac;
- using Microsoft.Framework.DependencyInjection.Autofac;
- using Microsoft.Framework.OptionsModel;
- #endif
- namespace DISample
- {
- public class Startup
- {
- public void Configure(IBuilder app)
- {
- // Add the MVC services
- ServiceCollection services = new ServiceCollection();
- services.AddMvc();
- services.Add(OptionsServices.GetDefaultServices());
- // The NET45 symbol is defined when the project targets .NET Framework 4.5
- #if NET45
- // Create the autofac container
- ContainerBuilder builder = new ContainerBuilder();
- // Register the message generator through autofac
- builder.RegisterType<HelloMessageGenerator>().As<IMessageGenerator>();
- // Create the container and use the default application services as a fallback
- AutofacRegistration.Populate(
- builder,
- services,
- fallbackServiceProvider: app.ApplicationServices);
- IContainer container = builder.Build();
- // Replace the default container
- app.ApplicationServices = container.Resolve<IServiceProvider>();
- #else
- // Here we are running on .NET Core Framework 4.5 so we cannot use Autofac
- services.AddTransient<IMessageGenerator, HelloMessageGenerator>();
- app.ApplicationServices = services.BuildServiceProvider(app.ApplicationServices);
- #endif
- // MVC requires the container middleware
- app.UseMiddleware(typeof(ContainerMiddleware));
- app.UseMvc(routes =>
- {
- routes.MapRoute(
- name: "default",
- template: "{controller}/{action}/{id?}",
- defaults: new { controller = "Message", action = "GetMessage" });
- });
- }
- }
- }
In order to compile the code above, you must add the
dependency injection dependencies to project.json. Since Autofac is not
available for .NET Core Framework 4.5, the Autofac dependency is only
defined for in the net45 section:
Code Snippet
- {
- "dependencies": {
- "Helios": "0.1-alpha-build-*",
- "Microsoft.AspNet.Mvc": "0.1-alpha-build-*",
- "Microsoft.AspNet.Identity.Entity": "0.1-alpha-build-*",
- "Microsoft.AspNet.Identity.Security": "0.1-alpha-build-*",
- "Microsoft.AspNet.Security.Cookies": "0.1-alpha-build-*",
- "Microsoft.AspNet.Server.WebListener": "0.1-alpha-build-*",
- "Microsoft.AspNet.StaticFiles": "0.1-alpha-build-*",
- "Microsoft.Data.Entity": "0.1-alpha-build-*",
- "Microsoft.Data.Entity.SqlServer": "0.1-alpha-build-*",
- "Microsoft.Framework.ConfigurationModel.Json": "0.1-alpha-build-*",
- "Microsoft.VisualStudio.Web.BrowserLink.Loader": "14.0-alpha",
- "Microsoft.Framework.DependencyInjection": "0.1-alpha-build-*"
- },
- "commands": {
- "web": "Microsoft.AspNet.Hosting --server Microsoft.AspNet.Server.WebListener --server.urls http://localhost:5000"
- },
- "configurations": {
- "net45": {
- "dependencies": {
- "System.Data": "",
- "System.ComponentModel.DataAnnotations": "",
- "Microsoft.Framework.DependencyInjection.Autofac": "0.1-alpha-build-*",
- "Autofac": "3.3.0"
- }
- },
- "k10": {
- }
- }
- }
Best Practices for DI in ASP.NET vNext
When using DI, we recommend the following practices:
-
Register all dependencies in the application startup method, before doing anything else.
-
Avoid consuming the IServiceProvider interface directly. Instead, add explicit dependencies as constructor parameters and let the callers resolve them. Use abstract factories when the number of dependencies becomes hard to manage.
-
Code against contracts rather than actual implementations.
Known Limitations
The out of the box container for ASP.NET vNext Alpha has a few known limitations:
-
It only supports constructor injection
-
It can only resolve types with one and only one public constructor
-
It doesn’t support advanced features (like per thread scope or auto discovery)
Links
Here you can find all the links used for this blog post.
- Service Locator Pattern (MSDN)
- Service Locator Pattern (Wikipedia)
- Dependency Injection in ASP.NET vNext
- IServiceProvider (MSDN)
- GitHub repository for the ASP.NET vNext Dependency Injection component
- GitHub repository for the ASP.NET vNext Hosting component
- Example from the ASP.NET web page
- Mark Seemanns Point of View
- Mark Seemanns Conforming Container
- Mark Seemanns Succeeding with Dependency Injection
- Discussion in the ASP.NET vNext Forum
- Inversion of Control and Dependency Injection (MSDN)
- Inversion of Control (Wikipedia)
- Dependency Inversion Principle
- Liskov Substitution Principle
- LINQ Providers
No comments:
Post a Comment