In modern .NET applications, dependency injection is a fundamental design pattern that promotes loose coupling and enhances maintainability. At the center of .NET’s dependency injection system lies the IServiceProvider interface, which acts as a service locator for retrieving service instances. Understanding how to obtain and work with an IServiceProvider instance is vital for developers who want to leverage the full power of dependency injection in their applications.

To download the source code for this article, you can visit our GitHub repository.

In this article, we’ll explore various methods to get an instance of IServiceProvider in .NET in different situations where this might be necessary.

What Is IServiceProvider?

IServiceProvider is a core interface that defines a single method, GetService():

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!
public interface IServiceProvider
{    
    object? GetService(Type serviceType);
}

This method allows us to resolve instances of services registered in the DI container. It’s beneficial when we need to obtain service instances at runtime dynamically.

By leveraging IServiceProvider, we centralize the management of service dependencies, leading to a cleaner and more maintainable architecture. The DI container manages the services lifecycle, ensuring proper instance management across the application.

Additionally, Microsoft provides the ServiceProviderServiceExtensions class within the Microsoft.Extensions.DependencyInjection namespace, which offers more convenient methods for working with IServiceProvider. We’ll explore some of these in our examples.

Methods to Obtain an Instance of IServiceProvider

For demonstration purposes, let’s create a simple interface and some services that implement it:

public interface IExampleService { }

public class ExampleService : IExampleService { }

public class DevelopmentExampleService : IExampleService { }

Next, we register this service as scoped in the DI container:

builder.Services.AddScoped<IExampleService, ExampleService>();

Now, let’s explore how to resolve this service using IServiceProvider in different contexts.

During Application Startup

There are several ways to access IServiceProvider during the startup phase of an application.

We can access IServiceProvider  by using the BuildServiceProvider() method before building the application and using it to resolve instances of our services:

IServiceProvider serviceProvider = builder.Services.BuildServiceProvider();
var serviceInstance1 = serviceProvider.GetService<IExampleService>();

However, this approach can lead to issues like creating multiple instances of scoped services and should generally be avoided.

Once the application is built using builder.Build(), services can be properly resolved using the app.Services property:

IServiceProvider serviceProvider = app.Services;
using var serviceScope = serviceProvider.CreateScope();
var serviceInstance2 = serviceScope.ServiceProvider.GetRequiredService<IExampleService>();

Here, we demonstrate proper handling of service lifetimes, especially for scoped services.

If you want to know more about dependency injection lifetimes, check out our article Dependency Injection Lifetimes in ASP.NET Core.

We can also use IServiceProvider when configuring services with factory methods:

builder.Services.AddScoped<IExampleService>(serviceProvider =>
{
    var config = serviceProvider.GetRequiredService<IHostEnvironment>();

    return config.IsDevelopment() ? new DevelopmentExampleService() : new ExampleService();
});

Here, we dynamically resolve the environment and choose the appropriate implementation of IExampleService based on its value.

Within a Service Class 

In services like background tasks, injecting IServiceProvider allows dynamic resolution of scoped services:

public class ExampleBackgroundService(IServiceProvider ServiceProvider) : BackgroundService
{
    protected override Task ExecuteAsync(CancellationToken stoppingToken)
    {
        using var scope = ServiceProvider.CreateScope();
        var service = scope.ServiceProvider.GetRequiredService<IExampleService>();

        return Task.CompletedTask;
    }
}

Here, the IExampleService is resolved within the background service’s execution scope, ensuring proper lifetimes.

Within a Controller

Using IServiceProvider in a controller is straightforward, thanks to HttpContext.RequestServices:

[ApiController]
public class ExampleController : ControllerBase
{
    [HttpGet("/api/example/get-serviceprovider-within-api-controller")]
    public IActionResult GetIServiceProviderFromHttpContext()
    {
        var resolvedService = HttpContext.RequestServices.GetService<IExampleService>();

        return Ok(resolvedService is not null);
    }
}

This approach simplifies sharing dependencies across multiple controllers without explicitly injecting them into each one when necessary.

Manually Building a ServiceProvider

In unit testing or console applications, we might need to manually create a ServiceProvider using the BuildServiceProvider() method, similar to our example during application startup:

var services = new ServiceCollection();
services.AddSingleton<IExampleService, ExampleService>();
var serviceProvider = services.BuildServiceProvider();
var myService = serviceProvider.GetService<IExampleService>();

This approach is particularly useful when the full ASP.NET Core infrastructure is unavailable, allowing us to control service registration and resolution manually.

Best Practices and Common Pitfalls Using IServiceProvider

Using IServiceProvider directly can bring benefits in specific scenarios such as factory patterns, middleware, or conditionally resolving services at runtime. However, we should avoid its overuse to prevent service lifetime mismatches, such as resolving scoped services from singletons, which can lead to memory leaks or unexpected behavior.

Additionally, relying too heavily on IServiceProvider complicates testing, obscures class dependencies, and can lead to tight coupling. Resolving services this way follows the service locator pattern, often considered an anti-pattern by experienced software developers.

A common mistake is resolving scoped services from singleton instances, which leads to improper lifecycle management and potential memory leaks. We should ensure proper disposal of services, especially those implementing IDisposable, to avoid resource leaks. Improper handling of transient services can also degrade performance if resolved too frequently, leading to increased memory usage.

The general rule is that we should always favor constructor injection, as the framework knows best how to manage lifetimes, dependencies, and their disposal. This approach also promotes clean, testable, and maintainable code and makes our dependencies explicit. 

Conclusion

This article explored various ways to obtain and use IServiceProvider in .NET, from application startup to within services, controllers, and more. By understanding when and how to access IServiceProvider, we can manage service lifetimes effectively and maintain clean, efficient code.

Stay tuned for the next article where we dive deeper into more .NET topics!

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!