다음을 통해 공유


Dependency Injection in ASP.NET vNext

Motivation 

Microsoft changed the way how dependency injection (DI) is done in ASP.NET vNext. In previous versions of ASP.NET there were different entry points for user defined classes like the IControllerFactory for ASP.NET MVC applications or the IHttpControllerActivator for ASP.NET WebAPI applications. Starting with ASP.NET vNext DI becomes a first-class citizen in the framework through the whole stack. With showing the new DI features in ASP.NET vNext, discussions came up whether it is the correct way to do DI. In this post I like to show how DI works in ASP.NET vNext and then lead to the current discussion about it. At the end I like to ask you for your opinion.

How DI works in ASP.NET vNext

As mentioned, DI is a first-class citizen in the new version of ASP.NET. The ASP.NET team achieved this by using the Service Locator Pattern since all framework components (MVC, EF, SignalR etc.) rely on the IServiceProvider interface. Further, in order to bring your own DI container to inject your own specific objects, you can replace the default implementation of IServiceProvider with a wrapper around this container and use the standard container as fallback for framework specific components (e.g. MVC or SignalR).

By using the same container for all types, dependencies can flow through the whole stack (e.g. a SignalR broadcaster can be injected into MVC controller actions, as stated by the ASP.NET team). So, services can be registered for all framework components at a single place. That keeps the code a little bit more clean, since you do not need to register a factory here, a factory there for your own types. This is one advantage of the new DI in ASP.NET vNext from my point of view.

But let’s come to an example.

The basic Approach

First let’s have a look at the basic or default DI container built in ASP.NET vNext. Imagine the following ASP.NET WebAPI 6 controller for managing todo items (to save time, I took this example from the ASP.NET web page):

[Route("api/[controller]")]
public class TodoController : Controller
{
   static readonly List<TodoItem> _items = new List<TodoItem>()
   {
      new TodoItem { Id = 1, Title = "First Item" }
   };

   [HttpGet]
   public IEnumerable<TodoItem> GetAll()
   {
      return _items;
   }

   [HttpGet("{id:int}", Name = "GetByIdRoute")]
   public IActionResult GetById(int id)
   {
      var item = _items.FirstOrDefault(x => x.Id == id);

      if (item == null)
      {
         return HttpNotFound();
      }

      return new ObjectResult(item);
   }

   [HttpPost]
   public void CreateTodoItem([FromBody] TodoItem item)
   {
      if (!ModelState.IsValid)
      {
         Context.Response.StatusCode = 400;
      }
      else
      {
         item.Id = 1 + _items.Max(x => (int?)x.Id) ?? 0;
         _items.Add(item);

         string url = Url.RouteUrl("GetByIdRoute", new { id = item.Id }, Request.Scheme, Request.Host.ToUriComponent());

         Context.Response.StatusCode = 201;
         Context.Response.Headers["Location"] = url;
      }
   }

   [HttpDelete("{id}")]
   public IActionResult DeleteItem(int id)
   {
      var item = _items.FirstOrDefault(x => x.Id == id);

      if (item == null)
      {
         return HttpNotFound();
      }

      _items.Remove(item);

      return new HttpStatusCodeResult(204); // 201 No Content
   }
}

There is a simple list within this controller containing the todo items and some Get-, Post- and Delete-methods to manage todo items. Nothing special. Now the task is, to not hard-code the logic in the controller. Instead, the logic should be injected into the controller. For this we create an interface for a todo item repository, then create a class the implements this repository and initialize the controller with an instance of the repository class in the constructor. Then the controller implementation uses this repository rather the hard-coded logic. The result looks like this:

The ITodoRepository interface:

public interface ITodoRepository
{
   IEnumerable<TodoItem> AllItems { get; }
   void Add(TodoItem item);
   TodoItem GetById(int id);
   bool TryDelete(int id);
} 

The repository implementation:

public class TodoRepository : ITodoRepository
{
   readonly List<TodoItem> _items = new List<TodoItem>();

   public IEnumerable<TodoItem> AllItems
   {
      get
      {
         return _items;
      }
   }

   public TodoItem GetById(int id)
   {
      return _items.FirstOrDefault(x => x.Id == id);
   }

   public void Add(TodoItem item)
   {
      item.Id = 1 + _items.Max(x => (int?)x.Id) ?? 0;
      _items.Add(item);
   }

   public bool TryDelete(int id)
   {
      var item = GetById(id);

      if (item == null)
      {
         return false;
      }
   
      _items.Remove(item);
   
      return true;
   }
}

The new TodoController implementation:

[Route("api/[controller]")]
public class TodoController : Controller
{
   private readonly ITodoRepository _repository;

  /// The framework will inject an instance of an ITodoRepository implementation.
   public TodoController(ITodoRepository repository)
   {
      _repository = repository;
   }

   [HttpGet]
   public IEnumerable<TodoItem> GetAll()
   {
      return _repository.AllItems;
   }

   [HttpGet("{id:int}", Name = "GetByIdRoute")]
   public IActionResult GetById(int id)
   {
      var item = _repository.GetById(id);

      if (item == null)
      {
         return HttpNotFound();
      }

      return new ObjectResult(item);
   }

   [HttpPost]
   public void CreateTodoItem([FromBody] TodoItem item)
   {
      if (!ModelState.IsValid)
      {
         Context.Response.StatusCode = 400;
      }
      else
      {
         _repository.Add(item);

         string url = Url.RouteUrl("GetByIdRoute", new { id = item.Id }, Request.Scheme, Request.Host.ToUriComponent());
         Context.Response.StatusCode = 201;
         Context.Response.Headers["Location"] = url;
      }
   }

   [HttpDelete("{id}")]
   public IActionResult DeleteItem(int id)
   {
      if (_repository.TryDelete(id))
      {
         return new HttpStatusCodeResult(204); // 201 No Content
      }
      else
      {
         return HttpNotFound();
      }
   }
}

Please note that the controller gets an instance of an ITodoRepository implementation. This instance is used all over the controller implementation rather the the hard-coded stuff from above. And so the logic is injected into the controller via the constructor. So far it is a simple example of decoupled responsibilities. We separate the concerns of providing web services and a todo item managing service and plug them together when we need them.

So, how does ASP.NET dependency injection comes in place? This is done at a central point: in the Startup class. There we register an instance of an **ITodoRepository **implementation. This instance will be used by the framework to inject it into the TodoController when the controller is selected and instantiated for a specific request that comes in.

Have a look at the Startup class:

public class Startup
{
   public void Configure(IApplicationBuilder app)
   {
      // Add Mvc to the pipeline.
      app.UseMvc();

      // Add the welcome page to the pipeline.
      app.UseWelcomePage();
   }

   public void ConfigureServices(IServiceCollection services)
   {
      // Add all dependencies needed by Mvc.
      services.AddMvc();

      // Add TodoRepository service to the collection. When an instance of the repository is needed,
      // the framework injects this instance to the objects that needs it (e.g. into the TodoController).
      services.AddSingleton<ITodoRepository, TodoRepository>();
   }
}

The Configure method and the ConfigureServices method are called by the framework when the application is started. The Configure method is used to register components in the application pipeline. As you can see in the code above, ASP.NET MVC and the welcome page are registered.

The more interesting part is the ConfigureServices method. This is the global place where dependencies are registered that can be used through the whole application stack. First, we register the dependencies from ASP.NET MVC 6. This brings all dependencies for MVC and WebAPI functionality. The Mvc package brings in the method AddMvc as an extension method for IServiceCollection.

After Mvc has been added, the AddSingleton method is called on the service collection. With this call we register the ITodoRepository interface and a specific implementation of it in the global DI container. As with the Mvc dependencies above, the TodoRepository can be used through the whole application stack. So think of the TodoController implementation. It takes an instance of an **ITodoRepository **implementation in the constructor. So, when a request comes in and the TodoController is selected for processing the request, the DI framework looks for an appropriate instance of the ITodoRepository implementation in the services collection and if found, it injects this instance into the controller. Pretty cool, or?

So, if the framework does not find the dependency, an appropriate exception is thrown that ends up with 500 Internal Server Error:

https://robinsedlaczek.files.wordpress.com/2014/11/resolve-error.png?w=646&h=597

Error when dependency cannot be resolved.

So, that is the way how DI works with the default container built in ASP.NET vNext. But what about using some other containers, like Autofaq, Ninject, Unity, StructureMap etc.? ASP.NET vNext will support this. You can bring your own container.

Bring your own Container

Now, I like to show how you can use your own DI container in ASP.NET vNext. Please consider that the framework code is currently in the beta status. So the API can change until it is released. I tried some example and tutorials of using own DI container in ASP.NET vNext. But it was a little hard to get them running. But a look into the source code of the framework gave me some hints how it should work.

The code that loads your ASP.NET vNext application into the pipeline can be found in theGitHub repository for the ASP.NET vNext Hosting component, especially in the StartupLoader class. Following this code, there can be different methods in your Startup class that will be called when your application starts. First of all the **Configure **method, where you configure the pipeline and attach those components to it, that you need in your application (e.g. MVC, SignalR):

public void Configure(IApplicationBuilder app)
{
   app.UseMvc();
   app.UseWelcomePage();
}

Second, there can be ConfigureServices method where you can register all those components you need and you want to use via DI. I talked about this method above:

public void ConfigureServices(IServiceCollection services)
{
   // Add all dependencies needed by Mvc.
   services.AddMvc();

   // Add TodoRepository service to the collection. When an instance of the repository is needed,
   // the framework injects this instance to the objects that needs it (e.g. into the TodoController).
   services.AddSingleton<ITodoRepository, TodoRepository>();
}

The source code of the ASP.NET vNext Hosting component reveals that there can be the ConfigureServices method with a slightly different signature. This method returns a reference to an IServiceProvider implementation. I assume, that we can plugin our own DI container here. So please consider the following implementation of this method:

public IServiceProvider ConfigureServices(IServiceCollection services)
{
   var builder = new ContainerBuilder();

   builder.Populate(services);

   IContainer container = builder.Build();

   return container.Resolve<IServiceProvider>();
}

The ConfigureServices method gets a collection of services. The source code of the StartupLoader class reveals that all services that are registered until the ConfigureServices method is called are passed into this method here. So it is possible to set up the own DI container (Autofaq in the example above) and populate all the services here. And so we do in the example above. First the Autofaq container builder is created. Then we populate all existing services into it and then build the container. At the end, a reference to an IServiceProvider implementation of Autofaq is returned. The framework then replaces the ApplicationServices with this reference.

Please remember, that the framework code is in beta status. So a lot can change here until it is released. But you get an idea about how it works.

So, now we see that there is a default built-in DI container in the ASP.NET vNext framework and you can bring your own container and replace the default one, I like to show the subject of the discussion about the solution that is around in the web at the moment.

The Discussion

The discussion came up whether the Service Locator Pattern is the correct way to implement dependency injection into a framework like ASP.NET vNext. Mark Seemann raised this question while giving feedback to the ASP.NET vNext team. He stated, that the team is implementing a “Conforming Container” with some implications. Further, I like to refer to his post on Succeeding with Dependency Injection. I like to list some of the stated points taken from Mark’s posts (excerpts):

  1. It pulls in the direction of the lowest common denominator.

  2. It stifles innovation, because new, creative, but radical ideas may not fit into the narrow view of the world a Conforming Container defines.

  3. It makes it more difficult to avoid using a DI Container.

  4. A Conforming Container is often a product of Speculative Generality, instead of a product of need.

  5. If Adapters are supplied by contributors (often the DI Container maintainers themselves), the Adapters may have varying quality levels, and may not support the latest version of the Conforming Container.

  6. This breaks encapsulation, because it’s impossible to identify a class’ collaborators without reading its entire code base.

  7. Each DI Container has dozens of features – many of them unique to that particular DI Container. A Conforming Container can either support an intersection or union of all those features.

  8. Lack of understanding of Dependency Injection. Dependency Injection is a set of patterns driven by the Dependency Inversion Principle.

  9. Assuming other consumers may need other objects instantiated with different parameters, the Service Locator won’t work.

  10. One of the potential problems is that it becomes very easy to violate theLiskov Substitution Principle.

IMHO

I do not want to let you alone with all this points. I like to tell my humble opinion about several statements. I agree partly with Mark, but not in all points. So the points below are my point of view and could be base for discussions.

Opinion to Point 2

Why not? New ideas can be implemented a snapped into such a container. As you can see in the ConfigureServices method above, the ASP.NET vNext frameworks plugs in other containers when needed. The whole story in ASP.NET vNext is about how to register and retrieving a reference to a service, just to make it simple for developers. So developers have a unique approach in applications and developers who are new to projects can easily understand the code. That is one main aspect when the Microsoft teams design frameworks. But the point cannot be  rejected completely, since radical ideas may imply rejecting the container approach itself.

Opinion to Point 3

Yes, it is hard to avoid using a DI container. With this point, Mark states that the solution used in ASP.NET vNext is a DI container and so DI is done by using the Service Locator Pattern. That makes the statement a little contradictory. At the end, there are costs to pay if you want to have a consistent DI container through the whole framework and if you want to have one approach for DI to make developers life easy.

Opinion to Point 4

Generally, it is correct that a conforming container is a product of speculative generality, but what in case of the ASP.NET vNext framework? There was some feedback from the community to implement consistent DI through the whole framework. And so the conforming container is a product of need here.

Opinion to Point 5

It is correct that there must be adapters for specific DI containers in a conforming container and maybe these adapters are behind the current version of the specific container. But what about the fact, that we are in an open source world? Those open source projects live from contributions made by its consumers. And so, adapters can be changed and adapted to newer versions of the specific containers very easily. And, hopefully, those changes return to the framework by contributions of their developers. Think about LINQ providers. Should we throw LINQ away because of the point stated my Mark?

Opinion to Point 6

It is correct that it is hard to identify collaborators of a class with the Service Locator Pattern without reading the whole code. It is another degree of lose coupling of components. But if implementation details changes, e.g. when using other services within a specific method, the signature of this method do not need to be changed. And so interfaces to a class do not change that often. Consumer codes stays compile-able. Encapsulation is not broken e.g. if documentation exists or guards are used.

Opinion to Point 7

It is partly correct, that a conforming container can either support an intersection or union of all those features provided by all the DI containers. But again, there are some costs we have to pay when we design to make a framework easy to use for developers and if this approach is higher prioritized then maximum flexibility. Yes, the ASP.NET vNext framework is designed to be easy and flexible, but here you can see exactly the problem: it is a balancing act to gain both.

Opinion to Point 8

One can argue what DI means based on several definitions. I like to say something about inversion of control. Let’s remembers what inversion of control means. Inversion of control means, that components are plugged together by some responsibility that is not the consumers itself. That means, that not the component using some other component manages the seconds component lifetime. Second point is, that the consuming components do not know which specific implementation is used, rather consuming components are developed against interfaces. So retrieving instances of specific interface implementations by the service locator pattern inverts the control since the instances are managed by the container that is accessed by the locator. The consumer does not know the specific implementation since the container knows it and lifetime is managed somewhere else, not in the consuming component.

Opinion to Point 9

Assuming other consumers may need other objects instantiated with different parameters, the service locator won’t work. That is correct. But it is the same when injecting dependencies. The consumer cannot instantiated those components with this or this parameters. Instantiating is done somewhere else, and so the stated problem is the same, whether using the service locator or using direct injection of dependencies.

Opinion to Point 10

Is the Liskov Substitution Principle really hurt be the Service Locator Pattern? In my opinion it is not. Let’s remember what the Liskov Substitution Principle states: if you have a type T and a sub type S, then you can use the type S wherever type T is used. In other words: a general type can be replaced with a more specific type. So, since we are developing against interfaces, DI works on interface and the service locator works on interfaces, we can replace the used type by a more specific type. Important is, of course, that the used types implement the required interfaces. Again, the responsibility for creating instances lies somewhere outside the consumer. The consumer does not know which type is instantiated, it just uses the object by the defined interface. So the responsibility that creates the instances can instantiate whatever is needed.

Conclusion

So what do we have at the end? First, there is the Service Locator Pattern used to register and resolve services through the whole APS.NET vNext framework stack. At the other hand, there are some mechanism, that inject dependencies into own classes, as shown above with the TodoController and the TodoRepository classes. This injection is done by constructor injection. Then there is some kind of container selection. You can bring your own DI container. Of course, there are some points that are missing, e.g. property injection. But at the end, all this is open source and can be extended by your needs. So we have a flexible and easy to learn framework.

What do you think?

Now, since you know how DI works in ASP.NET vNext and you know the subject of the current discussion, what do you think about it? Do you like DI in ASP.NET vNext? Is the service locator pattern the correct way to do DI in ASP.NET vNext and is the Service Locator Pattern some kind of DI? Is Mark Seemann right with his point of view and should the ASP.NET team address this?

I would really appreciate if you let me know what you are thinking. Please use the comments for this blog post, write me an email or start discussion on twitter.

Here you can find all the links used for this blog post.

  1. Service Locator Pattern (MSDN)
  2. Service Locator Pattern (Wikipedia)
  3. Dependency Injection in ASP.NET vNext
  4. IServiceProvider (MSDN)
  5. GitHub repository for the ASP.NET vNext Dependency Injection component
  6. GitHub repository for the ASP.NET vNext Hosting component 
  7. Example from the ASP.NET web page
  8. Mark Seemanns Point of View
  9. Mark Seemanns Conforming Container
  10. Mark Seemanns Succeeding with Dependency Injection
  11. Discussion in the ASP.NET vNext Forum
  12. Inversion of Control and Dependency Injection (MSDN)
  13. Inversion of Control (Wikipedia)
  14. Dependency Inversion Principle
  15. Liskov Substitution Principle
  16. LINQ Providers

This article was posted originally at Robin Sedlaczek's Blog.

See Also