Well, it is, sometimes. It depends.
I often get pushback on MediatR for using service location for resolving handlers, often getting pointed at Mark Seemann’s post that Service Locator is an Anti-Pattern. And for all of the examples in the post, I agree that service location in those cases should not be used.
However, like any pattern, it’s not so cut and dry. Patterns have advantages and disadvantages, situations where it’s advantageous or even necessary to use them, and situations where it’s not. In Mark’s post, I see only situations that service location is not required and better alternatives exist. However, if you’re building anything on .NET Core/6 these days, it’s nearly impossible to avoid service location because many of the underlying infrastructure components use it or only expose interfaces that only allow for service location. Why is that the case? Let’s examine the scenarios involved to see when service location is required or even preferable.
Mismatched Lifetimes
Modern dependency injection containers have a concept of service lifetimes. Different containers have different lifetimes they support, but in general I see three:
- Transient
- Scoped
- Singleton
Transient lifecycle means every time you ask the container to resolve a service, all transient services will be a new instance. Scoped lifecycle means that for a given scope or context, all scoped services will resolve in the same instance for that given scope. Singleton means that for the lifetime of the container, you’ll only get one single instance.
One must be careful not to mix and match these lifetimes that are incompatible – namely, when you inject a transient into a singleton, you’ll only get that one instance. Scoped services should only be resolved from a scope, and shouldn’t be injected into a singleton.
But sometimes that is unavoidable. If you use ASP.NET Core, you’ve likely already ran into these scenarios, where you want to use a scoped service like DbContext
inside a singleton (filters, hosted services, etc.). In these cases, you likely have no other option but to use service location.
In some cases, service location is provided through a method that provides access to the scoped composition root:
public class ValidatorActionFilter
: IAsyncActionFilter
{
public async Task OnActionExecutionAsync(
ActionExecutingContext context,
ActionExecutionDelegate next)
{
var serviceProvider = context.HttpContext.RequestServices;
var validator = serviceProvider.GetRequiredService();
if (!await validator.IsValidAsync(context))
{
context.Result = new BadRequestResult();
}
await next();
}
}
In this case, context.HttpContext.RequestServices
is the scope from which we can resolve other scoped services