👉🏼 Click here to Join I ❤️ .NET WhatsApp Channel to get 🔔 notified about new articles and other updates.
Service Locator Antipattern in Dependency Injection

Service Locator Antipattern in Dependency Injection

Author - Abdul Rahman (Bhai)

Dependency Injection

7 Articles

Improve

Table of Contents

  1. What we gonna do?
  2. Why we gonna do?
  3. How we gonna do?
  4. Summary

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.

👉🏼 Click here to Join I ❤️ .NET WhatsApp Channel to get 🔔 notified about new articles and other updates.
  • Dependency Injection
  • Service Locator
  • Antipattern
  • DI
  • Dependency Resolution
  • Hidden Dependencies