Welcome back! In our last session, we explored how a DI Container automates the construction of your application's object graph, saving you from the manual labor of Pure DI. We saw that the container's job is to resolve a requested service by automatically creating it and all of its dependencies.
This brings us to a crucial architectural choice. Now that you have this powerful container, how should your classes obtain their dependencies from it? There are two main approaches. The first is the Dependency Injection pattern we've been building towards. The second is a pattern called Service Locator. This lesson will distinguish between these two patterns and make the case for why Dependency Injection is strongly preferred in modern C# development. Understanding this distinction is fundamental to writing clean, maintainable, and testable code.
What is the Service Locator Pattern?
Let's begin by defining Service Locator. In this pattern, a class that needs a dependency actively requests it from a central, known "locator" service. Instead of having its dependencies passively "injected" into its constructor, the class holds a reference to the locator and calls a method on it, such as locator.GetService<IMyDependency>(), whenever it needs a dependency.
The class essentially says, "I know where to find things; I'll go get what I need."

This creates a different dependency structure compared to Dependency Injection. The diagram below contrasts the three main approaches to managing dependencies.

Notice how in the "Service Location" flow, both Class C and Class A have a direct dependency on the Service Locator itself. In the "IoC / DI" flow, the components (C, A, B) are wired together by an external IoC mechanism, and they are not aware of the container or locator at all.
The Case Against Service Locator
While Service Locator might seem convenient at first, it is widely considered an anti-pattern in application code for several critical reasons. Mark Seemann, a leading voice in the .NET DI community, wrote a foundational blog post explaining why.
Service Locator is an Anti-Pattern - ploeh blog
This classic article by Mark Seemann clearly articulates the fundamental problems with the Service Locator pattern.
Read the introduction and the section OrderProcessor example. Pay attention to how the OrderProcessor class uses a static Locator to fetch its dependencies, and the problems that immediately arise for a developer trying to use that class.
As the article demonstrates, using a Service Locator introduces significant problems related to code clarity, maintainability, and reliability. Let's break down the main arguments.
1. Dependencies are Hidden
With Constructor Injection, a class's dependencies are part of its public contract. You can tell everything a class needs just by looking at its constructor signature.
// With Constructor Injection, dependencies are explicit.
public class CommerceService
{
public CommerceService(IBillingProcessor processor, ICustomerRepository customers)
{
// ...
}
}
With Service Locator, the constructor is often parameterless. The dependencies are hidden inside the implementation details of its methods.
// With Service Locator, dependencies are hidden.
public class CommerceService
{
public CommerceService() { /* No dependencies are declared here */ }
public void Checkout(Order order)
{
// The dependency is requested deep inside a method.
var processor = ServiceLocator.Current.GetInstance<IBillingProcessor>();
processor.Process(order);
}
}
This makes the class dishonest. Its public API doesn't communicate its true requirements. A developer using CommerceService has no idea it needs an IBillingProcessor registered in the service locator until they either read the source code or the application crashes.
2. Runtime Errors Instead of Compile-Time or Start-Up Errors
This leads to the second major issue. If you forget to register a dependency that a class resolves via Service Locator, you won't know until the code path that resolves it is executed. The result is a runtime exception, which might only be discovered by users in production.
In contrast, a system using proper DI with a container will fail-fast. When the container tries to resolve your object graph at application startup (in the Composition Root), it will immediately throw an exception if a required dependency is not registered. This gives you instant feedback during development.
3. Maintenance and Refactoring are Difficult
Imagine you need to add a new dependency, INotifier, to the Checkout method. With Service Locator, you would just add a new line: var notifier = ServiceLocator.Current.GetInstance<INotifier>();.
The code compiles without issue, but have you just introduced a breaking change? It's impossible to tell without analyzing every part of the application that uses this CommerceService to see if INotifier is already registered for other reasons. The compiler offers no help.
With Constructor Injection, adding a new dependency means adding a parameter to the constructor. This is a breaking change that the C# compiler will immediately flag everywhere CommerceService is created, forcing you to update the Composition Root. This is a good thing—the compiler is helping you correctly maintain your application.
For these reasons, Service Locator is considered an anti-pattern. It violates encapsulation and leads to code that is harder to understand, test, and maintain.
Service Locator is an Anti-Pattern - ploeh blog
Let's continue with Mark Seemann's analysis to solidify these points.
Please read the sections Maintenance issues and Summary. These sections perfectly summarize why the pattern makes maintenance difficult and why Constructor Injection provides a superior development experience with compiler assistance.
A Nuanced View: When is Service Locator Acceptable?
While you should avoid Service Locator in your application and business logic classes, the pattern does have legitimate uses, primarily within infrastructure code. Recognizing these exceptions is key to the "nuanced use" you're aiming for.
Jimmy Bogard, the creator of popular libraries like AutoMapper and MediatR, provides a compelling counter-perspective. He argues that while the pattern is often misused, it can be a necessary tool in specific scenarios, especially when working with the intricacies of modern DI containers.
Service Locator is not an Anti-Pattern
This article by Jimmy Bogard offers a balanced perspective, showing where Service Locator can be a necessary and pragmatic choice.
Read the introduction and the section titled Mismatched Lifetimes. This is a very common and important scenario in .NET applications.
As the article explains, one of the most common legitimate uses of Service Locator is to resolve lifetime mismatches. For example:
- You have a Singleton service, like an
IHostedServicefor background processing. - This service needs to use a Scoped service, like an
EF Core DbContext.
You cannot inject a Scoped service directly into a Singleton's constructor. This would create a "captive dependency," where the shorter-lived scoped service is captured by the singleton and lives for the entire application lifetime, which is almost certainly a bug.
The correct solution is to inject the container's IServiceProvider (which acts as a service locator) into the singleton. Then, within the method that runs, you create a manual scope, resolve the DbContext from that scope, use it, and then dispose of the scope.
public class EmailSenderService : IHostedService
{
private readonly IServiceProvider _serviceProvider;
// Inject the Service Locator (IServiceProvider)
public EmailSenderService(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
// ... inside a loop or timer ...
// Create a new scope for this unit of work
using (var scope = _serviceProvider.CreateScope())
{
// Resolve a scoped service from the new scope's provider
var dbContext = scope.ServiceProvider.GetRequiredService<MyDbContext>();
// ... use dbContext and other scoped services ...
}
}
// ...
}
This is the Service Locator pattern in action, but used appropriately within infrastructure code to solve a specific technical challenge imposed by service lifetimes. Other valid use cases, as Bogard points out, include resolving types that aren't known at compile time (common in framework middleware) or lazily resolving services to improve application startup time.
Conclusion
You now have a clear understanding of the two primary ways to use a DI container and the trade-offs involved. While both patterns use a container to provide dependencies, their approach and consequences are vastly different.
Let's summarize the key takeaways:
- Dependency Injection (Constructor Injection) is the preferred pattern for application code. It makes dependencies explicit, a class's requirements are clear from its constructor, and it enables compile-time safety and fast failure at application start-up.
- Service Locator is an anti-pattern in application code. It hides dependencies, making the code harder to understand and maintain. It trades compile-time safety for runtime errors.
- The Service Locator pattern has legitimate but rare uses, primarily in infrastructure code, to solve complex problems like mismatched service lifetimes or dynamic resolution. In these cases, you are often interacting directly with the
IServiceProvider.
The rule of thumb is simple: always prefer Constructor Injection. Only resort to using IServiceProvider directly when you have a specific infrastructure problem that cannot be solved by passing dependencies through the constructor.
In our next module, we will elevate this discussion by connecting these practical patterns to the foundational SOLID principles of object-oriented design. We'll start by exploring exactly how DI embodies the Inversion of Control (IoC) principle and supports the Single Responsibility Principle (SRP).