How to use inversion of control in C#

 The direction of dependency within the application should be in the direction of abstraction, not implementation details.

The direction of dependency within the application should be in the direction of abstraction, not implementation details.

Both inversion of control and dependency injection enable you to break dependencies between the components in your application and make your application easier to teste and maintain. However, inversion of control and dependency injection are not the same — there are subtle differences between the two.

Inversion of control (IoC) is a design pattern in which the control flow of a program is inverted. You can take advantage of the inversion of control pattern to decouple the components of your application, swap dependency implementations, mock dependencies, and make your application modular and testable.

Dependency injection is a subset of the inversion of control principle. In other words, dependency injection is just one way of implementing inversion of control. You can also implement inversion of control using events, delegates, template pattern, factory method, or service locator.

The inversion of control design pattern states that objects should not create objects on which they depend to perform some activity. Instead, they should get those objects from an outside service or a container.

Multiple constructor discovery rules 

When a type defines more than one constructor, the service provider has logic for determining which constructor to use. The constructor with the most parameters where the types are DI-resolvable is selected.

public class ExampleService
{
    public ExampleService()
    {
    }

    public ExampleService(ILogger<ExampleService> logger)
    {
        // omitted for brevity
    }

    public ExampleService(FooService fooService, BarService barService)
    {
        // omitted for brevity
    }
}

In the preceding code, assume that logging has been added and is resolvable from the service provider but the FooService and BarService types are not. The constructor with the ILogger<ExampleService> parameter is used to resolve the ExampleService instance. Even though there's a constructor that defines more parameters, the FooService and BarService types are not DI-resolvable.

If there's ambiguity when discovering constructors, an exception is thrown. Consider the following C# example service:

public class ExampleService
{
    public ExampleService()
    {
    }

    public ExampleService(ILogger<ExampleService> logger)
    {
        // omitted for brevity
    }

    public ExampleService(IOptions<ExampleOptions> options)
    {
        // omitted for brevity
    }
}
In the preceding example, there are three constructors. The first constructor is parameterless and requires no services from the service provider. Assume that both logging and options have been added to the DI container and are DI-resolvable services. When the DI container attempts to resolve the ExampleService type, it will throw an exception, as the two constructors are ambiguous.

Service lifetimes

Services can be registered with one of the following lifetimes:

  • Transient
  • Scoped
  • Singleton

The following sections describe each of the preceding lifetimes. Choose an appropriate lifetime for each registered service.

Transient

Transient lifetime services are created each time they're requested from the service container. This lifetime works best for lightweight, stateless services. Register transient services with AddTransient.

In apps that process requests, transient services are disposed at the end of the request.

Scoped

For web applications, a scoped lifetime indicates that services are created once per client request (connection). Register scoped services with AddScoped.

In apps that process requests, scoped services are disposed at the end of the request.

When using Entity Framework Core, the AddDbContext extension method registers DbContext types with a scoped lifetime by default.


By default, in the development environment, resolving a service from another service with a longer lifetime throws an exception.


Singleton

Singleton lifetime services are created either:

  • The first time they're requested.
  • By the developer, when providing an implementation instance directly to the container. This approach is rarely needed.

Every subsequent request of the service implementation from the dependency injection container uses the same instance. If the app requires singleton behavior, allow the service container to manage the service's lifetime. Don't implement the singleton design pattern and provide code to dispose of the singleton. Services should never be disposed by code that resolved the service from the container. If a type or factory is registered as a singleton, the container disposes the singleton automatically.

Register singleton services with AddSingleton. Singleton services must be thread safe and are often used in stateless services.

In apps that process requests, singleton services are disposed when the ServiceProvider is disposed on application shutdown. Because memory is not released until the app is shut down, consider memory use with a singleton service.


Scope validation

When the app runs in the Development environment and calls CreateDefaultBuilder to build the host, the default service provider performs checks to verify that:

  • Scoped services aren't resolved from the root service provider.
  • Scoped services aren't injected into singletons.

The root service provider is created when BuildServiceProvider is called. The root service provider's lifetime corresponds to the app's lifetime when the provider starts with the app and is disposed when the app shuts down.

Scoped services are disposed by the container that created them. If a scoped service is created in the root container, the service's lifetime is effectively promoted to singleton because it's only disposed by the root container when the app shuts down. Validating service scopes catches these situations when BuildServiceProvider is called.

Scope scenarios

The IServiceScopeFactory is always registered as a singleton, but the IServiceProvider can vary based on the lifetime of the containing class. For example, if you resolve services from a scope, and any of those services take an IServiceProvider, it'll be a scoped instance.

To achieve scoping services within implementations of IHostedService, such as the BackgroundService, do not inject the service dependencies via constructor injection. Instead, inject IServiceScopeFactory, create a scope, then resolve dependencies from the scope to use the appropriate service lifetime.


In the preceding code, while the app is running, the background service:

  • Depends on the IServiceScopeFactory.
  • Creates an IServiceScope for resolving additional services.
  • Resolves scoped services for consumption.
  • Works on processing objects and then relaying them, and finally marks them as processed.

From the sample source code, you can see how implementations of IHostedService can benefit from scoped service lifetimes.


Vikash Chauhan

C# & .NET experienced Software Engineer with a demonstrated history of working in the computer software industry.

Post a Comment

Previous Post Next Post

Contact Form