OdeToCode IC Logo

Avoiding the Service Locator Pattern in ASP.NET Core

Thursday, February 18, 2016

Six years ago, Mark Seeman authored a post titled “Service Locator is an Anti-Pattern”. All of the problems Mark demonstrated back then are still problems today. I’ve been pointing developers to Mark’s post because the service locator anti-pattern is a pattern I’ve seen creep into some early ASP.NET Core code bases. The code usually looks something like the following:

var provider = HttpContext.ApplicationServices;
var someService = provider.GetService(typeof(ISomeService));

It is easy to use the service locator pattern in ASP.NET. Nearly every type of component, from controllers to views, have access to the new HttpContext, and HttpContext exposes a service provider via ApplicationServices and ReqeustServices properties. Even though HttpContext facilitates the service locator pattern, I try to avoid using these properties. 

The service locator pattern typically appears in applications that have not fully embraced an inversion of control container, or dependency injection. Yet, the new ASP.NET offers these features from the very beginning of an application, meaning it is just as easy (or easier) to avoid the service locator pattern.

For Startup

In “How To Stop Worrying About ASP.NET Startup Conventions”, I remarked how the Configure method of the Startup class for an application is an injectable method. The runtime will resolve any services you need from the application container and pass them along as arguments to this method.

public class Startup
{
    public void ConfigureServices(IServiceCollection services) { }
 
    public void Configure(IApplicationBuilder app,
                          IAmACustomService customService)
    {
        // ....   
    }        
}

For Middleware

Custom middleware has two opportunities to ask for services. Both the constructor and the conventional Invoke method are injectable.

public class TestMiddleware
{
    public TestMiddleware(RequestDelegate next, IAmACustomService service)
    {
        // ...
    }

    public async Task Invoke(HttpContext context, IAmACustomService service)
    {
        // ...
    }    
}

Constructor injection is good for those services you can keep around for the duration of the application, while Invoke injection is useful when you may need the framework to give you a service instance scoped to the current HTTP request.

For Controllers

Constructors are injectable.

public class HelloController : Controller
{
    private readonly IAmACustomService _customService;

    public HelloController(IAmACustomService customService)
    {
        _customService = customService;
    }

    public IActionResult Get()
    {
        // ...
    }
}

For Actions

You can also ask for a dependency in a controller action using the [FromServices] attribute on a parameter. Instead of model binding the parameter from the HTTP request environment, the framework will use the container to resolve the parameter. 

[HttpGet("[action]")]
public IActionResult Index([FromServices] IAmACustomService service)
{            
    // ...
}

For Models

[FromServices] also works on model properties. For example, the following action needs an instance of a TestModel.

public IActionResult Index(TestModel model)
{
    // ...
}

The TestModel class class looks like the following.

public class TestModel
{       
    public string Name { get; set; }

    [FromServices]
    public IAmACustomService CustomService { get; set; }
}

For property injection to work, the property does need a public setter. Also, constructor injection does not work for model objects. Both of these facts are lamentable.

For Views

In “Extending Razor Views”, I made the argument that the @inject directive should be the only extensibility mechanism you should use.

@inject IAmACustomService CustomService;
 
<div>
    Blarg   
</div>

For Everything Else, Even Filters

For every other type of component there is almost always a way to have constructor injection work. This is true even for action filters, which have been notoriously difficult in previous versions of ASP.NET MVC. For more on DI with filters specifically, see Filip’s post “Action Filters, Service Filters, and Type Filters”.