Dependency Injection in ASP.NET MVC6

Dependency injection (DI) has been possible in previous versions of MVC. With each new version DI has been easier to implement and, with MVC6, DI is supplied right out of the box. In this article we’ll look at how the new DI implementation works, what are its weaknesses and how we can replace it with our favorite DI framework.

What’s new

The unification of APIs across ASP.NET is a common theme throughout ASP.NET 5, and dependency injection is no different. The new ASP.NET stack including: MVC, SignalR and Web API, etc. rely on a built-in minimalistic DI container. The core features of the DI container have been abstracted out to the IServiceProvider interface and are available throughout the stack. Because the IServiceProvider is the same across all components of the ASP.NET framework a single dependency can be resolved from any part of the application.

The DI container supports just 4 modes of operation:

  • 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.

Basic setup

Let’s walk through setting up DI in a MVC application. To demonstrate the basics, we’ll resolve the dependency for the service used to get project data. We don’t need to know anything about the service other than that it implements the IProjectService interface, an interface custom to our demo project. IProjectService has one method, GetOrganization(), that method retrieves an organization and its corresponding list of projects.

public interface IProjectService
{
    string Name { get; }
    Organization GetOrganization();
}

public class Organization
{
    public string Name { get; set; }
    [JsonProperty("Avatar_Url")]
    public string AvatarUrl { get; set; }
    public IQueryable<Project> Projects { get; set; }
}

We’ll use the IProjectService to get the organization data and display it in a view. Let’s start by setting up the controller where the service will be used. We’ll use constructor injection by creating a new constructor method for our controller that accepts an IProjectService. Next, the Index action will call GetOrganization, sending the data to the view to be rendered.

private readonly IProjectService projectService;
public HomeController(IProjectService projectService)
{
    this.projectService = projectService;
}
public IActionResult Index()
{
    Organization org = projectService.GetOrganization();
    return View(org);
}

If we try to run the application at this point we’ll receive an exception because we haven’t yet added a concrete implementation of our IProjectService to the DI container.

InvalidOperationException: Unable to resolve service for type 'DependencyInjectionMVC6Demo. Services. IProjectService' while attempting to activate 'DependencyInjectionMVC6Demo. Controllers. HomeController'.
Microsoft. Framework. DependencyInjection. ActivatorUtilities.GetService(IServiceProvider sp, Type type, Type requiredBy, Boolean isDefaultParameterRequired)

The exception message shows that the code fails during a call to ActivatorUtilities.GetService. This is valuable information because it shows that in MVC6 the DI container is already involved in the controller’s construction. Now we just need to tell the container how to resolve the dependency.

In order to resolve the dependency, we need a concrete implementation of IProjectService. We’ll add a DemoService class and, for simplicity, it will use static dummy data.

public class DemoService : IProjectService
{
    public string Name { get; } = "Demo";

    public Organization GetOrganization() => new Organization
    {
        Name = this.Name,
        AvatarUrl = $"http://placehold.it/100&text={this.Name}",
        Projects = GetProjects()
    };

private IQueryable<Project> GetProjects() => new List<Project> {
         new Project {
             Id = 0,
             Description = "Test project 0",
             Name = "Test 0",
             Stars = 120
         },
         //...
         new Project {
             Id = 4,
             Description = "Test project 4",
             Name = "Test 4",
             Stars = 89
         }
    }.AsQueryable();
}

Finally, we’ll instruct the DI container to instantiate a new DemoService whenever IProjectService is required. To configure the container we’ll modify the ConfigureServices method in Startup.cs. Our configuration will be added to the end of this method.

// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
    //... other services
    // Add MVC services to the services container.
    services.AddMvc();

    //our services
}

The service is added by using the AddTransient extension method on the services collection, and setting the IProjectService as the type of service and the DemoService as the implementation.

public void ConfigureServices(IServiceCollection services)
{
    //... other services
    // Add MVC services to the services container.
    services.AddMvc();

    //our services
    services.AddTransient<IProjectService, DemoService>();
}

With the service added, DemoService will now be instantiated when the controller is created, and the exception will no longer be thrown.

demo-service

Weaknesses

expectations-real

Having a baked in DI layer throughout the ASP.NET stack is helpful, but you should temper your expectations. While it is useful for simple scenarios, it is very limited. The default container only supports constructor injection and it can only resolve types with one, and only one, public constructor. There’s no need to worry though, you have complete control over the DI container and it’s easy to replace with your favorite DI solution.

Replacing default MVC6 DI container

Setting up a replacement DI container does not require a lot of code, but the process could be more discoverable. We’ll continue with our previous example to show exactly where the extensibility point is.

The method we are using, ConfigureServices, is actually one of two delegates the application will use to register the DI container and its services. The first, and default, delegate is an Action<IServiceCollection>, which was used in the previous example to resolve our DemoService. The second is a Func<IServiceCollection, IServiceProvider>, which is used to replace the default container by returning a custom implementation IServiceProvider.

To change from the default and allow an alternative IServiceProvider to be used, we’ll need to change the method signature. We can test this easily by modifying our example, we’ll change the method from returning void to IServiceProvider, then at the very end of the method return services.BuildServiceProvider(). At this point the method does exactly the same thing as before – we’re still using the default DI container.

public IServiceProvider ConfigureServices(IServiceCollection services)
{
    //... other services
    // Add MVC services to the services container.
    services.AddMvc();

    //our services
    services.AddTransient<IProjectService, DemoService>();
    return services.BuildServiceProvider();
}

The application will still build and run just as it did before, except now we’ve exposed the extensibility point we need.

Next, we need to add a third party DI container. There are a variety of DI containers to choose from, all with their own strengths and weaknesses. For this example we’ll be using Autofac. At the time of writing, Autofac has a DNX compatible alpha version we can use.

To install Autofac we’ll open our package.json file and add the Autofac alpha binaries.

"dependencies": {
    ...
    "Autofac": "4.0.0-alpha2",
    "Autofac.Dnx": "4.0.0-alpha2"
}

In the Startup.cs, we’ll modify the ConfigureServices method again. This time, we’ll add our dependencies to Autofac and resolve the container back to an IServiceProvider replacing the default container.

public IServiceProvider ConfigureServices(IServiceCollection services)
    {
        //... other services
        // Add MVC services to the services container.
        services.AddMvc();

        //Autofac config
        var builder = new ContainerBuilder();

        builder.RegisterType<DemoService>()
            .As<IProjectService>().InstancePerLifetimeScope();

        //Populate the container with services that were previously registered
        builder.Populate(services);

        var container = builder.Build();

        return container.Resolve<IServiceProvider>();
}

Now our application is using the Autofac container in place of the default container. We can fully take advantage of Autofac’s features and, because ASP.NET was developed against the IServiceProvider, we wont need to change any other code in our project.

Let’s make one final change to the example by swapping our DemoService to another implementation that uses GitHub’s REST API to populate our project list. The GitHubService implements the same IProjectService as DemoService, however it takes a name parameter in its constructor. Using Autofac we can set the Name value of the constructor in the configuration.

public IServiceProvider ConfigureServices(IServiceCollection services)
{
    //...
    builder.Register(svc => new GitHubService("Telerik"))
        .As<IProjectService>().InstancePerLifetimeScope();

    //builder.RegisterType<DemoService>()
    //  .As<IProjectService>().InstancePerLifetimeScope();
}

Restarting the application reveals that our new GitHubService was successfully resolved without touching our controller code.

github-service

Wrap up & Resources

The new ASP.NET provides out-of-the-box DI throughout the stack. The new DI container is basic enough to get the job done, but lacks robust configurations. The container is easy enough to replace if you know were to look, allowing you to use fully-featured, third party tools. The source code for both the framework and the examples for this project can be found out GitHub.

[cm_ad_changer campaign_id=141]

Comments