Dependency Injection - mcbride-clint/DeveloperCurriculum GitHub Wiki

Dependency Injection is a design pattern to help allow create Inversion of Control within your application classes and dependencies. That is the lower level parts of your application depend on higher level abstractions instead of your higher level classes depending on the lower level instances. This helps to loosely couple your application classes and can help promote testability and code reuse. The key to allowing for this is to properly utilize Interfaces at the edges of application layers.

The Issue

For Example, if you have a UserService class that can save users to the database, the easiest thing to do is to write the call to the database right there is the Save Users function.

public class UserService {
  public void SaveUsers(IEnumerable<User> users {
    // Psuedo Database code
    var dbConnection = new DataBaseConnection(DBHelper.GetConnectionString());
    dbConnection.InsertAll(users);
  }
}

As your application grows, you will likely begin to notice that you may want to reuse that saving logic you you separate it out into a UserRepository class to handle that. Now your UserService has a direct dependency on the UserRepository and in turn the specific database implementation as well. This locks away the capability of an Unit Testing and locks your UserService into your database.

public class UserService {
  public void SaveUsers(IEnumerable<User> users {
    var repo = new UserRepository();
    repo.SaveUsers(users);
  }
}

public class UserRepository {
  public void SaveUsers(IEnumerable<User> users {
    // Psuedo Database code
    var dbConnection = new DataBaseConnection(DBHelper.GetConnectionString());
    dbConnection.InsertAll(users);
  }
}

To invert the dependency, you would create an IUserRepository interface that the UserRepository will implement. Then the UserService uses uses that Interface within it's methods.

public class UserService {
  public void SaveUsers(IEnumerable<User> users {
    IUserRepository repo = new UserRepository();
    repo.SaveUsers(users);
  }
}

public interface IUserRepository {
  void SaveUsers(<IEnumerableUsers> users);
}

public class UserRepository : IUserRepository {
  public void SaveUsers(IEnumerable<User> users {
    // Psuedo Database code
    var dbConnection = new DataBaseConnection(DBHelper.GetConnectionString());
    dbConnection.InsertAll(users);
  }
}

This may not look like it buys you very much in terms of functionality and actually makes your code more complicated for little benefit. This is where Dependency Injection comes in to simplify creating implementations for your interfaces.

Dependency Injection

Historically, .Net Framework did not provide any support for Dependency Injection and relied on 3rd party providers such as Ninject and Unity. these providers often had to create their own custom solution to hook the capabilities into WebForms, MVC, etc. When refactoring for .Net Core, Microsoft added in Interfaces for 3rd parties to create their own frameworks that will hook in seamlessly to applications. Microsoft also created a basic implementation that will suffice in most circumstances.

Service Collection

Microsoft's implementation of Dependency Injection is accomplished through what they call Services. When setting up your application container, you can register Concrete Implementation Types that will be provided in place of Service Types. These types are registered in an instance of an IServiceCollection which is used to build an IServiceProvider that will be used by the framework to inject needed dependencies.

Abbreviated Application Start Up:

using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace DependencyInjection.Example
{
    class Program
    {
        static Task Main(string[] args) =>
            CreateHostBuilder(args).Build().RunAsync();

        static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureServices((_, services) =>
                    services.AddHostedService<Worker>()
                            .AddScoped<IMessageWriter, MessageWriter>()); // Registers MessageWriter should be provided for needed Instances of IMessageWriter
    }
}

The previous example is using a lot of Lambda Expressions to abbreviate the code. Here is a longer version that you will be more likely to see.

Program Main method to start:

public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>(); // Calls the special Startup class to handle Application and Services Setup
            });
}

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        // Register Classes to be injected when needed
        services.AddScoped<IMessageWriter, MessageWriter>();
        services.AddRazorPages();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler("/Error");
            app.UseHsts();
        }

        app.UseHttpsRedirection();
        app.UseStaticFiles();

        app.UseRouting();

        app.UseAuthorization();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapRazorPages();
        });
    }
}

Service LifeTimes

When registering a services you must specify a lifetime for how long the instantiated object will persist. The options are:

  • Singleton
  • Scoped
  • Transient

Singleton

Objects that are registered as a Singleton are instantiated when the ServiceCollection is built and retained until the application is shutdown. There will ever only be a single instance of the class. Every time a singleton is injected they will all be given the same reference.

services.AddSingleton<Interface, Class>();

Scoped

Objects that are registered as a Scoped are instantiated the first time they are needed within a Scope and persist until the scope is complete. Generally a Scope is a single HttpRequest, so if the same service is requested twice within a request they will be given the same reference. Two simultaneous requests will have independent instances of the same object.

services.AddScoped<Interface, Class>();

Transient

Objects that are registered as a Transient are instantiated every time they are requested.

services.AddTransient<Interface, Class>();

Injection Points

Initial difficulties of Dependency Injection for a developer is getting used to when the Injection can happen. It cannot happen at just any point, such as new IMessageWriter();. The injection occurs at different points in the process depending on the Type of Project, but generally it occurs when the Framework enters into project code during a request.

  • MVC -> Constructor Injection within Controller Class
  • Razor Pages -> Constructor Injection within PageModel Class
  • Blazor Components -> Property Injection when marked with [Inject] Attribute or @inject command within Razor Syntax
  • WebForms -> Constructor Injection within Page Class

Dependency Injection works from the top down so when it creates the dependencies for the items in the Page or Controller, it must also create all the dependencies for those items and the dependencies for those dependencies and so on. If all the needed dependencies are not in the ServicesCollection, then a runtime exception will be thrown.

Example

Psuedo UserService Code from above. All dependent object instantiations have been moved to the Constructors so they can be fulfilled by the Dependency Injection container.

public class UserService {
  private IUserRepository _userRepo;
  public UserService (IUserRepository repo){ // Inject Instance of UserRepository that implements IUserRepository
    _userRepo = repo; // Store the instance in a private variable for later use
  }
  public void SaveUsers(IEnumerable<User> users {
    _userRepo.SaveUsers(users);
  }
}

public interface IUserRepository {
  void SaveUsers(<IEnumerableUsers> users);
}

public class UserRepository : IUserRepository {
  private IDataBaseConnection _dbConn;
  public UserRepository(IDataBaseConnection dbConn) { // The dependency on a DatabaseConnection can also be extracted out
    _dbConn = dbConn;
  }
  public void SaveUsers(IEnumerable<User> users {
    // Psuedo Database code
    _dbConn.InsertAll(users);
  }
}

Example of the Startup that will support these classes.

public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>(); // Calls the special Startup class to handle Application and Services Setup
            });
}

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        // Register Classes to be injected when needed
        services.AddScoped<IUserService, UserService>();
        services.AddScoped<IUserRepository, UserRepository>();
        services.AddTransient<IDataBaseConnection>(_ => new DataBaseConnection(DBHelper.GetConnectionString()));

        services.AddRazorPages();
    }

 ...
}

See Also

⚠️ **GitHub.com Fallback** ⚠️