Mastering Dependency Injection in ASP.NET Core: Going Beyond the Basics

Improving Dependency Injection in ASP.NET Core Using Advanced Methods

Md hasanuzzzaman
5 min readSep 23, 2024

Introduction

Dependency Injection (DI) is an important feature in ASP.NET Core, enabling you to manage the parts of your utility that rely upon every different in a bendy manner. While many developers understand the basics, superior DI strategies can extensively enhance the capability of huge or complex initiatives. In this article, We will go through few advance techniques.

Section 1: Deep Dive into Service Lifetimes

ASP.NET Core supports three kinds of provider lifetimes: Singleton, Scoped, and Transient. Each has its very own use case and implications for utility performance.

  • Singleton: A single instance is used during the application’s lifetime. Suitable for stateless services or caching.
  • Scoped: A new instance is created according to request. Best for services that want to manipulate state for a request however no longer globally.
  • Transient: A new instance is created every time the provider is asked. Ideal for lightweight, stateless offerings.

Advanced Tip: Be careful with injecting scoped offerings into singleton offerings. ASP.NET Core will throw exceptions in case you try to clear up a scoped provider from a singleton. A common answer is to use IServiceProvider or a factory sample to manipulate the scope manually.

public class MySingletonService
{
private readonly IServiceProvider _serviceProvider;

public MySingletonService(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}

public void DoWork()
{
using (var scope = _serviceProvider.CreateScope())
{
var scopedService = scope.ServiceProvider.GetRequiredService<IMyScopedService>();
scopedService.PerformTask();
}
}
}

Section 2: Using IServiceProvider and IServiceScopeFactory

In some eventualities, you can want greater management over service lifetimes. The IServiceProvider interface lets in for programmatic introduction of provider scopes, which is specifically useful in lengthy-walking responsibilities like history services or in programs with non-standard request lifetimes.

For instance, if you have a hosted service that desires to remedy scoped services, you can use IServiceScopeFactory:

public class MyBackgroundService : BackgroundService
{
private readonly IServiceScopeFactory _scopeFactory;

public MyBackgroundService(IServiceScopeFactory scopeFactory)
{
_scopeFactory = scopeFactory;
}

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
using (var scope = _scopeFactory.CreateScope())
{
var scopedService = scope.ServiceProvider.GetRequiredService<IMyScopedService>();
await scopedService.DoWorkAsync();
}
await Task.Delay(1000, stoppingToken);
}
}
}

This sample ensures that every iteration of the background undertaking has its very own sparkling instance of the scoped provider.

Section 3: Conditional DI with IServiceProvider

In advanced eventualities, you may want to register offerings conditionally based on runtime configurations or different factors. Using the IServiceProvider for runtime DI can give you this pliability:

public void ConfigureServices(IServiceCollection services)
{
if (someCondition)
{
services.AddTransient<IMyService, MyServiceA>();
}
else
{
services.AddTransient<IMyService, MyServiceB>();
}
}

ASP.NET Core’s IServiceProvider permits runtime good judgment to decide which implementation to inject based totally on the environment or specific situations.

Section 4: Handling Multiple Implementations with IEnumerable<T>

Handling Multiple Implementations with IEnumerable<T>

Sometimes, you may want to inject a couple of implementations of an interface and work with them dynamically. ASP.NET Core DI supports this through IEnumerable<T>:

public class MyConsumerService
{
private readonly IEnumerable<IMyService> _services;

public MyConsumerService(IEnumerable<IMyService> services)
{
_services = services;
}

public void ExecuteAll()
{
foreach (var service in _services)
{
service.PerformOperation();
}
}
}

This permits you to register multiple offerings and determine at runtime which one to invoke.

services.AddTransient<IMyService, MyServiceA>();
services.AddTransient<IMyService, MyServiceB>();

Section 5: Managing Circular Dependencies

Circular dependencies may be problematic while the usage of DI. For example, if two offerings rely upon each other, it may cause runtime exceptions. ASP.NET Core lets in for breaking such cycles using constructor injection or by applying strategies like technique or assets injection, or by the usage of factories.

To clear up circular dependencies:

  1. Use a manufacturing unit sample to break the cycle.
  2. Inject IServiceProvider and resolve one of the offerings at runtime.
public class ServiceA
{
private readonly IServiceProvider _serviceProvider;

public ServiceA(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}

public void UseServiceB()
{
var serviceB = _serviceProvider.GetService<ServiceB>();
serviceB.PerformOperation();
}
}

Section 6: Implementing a Class with Multiple Interfaces Sharing the Same Instance

When a category implements more than one interface, it’s viable to sign in and inject the equal example for special interfaces, ensuring that the same item is shared throughout the application for each interface.

Here’s an example wherein a single instance of a class implements multiple interfaces:

public interface IEmailSender
{
void SendEmail(string to, string subject, string body);
}

public interface ISmsSender
{
void SendSms(string number, string message);
}

public class NotificationService : IEmailSender, ISmsSender
{
public void SendEmail(string to, string subject, string body)
{
// Email sending logic
}

public void SendSms(string number, string message)
{
// SMS sending logic
}
}

To make sure the same instance is shared throughout each interfaces, sign in the elegance as soon as and map both interfaces to that equal instance:

services.AddSingleton<NotificationService>(); // Register the class
services.AddSingleton<IEmailSender>(provider => provider.GetService<NotificationService>()); // Use the same instance
services.AddSingleton<ISmsSender>(provider => provider.GetService<NotificationService>()); // Use the same instance

Here, NotificationService is registered as a singleton, and each IEmailSender and ISmsSender resolve to the equal example of NotificationService using issuer.GetService<NotificationService>().

When you inject both interfaces into a category, the same NotificationService example can be shared:

public class CommunicationController
{
private readonly IEmailSender _emailSender;
private readonly ISmsSender _smsSender;

public CommunicationController(IEmailSender emailSender, ISmsSender smsSender)
{
_emailSender = emailSender;
_smsSender = smsSender;
}

public void NotifyUser(string email, string phoneNumber)
{
_emailSender.SendEmail(email, "Subject", "Body");
_smsSender.SendSms(phoneNumber, "Message");
}
}

Why This Works

NotificationService is best instantiated once because of its singleton registration.

Both IEmailSender and ISmsSender are resolved because of the same instance of NotificationService, ensuring steady conduct and nation.

Section 7: Using AddKeyedTransient for Multiple Implementations

In .NET 8, ASP.NET Core introduces AddKeyed techniques, which includes AddKeyedTransient, AddKeyedScoped, and AddKeyedSingleton, which permit you to register a couple of implementations of the same carrier and retrieve the precise one based on a key at runtime. This is mainly beneficial if you have a couple of versions of the equal provider interface but need to choose one dynamically relying on the context.

Let’s bear in mind an example in which you’ve got distinct implementations of a IPaymentProcessor interface based on the price type, which includes CreditCardProcessor and PayPalProcessor:

public interface IPaymentProcessor
{
void ProcessPayment(decimal amount);
}

public class CreditCardProcessor : IPaymentProcessor
{
public void ProcessPayment(decimal amount)
{
// Logic for credit card payment
Console.WriteLine($"Processing credit card payment of {amount}");
}
}

public class PayPalProcessor : IPaymentProcessor
{
public void ProcessPayment(decimal amount)
{
// Logic for PayPal payment
Console.WriteLine($"Processing PayPal payment of {amount}");
}
}

Using AddKeyedTransient, you may sign in both implementations with exclusive keys:

services.AddKeyedTransient<IPaymentProcessor, CreditCardProcessor>("CreditCard");
services.AddKeyedTransient<IPaymentProcessor, PayPalProcessor>("PayPal");

To resolve the proper implementation based totally on the key, you can inject IKeyedServiceProvider:

public class PaymentService
{
private readonly IKeyedServiceProvider _keyedServiceProvider;

public PaymentService(IKeyedServiceProvider keyedServiceProvider)
{
_keyedServiceProvider = keyedServiceProvider;
}

public void MakePayment(string paymentType, decimal amount)
{
var processor = _keyedServiceProvider.GetRequiredService<IPaymentProcessor>(paymentType);
processor.ProcessPayment(amount);
}
}

Now, depending on the important thing (“CreditCard” or “PayPal”), the best implementation of IPaymentProcessor may be resolved and used.

public class PaymentService
{
private readonly IKeyedServiceProvider _keyedServiceProvider;

public PaymentService(IKeyedServiceProvider keyedServiceProvider)
{
_keyedServiceProvider = keyedServiceProvider;
}

public void MakePayment(string paymentType, decimal amount)
{
var processor = _keyedServiceProvider.GetRequiredService<IPaymentProcessor>(paymentType);
processor.ProcessPayment(amount);
}
}

This approach is particularly useful when your software supports multiple strategies or behaviors (e.g. charge strategies, notification channels) and lets in for easy separation between one-of-a-kind implementations.

Conclusion

Advanced DI techniques in ASP.NET Core offer more control over utility conduct, making it viable to optimize for overall performance, manipulate service lifetimes, and implement complex architectures cleanly. By expertise in a way to handle scoped offerings, decorators, and provider lifetimes properly, you can take complete benefit of ASP.NET Core’s powerful DI framework.

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

Md hasanuzzzaman
Md hasanuzzzaman

Written by Md hasanuzzzaman

Software Architect | Senior Software Engineer | Backend Developer | Tech Lead | Azure | ASP.NET | Blazor | C# | AI

No responses yet

Write a response

Recommended from Medium

Lists

See more recommendations