
Service Locator Antipattern in Dependency Injection
Author - Abdul Rahman (Bhai)
Dependency Injection
7 Articles
Table of Contents
What we gonna do?
The Service Locator is one of the most dangerous anti-patterns in dependency injection. In this article, let's learn about the Service Locator anti-pattern in .NET and understand why it's considered the exact opposite of proper Dependency Injection.
Why we gonna do?
The Service Locator anti-pattern supplies application components outside the Composition Root with access to an unbounded set of Volatile Dependencies. It's essentially a global registry that classes can query to get their dependencies.
While Service Locator might seem convenient at first, it creates several serious problems: it hides dependencies, makes code harder to test, introduces runtime errors that could be caught at compile time, and violates the principle of least knowledge.
Why Service Locator is Considered an Anti-pattern
Service Locator is problematic because:
- Hidden dependencies - Dependencies are not visible in the constructor
- Runtime errors - Missing dependencies only discovered during execution
- Tight coupling to infrastructure - Classes become dependent on the locator itself
- Difficult testing - Must set up the locator for every test
- Violates Single Responsibility Principle - Classes handle both their logic and dependency resolution
How we gonna do?
Example: ProductService Using Service Locator
Here's a typical example of the Service Locator anti-pattern:
// Service Locator interface
public interface IServiceLocator
{
T GetService<T>();
object GetService(Type serviceType);
}
// Service Locator implementation
public class ServiceLocator : IServiceLocator
{
private readonly Dictionary<Type, object> services = new();
public void RegisterService<T>(T service)
{
services[typeof(T)] = service;
}
public T GetService<T>()
{
if (services.TryGetValue(typeof(T), out var service))
return (T)service;
throw new InvalidOperationException($"Service of type {typeof(T).Name} not registered");
}
public object GetService(Type serviceType)
{
if (services.TryGetValue(serviceType, out var service))
return service;
throw new InvalidOperationException($"Service of type {serviceType.Name} not registered");
}
}
Now, here's how a class might use the Service Locator:
public class ProductService
{
private readonly IProductRepository repository;
private readonly IUserContext userContext;
public ProductService()
{
// Service Locator anti-pattern: Pulling dependencies
var locator = Locator.Current;
this.repository = locator.GetService<IProductRepository>();
this.userContext = locator.GetService<IUserContext>();
}
public Product GetFeaturedProduct()
{
var user = userContext.GetCurrentUser();
return repository.GetFeaturedProduct();
}
}
// Global access to the locator
public static class Locator
{
public static IServiceLocator Current { get; set; }
}
Problems with Service Locator Approach
1. Hidden Dependencies
Looking at the ProductService constructor, you cannot tell what dependencies it needs. The dependencies are only revealed when you examine the implementation details.
2. Runtime Errors
If a required service isn't registered, you'll only discover this at runtime when the code executes:
// This will throw at runtime, not compile time
var productService = new ProductService(); // InvalidOperationException possible
3. Difficult Unit Testing
Testing becomes complex because you must set up the Service Locator before each test:
[Test]
public void GetFeaturedProduct_ReturnsProduct()
{
// Must set up Service Locator for testing
var locator = new ServiceLocator();
var mockRepository = new Mock<IProductRepository>();
var mockUserContext = new Mock<IUserContext>();
locator.RegisterService<IProductRepository>(mockRepository.Object);
locator.RegisterService<IUserContext>(mockUserContext.Object);
Locator.Current = locator;
try
{
// Arrange
var expectedProduct = new Product { Id = 1, Name = "Test Product" };
mockRepository.Setup(r => r.GetFeaturedProduct()).Returns(expectedProduct);
// Act
var productService = new ProductService();
var result = productService.GetFeaturedProduct();
// Assert
Assert.AreEqual(expectedProduct, result);
}
finally
{
// Clean up global state
Locator.Current = null;
}
}
Refactoring from Service Locator to Proper DI
The solution is to use proper Constructor Injection instead:
// Properly designed ProductService with Constructor Injection
public class ProductService
{
private readonly IProductRepository repository;
private readonly IUserContext userContext;
public ProductService(IProductRepository repository, IUserContext userContext)
{
this.repository = repository ?? throw new ArgumentNullException(nameof(repository));
this.userContext = userContext ?? throw new ArgumentNullException(nameof(userContext));
}
public Product GetFeaturedProduct()
{
var user = userContext.GetCurrentUser();
return repository.GetFeaturedProduct();
}
}
Benefits of Constructor Injection over Service Locator
1. Explicit Dependencies
Dependencies are clearly visible in the constructor signature. You know exactly what the class needs to function.
2. Compile-time Safety
Missing dependencies are caught at compile time or during application startup, not at runtime during execution.
3. Easy Testing
[Test]
public void GetFeaturedProduct_ReturnsProduct()
{
// Arrange - Clean and simple
var expectedProduct = new Product { Id = 1, Name = "Test Product" };
var mockRepository = new Mock<IProductRepository>();
var mockUserContext = new Mock<IUserContext>();
mockRepository.Setup(r => r.GetFeaturedProduct()).Returns(expectedProduct);
var productService = new ProductService(mockRepository.Object, mockUserContext.Object);
// Act
var result = productService.GetFeaturedProduct();
// Assert
Assert.AreEqual(expectedProduct, result);
}
Composition Root Setup
With proper DI, the Composition Root handles object creation:
// In your Composition Root (e.g., Program.cs or Startup.cs)
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<IProductRepository, SqlProductRepository>();
services.AddScoped<IUserContext, HttpUserContext>();
services.AddScoped<ProductService>();
}
// Usage
public class ProductController : ControllerBase
{
private readonly ProductService productService;
public ProductController(ProductService productService)
{
this.productService = productService;
}
[HttpGet("featured")]
public ActionResult<Product> GetFeaturedProduct()
{
var product = productService.GetFeaturedProduct();
return Ok(product);
}
}
When Service Locator Might Seem Appropriate
Some developers reach for Service Locator when they think they need to resolve dependencies dynamically at runtime. However, even these scenarios usually have better solutions:
- Factory Pattern - For creating objects based on runtime data
- Strategy Pattern - For selecting algorithms at runtime
- Method Injection - For dependencies that vary per method call
Summary
The Service Locator anti-pattern might seem convenient, but it introduces significant problems including hidden dependencies, runtime errors, and difficult testing. It's essentially a global registry that violates many principles of good object-oriented design.
Instead of using Service Locator, embrace proper Constructor Injection where dependencies are explicitly declared and injected by the Composition Root. This approach provides compile-time safety, clear dependency visibility, and much easier unit testing. Remember: if you find yourself reaching for a Service Locator, step back and consider how to restructure your code to use proper dependency injection instead.