Dependency injection (DI) is a technology to realize loose coupling between objects and their partners or dependencies. Instead of instantiating the author directly or using static references, provide the object used to perform the action to the class in some way. Typically, classes declare their dependencies through their constructors, allowing them to follow the explicit Dependencies Principle, a method known as "constructor injection.".

Using the idea of DI in the design of class will make their dependencies more coupling, because they have no direct hard coding dependence on their partners. They follow the Dependency Inversion Principle "high-level modules should not rely on low-level modules, and both should rely on abstraction " Class is required to provide abstractions (usually interfaces) to them when they are constructed, rather than directly referring to specific implementations. Extracting the dependencies of interfaces and providing their implementation as parameters is also an example of Strategy design pattern.

Principle of dependency injection

//Contract an abstract interface
public interface IRepository
{
    string GetInfo();
}

//Using EF to implement the interface
public class EFRepository : IRepository
{
    public string GetInfo()
    {
        return "load data form ef!";
    }
}

//Using dapper to implement the interface
public class DapperRepository : IRepository
{
    public string GetInfo()
    {
        return "load data form dapper!";
    }
}

//Provides an abstract interface in the constructor of the service used to perform the operation
public class OperationService
{
    private readonly IRepository _repository;
    public OperationService(IRepository repository)
    {
        _repository = repository;
    }

    public string GetList()
    {
        return _repository.GetInfo();
    }
}   

From the above code, it can be seen that there is no strong dependency between service and repository, but both of them are related to the abstract interface irepository.

OperationService service = new OperationService(new EFRepository());

// Or

OperationService service = new OperationService(new DapperRepository());

service.GetInfo();

In the calling process, the caller decides which implementation method to use, which implements the dependency inversion principle.

When the system is designed to use Di, there will be many classes that request their dependencies through their constructors (or properties), which is that we need a common function to help us create these classes and their related dependencies. This common function is called containers, or more specifically, inversion of control (IOC) containers or dependency injection (DI) containers. A container is essentially a factory that is responsible for providing instances of the types requested from it. If a given type declares that it has a dependency and the container has been configured to provide a dependency type, it will create the dependency as part of creating the request instance. In this way, complex dependencies can be provided to types without any hard coded type construction. In addition to creating dependencies on objects, containers often manage the life cycle of objects in an application.

Dependency injection container in asp.net core

Asp.net core has taken Di as its infrastructure, and provides a simple built-in container (represented by iserviceprovider interface) that supports constructor injection by default, so that some services of asp.net core can be obtained through di. Asp.net container refers to the type managed by it is services, and services refers to the type managed by IOC container of asp.net core. You can configure the services of the built-in container in the configureservices method of the application startup class.

public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient<IRepository, EFRepository>();
    services.AddTransient<IRepository, DapperRepository>();

    services.AddTransient<OperationService, OperationService>();
}

The constructor injection behavior in asp.net core requires that the injected constructor is public, and the injected constructor supports overloading.

In the above example, for the same interface, registering different implementation classes will be overwritten according to the registration order, and the final effective implementation is the last registered type.

Define a middleware to test the call of dependency injection:

public class RepositoryMiddleware
{
    private readonly RequestDelegate _next;

    public RepositoryMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext context,
        OperationService operationService)
    {
        await context.Response.WriteAsync(operationService.GetList());
    }
}

public static class RepositoryMiddlewareExtensions
{
    public static IApplicationBuilder UseRepositoryMiddleware(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<RepositoryMiddleware>();
    }
}
// Startup.cs
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseRepositoryMiddleware();
}

Service Lifecycle and enrollment options

Asp.net core services can be configured for the following lifecycles:

Transient

Transient: services are created each time they are requested.

Scoped

Scope: the service is created only once per request.

Singleton

Singleton: life cycle services are created when they are first requested (or if you specify an instance when you run ConfigureServices ), then each subsequent request will use the first instance created.

public interface IOperation
{
    Guid OperationId { get; }
}

public interface IOperationTransient : IOperation
{
}
public interface IOperationScoped : IOperation
{
}
public interface IOperationSingleton : IOperation
{
}
public interface IOperationSingletonInstance : IOperation
{
}

public class Operation : IOperationTransient,
    IOperationScoped,
    IOperationSingleton,
    IOperationSingletonInstance
{
    Guid _OperationId;

    public Guid OperationId => _OperationId;

    public Operation()
    {
        _OperationId = Guid.NewGuid();
    }

    public Operation(Guid operationId)
    {
        _OperationId = operationId;
    }
}

public class OperationService2
{
    public IOperationTransient TransientOperation { get; }
    public IOperationScoped ScopedOperation { get; }
    public IOperationSingleton SingletonOperation { get; }
    public IOperationSingletonInstance SingletonInstanceOperation { get; }

    public OperationService2(IOperationTransient transientOperation,
        IOperationScoped scopedOperation,
        IOperationSingleton singletonOperation,
        IOperationSingletonInstance instanceOperation)
    {
        TransientOperation = transientOperation;
        ScopedOperation = scopedOperation;
        SingletonOperation = singletonOperation;
        SingletonInstanceOperation = instanceOperation;
    }
}

The above example uses the Operation class to implement these interfaces. Its constructor takes a Guidand generates a new Guid if it is not provided.

Next, in configureservices, each type is added to the container based on its named lifecycle:

services.AddTransient<IOperationTransient, Operation>();
services.AddScoped<IOperationScoped, Operation>();
services.AddSingleton<IOperationSingleton, Operation>();
services.AddSingleton<IOperationSingletonInstance>(new Operation(Guid.Empty));
services.AddTransient<OperationService2, OperationService2>();

Define a middleware again to test the call of dependency injection:

public class ServiceLifetimesMiddleware
{
    private readonly RequestDelegate _next;

    public ServiceLifetimesMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext context,
        OperationService2 operationService2,
        IOperationTransient transientOperation,
        IOperationScoped scopedOperation,
        IOperationSingleton singletonOperation,
        IOperationSingletonInstance singletonInstanceOperation)
    {
        await context.Response.WriteAsync($"Operations:\r\n");
        await context.Response.WriteAsync($"Transient:{transientOperation.OperationId}\r\n");
        await context.Response.WriteAsync($"Scoped:{scopedOperation.OperationId}\r\n");
        await context.Response.WriteAsync($"Singleton:{singletonOperation.OperationId}\r\n");
        await context.Response.WriteAsync($"SingletonInstance:{singletonInstanceOperation.OperationId}\r\n");
        await context.Response.WriteAsync("\r\n");
        await context.Response.WriteAsync($"Serivce Operations:\r\n");
        await context.Response.WriteAsync($"Transient:{operationService2.TransientOperation.OperationId}\r\n");
        await context.Response.WriteAsync($"Scoped:{operationService2.ScopedOperation.OperationId}\r\n");
        await context.Response.WriteAsync($"Singleton:{operationService2.SingletonOperation.OperationId}\r\n");
        await context.Response.WriteAsync($"SingletonInstance:{operationService2.SingletonInstanceOperation.OperationId}\r\n");
    }
}

public static class ServiceLifetimesMiddlewareExtensions
{
    public static IApplicationBuilder UseServiceLifetimesMiddleware(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<ServiceLifetimesMiddleware>();
    }
}
// Startup.cs
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseServiceLifetimesMiddleware();
}

Test the effect by two separate requests:

Operations:
Transient:bc337e93-0813-4d79-85b0-084f0d91a0cf
Scoped:0ff39177-42aa-4646-a1d4-5238a8bfb5b7
Singleton:72b37cae-edc3-411a-9e94-97ec29fdcd6a
SingletonInstance:00000000-0000-0000-0000-000000000000

Serivce Operations:
Transient:4efe931a-8004-4900-851f-8253f81d6ae3
Scoped:0ff39177-42aa-4646-a1d4-5238a8bfb5b7
Singleton:72b37cae-edc3-411a-9e94-97ec29fdcd6a
SingletonInstance:00000000-0000-0000-0000-000000000000
Operations:
Transient:57ac7f0a-e23e-460a-9435-55501a88f3b6
Scoped:e73cffd6-ff54-44df-a1e2-e4753d3a7ea5
Singleton:72b37cae-edc3-411a-9e94-97ec29fdcd6a
SingletonInstance:00000000-0000-0000-0000-000000000000

Serivce Operations:
Transient:748017c1-9a18-465b-b23c-75abdc910fd1
Scoped:e73cffd6-ff54-44df-a1e2-e4753d3a7ea5
Singleton:72b37cae-edc3-411a-9e94-97ec29fdcd6a
SingletonInstance:00000000-0000-0000-0000-000000000000

Through the change of operationid value in the two requests, we know that:

  • Transient objects are always different, and the framework provides a new instance for every controller and every service
  • Scope objects are the same in one request, but different in different requests
  • Singleton when an object is contained in any object and is the same in every request (whether or not an instance is provided in configureservices)

Request Services

Available services in an ASP. The net request httpcontext is exposed in the requestservices collection.

The request service represents the service configuration and request as part of the application. When your object specifies a dependency relationship, these satisfying objects are obtained by looking up the corresponding types in RequestServices, instead of ApplicationServices.

In general, we should not use these properties directly, but prefer to request the required service type through the constructor of the class, and use the framework to inject the dependency. This will result in easier to test (see testing) and more loosely coupled classes.

Design idea of dependency injection service

In applications, we design dependency injection services to get their partners. This means avoiding stateful static methods and directly instantiating dependent types during coding. Faced with the choice of instantiating a type or requesting it through dependency injection, New is Glue can help you. By following the solid principle of object-oriented design, your classes will tend to be small, easy to decompose, and easy to test.

What if we find that too many dependencies are injected into a designed class? This usually indicates that this class tries to do too many things and may violate the single responsibility principle (SRP). Can we refactor a class by transferring some responsibilities to a new class. Keep in mind that the controller class should focus on the user interface (UI), so business rules and data access implementation details should be stored in these classes that are suitable for individual attention.

For data access, if EF has been configured in the startup class, we can easily inject the dbcontext type of Entity Framework into the controller. However, it is best not to rely directly on dbcontext in UI projects. Instead, rely on an abstraction (such as a warehouse interface) and limit the use of EF (or any other data access technology) to implement the interface. This reduces the coupling between the application and specific data access policies, and makes the application code easier to test.

All Comments

Leave a Reply Cancel Reply

Tips: Your email address will not be disclosed!

If you can't see clearly,please click to change...

Popular Posts

Collections