Level Up ASP.NET Core DI with a Custom Service Resolver

Dependency Injection (DI) in ASP.NET Core is one of the framework’s most powerful features. It promotes clean architecture, improves testability, and reduces tight coupling between components. In most cases, the built-in DI container is more than enough to handle application needs efficiently.

However, as applications grow in complexity, you may encounter scenarios where services need to be resolved dynamically at runtime rather than being fixed at compile time.

Examples:
  • Selecting service based on runtime conditions
  • Resolving multiple implementations dynamically
  • Plug-in style architecture

This is where a Custom Service Resolver becomes useful. It allows you to go beyond the standard constructor injection model and introduce controlled runtime service resolution. While this approach should be used carefully, it can be extremely powerful in scenarios such as strategy selection, multi-tenant applications, or plugin-based architectures.

In this article, we will explore how to build a Custom Service Resolver in ASP.NET Core, understand when to use it, and implement practical examples that demonstrate real-world use cases.

Level Up ASP.NET Core DI with a Custom Service Resolver

What is a Service Resolver?

A Service Resolver is a pattern used in dependency injection to retrieve services dynamically at runtime instead of relying only on constructor injection at compile time.

In ASP.NET Core, the built-in DI container automatically injects dependencies when a class is created. This works well when the required service is known in advance. However, in some scenarios, the application needs to decide which service implementation to use while the program is running.

A Service Resolver acts as a central component that abstracts access to IServiceProvider and provides a controlled way to resolve services when needed.

Why You Need a Custom Service Resolver

In most ASP.NET Core applications, the built-in Dependency Injection system works perfectly well. You register services, inject them through constructors, and the framework handles the rest. However, this model assumes that the required service is known at compile time.

In real-world applications, that assumption doesn’t always hold true. You often need to decide which implementation to use at runtime, based on conditions that are only known while the application is running.

This is where a Custom Service Resolver becomes useful.

The Problem with Direct Injection

Direct injection in ASP.NET Core works well when the required dependency is known at compile time, but it becomes restrictive when the application needs to make decisions at runtime. Since dependencies are fixed through constructor injection, a class is tightly coupled to a specific implementation, which reduces flexibility.

This often leads to problems when multiple implementations exist for the same interface, such as different payment gateways or notification services, because developers are forced to introduce conditional logic (if-else or switch statements) across the codebase. As the application grows, this approach makes the code harder to maintain, extend, and test, since every new implementation requires changes in multiple places.

How a Custom Service Resolver Solves the Problem

A Custom Service Resolver solves this problem by introducing a centralized mechanism that decides which service implementation should be used at runtime. Instead of directly injecting a concrete service, the application injects a resolver that internally uses IServiceProvider to fetch the appropriate implementation based on runtime conditions such as user input, configuration, or business rules.

This approach removes scattered conditional logic, keeps controllers and business logic clean, and makes the system easier to extend. New implementations can be added with minimal changes, usually only within the resolver, while the rest of the application remains unaffected. In this way, a custom service resolver preserves the benefits of Dependency Injection while adding the flexibility needed for dynamic, real-world scenarios.

Creating a Custom Service Resolver

This code demonstrates a common design approach in ASP.NET Core where multiple services implement the same interface and a provider dynamically selects the correct one at runtime. In this example, there are different export services such as PDF, Excel, and CSV, all implementing the IExportService interface. Each service knows how to export data into its own format and identifies itself using the ExportType property. All these services are registered in the ASP.NET Core dependency injection container, which means the framework automatically keeps track of them and can inject them wherever needed.

The ExportProvider acts as a central resolver. When the application receives a request like /api/export/pdf, the controller calls the provider and passes "pdf" as the export type. The provider searches through all registered IExportService implementations and finds the one whose ExportType matches "pdf". It then returns the corresponding service, such as PdfExportService. The controller uses that service to generate file bytes from the supplied data and returns the result as a downloadable file response to the client.

The main advantage of this approach is flexibility and maintainability. Instead of writing large if-else or switch statements everywhere in the application, the provider pattern cleanly separates each export behavior into its own class. Adding a new format like XML or JSON becomes easy because you only create a new implementation of IExportService and register it in dependency injection without changing existing controller logic. This follows the Open/Closed Principle, where the system is open for extension but closed for modification, making the application easier to scale and maintain over time.

Create Export Interface

public interface IExportService
{
string ExportType { get; }
byte[] Export<T>(List<T> data);
}
Implement Export Services

//PDF Export
public class PdfExportService : IExportService
{
  public string ExportType => "pdf";
  public byte[] Export<T>(List<T> data)
  {
    var content = "PDF Export Generated";
    return System.Text.Encoding.UTF8.GetBytes(content);
  }
}
//Excel Export
public class ExcelExportService : IExportService
{
  public string ExportType => "excel";
  public byte[] Export<T>(List<T> data)
  {
    var content = "Excel Export Generated";
    return System.Text.Encoding.UTF8.GetBytes(content);
  }
}
//CSV Export
public class CsvExportService : IExportService
{
  public string ExportType => "csv";
  public byte[] Export<T>(List<T> data)
  {
    var csv = string.Join(",", data);
    return System.Text.Encoding.UTF8.GetBytes(csv);
  }
}
Create Export Provider

public interface IExportProvider
{
  IExportService GetExporter(string exportType);
}
Implement Provider

public class ExportProvider : IExportProvider
{
  private readonly IEnumerable<IExportService> _exportServices;
  public ExportProvider(IEnumerable<IExportService> exportServices)
  {
    _exportServices = exportServices;
  }
  public IExportService GetExporter(string exportType)
  {
    var service = _exportServices.FirstOrDefault(x =>
    x.ExportType.Equals(exportType,
    StringComparison.OrdinalIgnoreCase));
    if (service == null)
    throw new NotSupportedException(
    $"Export type '{exportType}' is not supported.");
    return service;
  }
}
Register Services in Program.cs

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddTransient<IExportService, PdfExportService>();
builder.Services.AddTransient<IExportService, ExcelExportService>();
builder.Services.AddTransient<IExportService, CsvExportService>();
builder.Services.AddSingleton<IExportProvider, ExportProvider>();
var app = builder.Build();
Use in Controller

[ApiController]
[Route("api/export")]
public class ExportController : ControllerBase
{
  private readonly IExportProvider _exportProvider;
  public ExportController(IExportProvider exportProvider)
  {
    _exportProvider = exportProvider;
  }
  [HttpGet("{type}")]
  public IActionResult Export(string type)
  {
    var data = new List<string>
    {
      "Item1",
      "Item2",
      "Item3"
      };
      var exporter = _exportProvider.GetExporter(type);
      var fileBytes = exporter.Export(data);
      return File(
      fileBytes,
      "application/octet-stream",
      $"report.{type}");
    }
  }
Example Requests

GET /api/export/pdf
GET /api/export/excel
GET /api/export/csv

Why This Is Useful
  • adding new export formats without changing existing code
  • implementing strategy pattern cleanly
  • keeping controllers lightweight
  • supporting plugin-like architecture

Summary

In this article, I explore how to enhance Dependency Injection (DI) in ASP.NET Core by building custom service registration extensions and improving dependency resolution strategies. The post goes beyond basic DI concepts and demonstrates how developers can create cleaner, more maintainable, and scalable applications using advanced DI patterns.

The article explains how ASP.NET Core’s built-in DI container works, the limitations of traditional service registration approaches, and how custom extensions can simplify large-scale application setups. It also covers practical implementation techniques, including organizing service registrations, reducing boilerplate code, and improving modular architecture.

By the end of the article, readers will understand how to “level up” their ASP.NET Core DI configuration with reusable patterns that improve code readability, flexibility, and maintainability in real-world enterprise applications.

Thanks

Kailash Chandra Behera

I am an IT professional with over 13 years of experience in the full software development life cycle for Windows, services, and web-based applications using Microsoft .NET technologies.

Previous Post Next Post

نموذج الاتصال