1

I have a solution that has the following projects

  • Acme.Core
  • Acme.Domain
  • Acme.Repositories
  • Acme.Services
  • Acme.Web

In the past I've used Unity for DI in full framework projects. I was able to register concrete objects to interface mappings in executable projects (web apps, console app, test apps).

I'm trying to implement the same approach with .NET Core. I wanted to first try using the Microsoft.Extensions.DependencyInjection library. Within the ASP.NET Core application it works great. Unfortunately I've run into an issue when I try to share/reference that instance with the registions to other projects, such as a .NET Standard library.

My idea was to inject the ServiceProvider into the constructor of the service:

public class AddressService : BaseService, IAddressService
{
        private readonly IServiceProvider _serviceProvider;

        public AddressService(IServiceProvider serviceProvider, string userOrProcessName)
        {
           _serviceProvider = serviceProvider;
        } 

        public IReadOnlyList<IState> GetAllStates()
        {
            _serviceProvider.GetService<IAddressRepository>();

            // other logic removed
        }
}

I tried the following inside the Startup.ConfigureServices():

services.AddTransient<IAddressService>(s => new AddressService(HttpContext.RequestServices, Environment.UserName));

The issue I ran into is that I cannot reference HttpContext.RequestServices outside of a Controller. I haven't been able to figure another way of passing the ServiceProvider instance.

My questions:

  1. How do pass a reference for the current ServiceProvider?
  2. Is there a better design to accomplish my goal sharing the configuration of Microsoft.Extensions.DependencyInjection in multiple libraries?
Josh
  • 8,219
  • 13
  • 76
  • 123
  • What is the problem you are trying to solve? Do you really need to use the same instance across all those projects? Or do you want to share the DI configuration across the projects? – Colin Young Jun 28 '18 at 16:26
  • you don't use dependancy injection in multiple libs, stright forward. each project has its own dependancy injection. – omriman12 Jun 28 '18 at 16:27
  • @ColinYoung, that is better stated, I want to use the same configuration across different projects – Josh Jun 28 '18 at 16:30
  • @omriman12, I don't want to setup DI configuration per library. The consuming app should configure the mappings. – Josh Jun 28 '18 at 16:32
  • not per library! per project.. that's what everyone does. that's the way to go – omriman12 Jun 28 '18 at 16:33
  • You should use `IServiceProvider` anywhere but _inside_ the [Composition Root](https://freecontent.manning.com/dependency-injection-in-net-2nd-edition-understanding-the-composition-root/). – Steven Jun 28 '18 at 16:36
  • @omriman12, a project produces an exe (application) or a dll (library). What are you referring to when you say "not per library! per project?" – Josh Jun 28 '18 at 16:36
  • @Steven, what is the better approach for the application to pass it's object graphs with other libraries using Microsoft.Extensions.DependencyInjection? – Josh Jun 28 '18 at 16:40
  • a project is a: `console app`, `windows service`, `website`.. etc.. – omriman12 Jun 28 '18 at 16:40
  • @omriman12, open Visual Studio and create Solution. Then go to the Add New Project Menu. Click on Visual C# and it will list templates for many project types that includes the .NET Standard Class Library I'm referring to, this project produces a dll. – Josh Jun 28 '18 at 16:47

2 Answers2

6

Prevent injecting IServiceProvider into your application components; that leads to the Service Locator anti-pattern.

Instead, you should build up application components solely using Constructor Injection. This means that your AddressService should require IAddressRepository as constructor argument, not IServiceProvider. For instance:

public class AddressService : IAddressService
{
    private readonly IAddressRepository repo;

    public AddressService(IAddressRepository repo, IUserContext userContext)
    {
       this.repo = repo;
    } 

    public IReadOnlyList<IState> GetAllStates()
    {
        // other logic removed
    }
}

Also try to prevent injecting primites into your constructors. It's not a bad practice per se, but it does complicate object graph construction. Instead, either wrap the value into a class, in case its a configuration value, or hide it behind an abstraction (as shown above) in case it's a runtime value.

Both practices simplify both your application code and the Composition Root.

For instance, this will be the result of the previous AddressService redesign:

services.AddTransient<IAddressRepository, SqlAddressRepository>();
services.AddTransient<IAddressService, AddressService>();
services.AddScoped<IUserContext, UserContext>();
services.AddHttpContextAccessor();

Here, UserContext could be defined as follows:

public class UserContext : IUserContext
{
    private readonly IHttpContextAccessor accessor;
    public UserContext(IHttpContextAccessor accessor) => this.accessor = accessor;
    public string UserName => this.accessor.HttpContext.User.Identity.Name;
}
Josh
  • 8,219
  • 13
  • 76
  • 123
Steven
  • 166,672
  • 24
  • 332
  • 435
  • This makes sense. My issue with Constructor Injection is I've found that number of parameters tend grow and the classes start looking really ugly. The other issue is if you have multiple constructors, you'll have to duplicate the same list of parameters for each constructor. Nice tip setup using classes opposed to passing primitive types. One question, I was going to use Environment.UserName opposed to User.Identity.Name. Is Environment.UserName not as reliable? – Josh Jun 28 '18 at 17:08
  • 1
    Constructor injection is great, especially because things become ugly with many parameters. This is a code smell called Constructor Over-Injection and it’s an indication that your class gets too complex; it violates the Single Responsibility Principle. This means it’s time to start refactoring your code and making your classes smaller. – Steven Jun 28 '18 at 17:12
  • 1
    Having multiple public constructors on application components (classes that contain behavior) is also a code smell. It means that the class contains optional dependencies, but [dependencies should not be optional](https://www.cuttingedge.it/blogs/steven/pivot/entry.php?id=97). – Steven Jun 28 '18 at 17:14
  • About `Environment.UserName`. This returns the name of the *local user* in which context the web application is running on the machine. This is most likely something very different from the user that accesses the website. – Steven Jun 28 '18 at 17:15
  • thank you the detailed responses. I'll give Constructor Injection another shot, since I'm switching the DI framework anyways whats one more change! I like the Unity approach of an XML config and calling the provider to obtain. I recognize that approach did fall under the Service Locator anti-pattern, and saw the run time issues described. – Josh Jun 28 '18 at 18:25
  • If you get constructor argument proliferation, it probably means your class is doing too much, violating the single responsibility principal, or you're taking dependencies of your dependencies (e.g. taking one service just to pass it to another) rather than only your direct dependencies. Try splitting the class in a logical place if this happens. You should then end up with classes that are simpler and easier to test. – George Helyar Jun 28 '18 at 19:39
1

In order to share configuration across multiple projects, you can put the configuration into a shared assembly, and register (not resolve) them in there. Many dependency injection libraries offer that functionality. e.g.

At a high level, you create a method that receives the DI container and adds your registrations to that container. If your DI framework doesn't provide hooks you need to manually call the method yourself, but the general concept doesn't change.

Splitting registrations into modules allows you to easily group similar sets of functionality while maintaining the flexibility of incorporating different sets of functionality into different projects. You could of course create a single shared assembly that registered the union of all dependencies for all projects, but that would carry around unnecessary baggage and result in a less reusable implementation.

The key point as Steven points out is that you configure the container and let it inject the dependencies rather than looking from the inside out for the dependencies.

Colin Young
  • 3,018
  • 1
  • 22
  • 46