Asp.Net Core Dependency Injection and Service Lifetimes

 In Development

Introduction

In this article, we will talk about Asp.Net Core dependency injection and service lifetimes. I’ll also give some tips and recommendations on how to use them in production, and how they work. It contains sample code that demonstrates how the service container manages and tracks services with different service lifetimes. If you don’t know what dependency injection is, I recommend you look at this article to get acquainted.

Asp.Net Core supports the dependency injection pattern out of the box. When you create a new Asp.Net Core web application from a template, it automatically generates a Startup class, that you use to register your services in the ConfigureServices method.

Asp.Net Core uses the Microsoft.Extensions.DependencyInjection library as the default dependency injection framework. This framework has many features that are sufficient for most applications. If you need more features, you can directly use or integrate other 3rd party dependency injection libraries. Unless you need extra features like custom service lifetimes, child containers I recommend sticking with the default .Net Core dependency injection library which is lightweight, performant and well maintained.

Why dependency injection?

  • You can use an interface or an abstract base class for a service.
    • This helps make your code testable.
    • It also provides some flexibility as you can also register different implementations of the service for different environments or application settings. As an example, let’s say your backend service stores and and serves user uploaded documents. If you abstract your storage service like IDocumentStorageService, you can can use FileDocumentStorageService for your local development, MemoryDocumentStorageService for unit tests, AzureDocumentStorageService to use Azure Storage Services. You can also write other implementations of the service for use with other cloud service providers.
  • The framework automatically injects dependent services into the service’s constructor. Also, the framework is responsible for creating the services and their dependencies and tracking disposable services. This will help you write less code to clean up dependent resources and avoid memory leaks.

What is a Service Scope?

You can think of the service scope as a short lived child container. All disposable scoped and transient services, which are resolved within the service scope, will be disposed when the service scope is disposed. In Asp.Net, each request creates its own service scope, so when request ends, all the resources generated within the request will be disposed. This provides isolation and helps prevent memory leaks. For isolation, each request can instantiate services which can access only tenant or user scoped data.

What are service lifetimes?

You can use three lifetimes with the default dependency injection framework. These lifetimes affect how the service is resolved and disposed of by the service provider.

  1. Transient: A new service instance is created each time a service is requested from the service provider. If the service is disposable, the service scope will monitor all instances of the service and destroy every instance of the service created in that scope when the service scope is disposed.
  2. Singleton: Only one instance of the service is created if it’s not already registered as an instance. Single instance services are tracked by the root scope if they are created by the framework. This means that single instance services will not be disposed until the root scope is disposed which is usually occur when the application exits. Please note that if your singleton service is disposable and you didn’t register it with its implemented type or service provider factory and registered it as an instance, framework does not track and dispose it. In this case you should manually dispose it after the service container is disposed.
  3. Scoped: A new instance of a service is created in each scope. It will act as if it is singleton within that scope. If the service is disposable it will be disposed when service scope is disposed.

Sample Code You can find the sample repository here.

public static void Run()
{
var serviceBuilder = new ServiceCollection();
serviceBuilder
.AddSingleton<SingletonDisposableService>()
.AddTransient<TransientDisposableService>()
.AddScoped<ScopedDisposableService>()
;
// You should use validateScopes: true as a best practice. It's set to false here for demo purposes.
using var services = serviceBuilder.BuildServiceProvider(validateScopes: false);
var logger = new TestLogger();
// Tests in Scope
logger.Header("Creating a service scope...");
using (var scope = services.CreateScope())
{
// Resolve scoped service twice and compare reference
// Resolve scoped service first time
logger.Header("Resolving first scoped service in service scope...");
var scopedService1 = scope.ServiceProvider.GetService<ScopedDisposableService>();
scopedService1.SayHello();
logger.Separator();
// Resolve scoped service second time
logger.Header("Resolving second scoped service in service scope...");
var scopedService2 = scope.ServiceProvider.GetService<ScopedDisposableService>();
scopedService2.SayHello();
logger.Separator();
// Compare references of two scope services and print the result
var scopedServicesAreSame = Object.ReferenceEquals(scopedService1, scopedService2);
logger.Log($"{nameof(scopedService1)} { (scopedServicesAreSame ? "==" : "!=") } {nameof(scopedService2)}");
// Resolve transient service
logger.Header("Resolving transient service in service scope...");
var transientInScope = scope.ServiceProvider.GetService<TransientDisposableService>();
transientInScope.SayHello();
logger.Separator();
// Resolve sigleton service
logger.Header("Resolving singleton service in service scope...");
var singletonInScope = scope.ServiceProvider.GetService<SingletonDisposableService>();
singletonInScope.SayHello();
logger.Separator();
}
logger.Log("Service scope disposed.", spaceAfter: true);
// Resolve tests in root scope
logger.Header("Testing in root scope");
// Singleton service
logger.Log("Resolving singleton service in ROOT scope...");
var singletonService = services.GetService<SingletonDisposableService>();
singletonService.SayHello();
logger.Separator();
// Transient service
logger.Log("Resolving transient service in ROOT scope...");
var transientService = services.GetService<TransientDisposableService>();
transientService.SayHello();
logger.Separator();
// Scoped service
logger.Log("Resolving scope service in ROOT scope...");
// If validateScopes parameter is true, [ ex: serviceBuilder.BuildServiceProvider(validateScopes: true) ]
// This line will throw an exception, because scope services will not be allowed in the root scope.
// In this test we used validateScopes: false for demo purposes.
var scopedService = services.GetService<ScopedDisposableService>();
scopedService.SayHello();
logger.Separator();
logger.Header("Disposing services...");
// Although we used "using..." keyword as a best practice while building services, we explicitly dispose it for demo purposes
services.Dispose();
logger.Log("Services disposed.");
logger.Separator();
}
public class ScopedDisposableService : DisposableServiceBase
{
public ScopedDisposableService(TransientDisposableService transientDisposableService, SingletonDisposableService singletonDisposableService)
{
TransientDisposableService = transientDisposableService;
SingletonDisposableService = singletonDisposableService;
}
public TransientDisposableService TransientDisposableService { get; }
public SingletonDisposableService SingletonDisposableService { get; }
}
public class SingletonDisposableService : DisposableServiceBase { }
public class TransientDisposableService : DisposableServiceBase { }

Output from sample code above.

Service lifetimes sample code output

Disposable Services

A service is considered as disposable if it implements IDisposable and/or IAsyncDisposable interface.

If a singleton service is disposable but registered as an instance, the service will not be tracked by the service container. Usually the root service container is disposed when the application exits. If that’s not the case, you should be aware that if you don’t dispose the service after the service container is disposed, there will be a memory leak. See the code below.

// Container tracks instance if service is registered by implemented type.
services.AddSingleton<IMyDisposableSingletonService, MyDisposableSingletonService>();
// Container tracks instance if service is registered by factory function.
services.AddSingleton<IMyDisposableSingletonService>(sp => new MyDisposableSingletonService());
// Container does NOT track if service is registered by instance
services.AddSingleton<IMyDisposableSingletonService>(new MyDisposableSingletonService());

If your transient service is a disposable service, you should resolve it from a scope other than the root scope. Since root scope is usually disposed when the application exits, if you resolve a transient service from root scope, each instance of the transient service will be alive for the lifetime of the application. This will lead to memory leaks. The recommended way of creating disposable transient service is to create it with a factory service. If your transient service depends on other services, you can pass the current service provider to your service factory method. This way service container will not track the disposable transient service and you will be responsible for disposing of it when you no longer need it.

Example code;

public interface IMyDisposableTransientServiceFactory
{
IMyDisposableTransientService CreateNew(IServiceProvider services);
}
// Usage
class MyService
{
IServiceProvider _services;
IMyDisposableTransientServiceFactory _transientServiceFactory;
public MyService(IServiceProvider services, IMyDisposableTransientServiceFactory transientServiceFactory)
{
// Code omitted
}
public void DoSomeStuff(IEnumerable<TaskNotification> tasks)
{
foreach (var task in tasks) {
using var transientService = _transientServiceFactory.CreateNew(_services);
// ...
}
// or create a new scope for the batch operation for dependent services used by IMyDisposableTransientService
using var scope = _services.GetRequiredService<IServiceScopeFactory>().CreateScope();
foreach (var task in tasks) {
using var transientService = _transientServiceFactory.CreateNew(scope.ServiceProvider);
// ...
}
}
}

How do Service Scopes track disposable services?

Requests with Service Scopes

In Asp.Net Core a new service scope will be created at each request and will be disposed when request ended with a response or an exception.

Service tracking by service scope and service lifetime

When…

  • A scoped service is requested;
    • Service scope will create an instance of the service that has not been already created in the service scope.
    • Service scope will always track scoped services.
  • A transient service is requested;
    • Service scope will always create an instance of the service within the service scope.
    • Service scope will track only disposable transient services.
  • A singleton service is requested;
    • If the service does not have any instances, the root scope creates an instance of the service.
    • Root scope will always track the singleton service.

How does a service container create services?

All service instances are created on demand, so even if you register a large number of single-instance services, they are instantiated as needed, unless you register them as an instance.

If you register your service with service implementation type;

  • Your service will be created by reflection at first request.
  • If it’s not a singleton, it will also be created by reflection, but the service engine will compile a service creation factory in the background after the second attempt.
  • After the service factory compilation, it will be created using this factory and after that point it will be very fast to create the service. Please note that the service may be created more than 2 times with reflection if it is created at the same time, as the build happens in the background.
  • This method is actually used by many frameworks, because compilation usually takes more time than creating with reflection using Activator.CreateInstance. It will not compile as singletons are not created more than once. If a scoped or transient service is not created more than once, this method will avoid unnecessary compilation. Also ahead of compilation would cause longer startups.
  • Most of the time this method will work, but if this warm-up time affects your application performance, you may want to consider using a service factory, which will be faster as there will be no reflection or compilation.
// If you need faster application warmups
// instead of registering startup services by type
// services.AddTransient<MyService>();
// Register them by service factory
services.AddTransient<MyService>(sp => new MyService(sp.GetRequiredService<IMyDependencyService>()));

Choosing service lifetime for your services

By context or functionality

  • Use singleton;
    • If your service has a shared state such as cache service. Singleton services with mutable state should consider using a locking mechanism for thread safety.
    • If your service is stateless. If your service implementation is very lightweight and infrequently used, you might also consider registering it as transient.
  • Use scoped;
    • If your service should act as a singleton within the scope of the request. In Asp.Net Core each request has in own service scope. Database and repository services are often registered as scoped services. Default registration of DbContext in EntityFramework Core is also scoped. Scoped lifetime ensures that all the services created within the request shares the same DbContext.
  • Use transient;
    • If your service holds a private (non-shared) state for the execution context.
    • If your service will be used by multiple threads concurrently and it is not thread safe.
    • If your service has a transient dependency, such as HttpClient, that has a short intended lifetime.

By dependencies

  • Singleton services can inject other singleton service. Singleton service can also inject transient services but be aware that transient service will be alive as long as the singleton service which is usually the lifetime of the application.
  • Singleton services should not inject scoped services as a best practice. Because scoped services will act as a singleton, which usually is not the intended way by design for scoped services.
  • Scoped services can inject other scoped services and singleton services. Scoped services can also use transient services but you should review your design and see if you can register your transient service as scoped service.
  • Transient services can inject all type of services. They are usually intended as short-lived services.
  • If you need to use transient services in singleton or scoped services you should consider service factory approach for transient services. If your transient service implementation is a disposable class, the service factory approach is recommended.

Useful Recommendations

  • Scoped services created in root scope are basically single-instance services, because they will be tracked and not disposed during the lifetime of the root scope. If the scoped service has dependencies, dependent services will also be created in root scope. When you request a scoped service from root scope, which has a disposable transient service dependency, transient service is also tracked by the root scope and will not be disposed until the root scope is disposed.
static class Program {
static void Main() {
var services = new ServiceCollection();
services.AddTransient<TransientService>();
services.AddScoped<ScopedService>();
using ServiceProvider serviceProvider = services.BuildServiceProvider();
// Since it is resolved in root scope, Scoped service will act like a singleton
// Transient service which is a dependency of ScopedService will also be not disposed until the service provider is disposed.
var avoidToResolveLikeThisScopedService = serviceProvider.GetService<ScopedService>();
// Correct usage
using (var scope = serviceProvider.CreateScope()) {
var resolvedScopedService = scope.ServiceProvider.GetService<ScopedService>();
// Do something with the resolved service
}
// ... some other code
}
}
// ...
sealed class TransientService : IDisposable {
public void Dispose() {}
}
class ScopedService {
public ScopedService(TransientService transientService){
// ...
}
}
  • Another bad design of service scoping would be to inject scoped service into singleton service’s constructor. To avoid invalid service scope references, you can use validateScopes parameters while building the service container. The root service container used by Asp.Net Core already sets this parameter to true.
static class Program {
static void Main() {
var services = new ServiceCollection();
services.AddSingleton<SingletonService>();
services.AddScoped<ScopedService>();
using ServiceProvider serviceProvider = services.BuildServiceProvider(validateScopes: true);
// This will cause an exception
var resolvedSingletonService = serviceProvider.GetService<SingletonService>();
}
}
class SingletonService {
public SingletonService(ScopedService scopedService)
{
// ...
}
}
class ScopedService {}
  • Avoid using service locator pattern whenever possible. Use dependency injection instead of calling GetService method of IServiceProvider. This will lead to easier testing, maintenance and readable code.

Try to Avoid This

public class MyService {
public MyService(IServiceProvider services) {
this._logger = services.GetRequiredService<ILogger<MyService>>();
}
}

Avoid static access to service provider

public class MyService {
public MyService() {
this._logger = ServiceLocator.Services.GetRequiredService<ILogger<MyService>>();
}
}

Better way

public class MyService {
public MyService(ILogger<MyService> logger) {
this._logger = logger;
}
}
  • If your Mvc controller has multiple endpoints and one or more of your endpoints are using a particular service that is not used by other endpoints, Asp.Net Core Mvc provides a custom binding by FromServicesAttribute. If you decorate your service parameter with this attribute, Mvc services will automatically resolve and bind the service before calling the action.

Example Code;

[ApiController]
public class EventsController: ConrollerBase {
public EventsController(ISomeCommonService commonService) {
_commonService = commonService;
}
[HttpGet("{id}")]
public ActionResult<EventModel> Get(string id){
// ... code using ISomeCommonService
}
[HttpPost("uncommon-action")]
public ActionResult UncommonAction([FromServices]IPrivateService privateService, [FromBody]MyData data){
// ... code using IPrivateService
}
}

First-Times

In order to publish this post, I did the following items for the first time.

  • This is my first public post. I have written articles for internal sharing, documentation, knowledge base and educational purposes before, but this is my first time publishing publicly. I’m grateful to PEAKUP, where I currently work, for encouraging me to share and contribute to the community, rather than labeling what I do in my spare time as intellectual property. Like me, they believe that it is very important to contribute to community.
  • I used Azure Static Web Apps for the first time. Azure Static Web Apps is a new managed service added to Azure and I’ve never had a chance to try it. I mostly use Azure Kubernetes Services, Azure App Services, Function Apps for deployments.
  • I shared 2 public docker images to Docker Hub for the first time. For production I mostly use private registries, specifically Azure Container Registry.
  • I used Git Actions for the first time. We use private repositories and Azure DevOps pipelines for the enterprise applications we develop. I’ve been thinking about trying it for a long time and finally I succeeded as a step towards achieving my goal.
  • I had to develop a custom git action for the first time. Well, all I did was to fork and make changes but it still counts, right?
  • I used Vue.js for the first time for production. It’s a simple project, but still, releasing a publicly available application developed with technology you’ve never used for production is a great deal. I also made quite a few changes to the boilerplate code for my liking which made it more exciting for me.

Credits

Cover Image
Photo by Hans-Peter Gauster on Unsplash

Recent Posts

Leave a Comment

Website Protected by Spam Master


Start typing and press Enter to search

X