The best way to avoid something is to know about it, so let’s see how to implement the Service Locator pattern using IServiceProvider to find a dependency.The service we want to use is an implementation of IMyService. Let’s start with the interface:
namespace ServiceLocator;
public interface IMyService : IDisposable
{
void Execute();
}
The interface inherits from the IDisposable interface and contains a single Execute method. Here is the implementation, which does nothing more than throw an exception if the instance has been disposed of (we’ll leverage this later):
namespace ServiceLocator;
public class MyServiceImplementation : IMyService
{
private bool _isDisposed = false;
public void Dispose() => _isDisposed = true;
public void Execute()
{
if (_isDisposed)
{
throw new NullReferenceException(“Some dependencies have been disposed.”);
}
}
}
Then, let’s add a controller that implements the Service Locator pattern:
namespace ServiceLocator;
public class MyController : ControllerBase
{
private readonly IServiceProvider _serviceProvider;
public MyController(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider ??
throw new ArgumentNullException(nameof(serviceProvider));
}
[Route(“/service-locator”)]
public IActionResult Get()
{
using var myService = _serviceProvider
.GetRequiredService<IMyService>();
myService.Execute();
return Ok(“Success!”);
}
}
In the preceding code, instead of injecting IMyService into the constructor, we are injecting IServiceProvider. Then, we use it (highlighted line) to locate the IMyService instance. Doing so shifts the responsibility for creating the object from the container to the consumer (MyController, in this case). MyController should not be aware of IServiceProvider and should let the container do its job without interference.What could go wrong? If we run the application and navigate to /service-locator, everything works as expected. However, if we reload the page, we get an error thrown by the Execute() method because we called Dispose() during the previous request. MyController should not control its injected dependencies, which is the point that I am trying to emphasize here: leave the container to control the lifetime of dependencies rather than trying to be a control freak. Using the Service Locator pattern opens pathways toward those wrong behaviors, which will likely cause more harm than good in the long run.Moreover, even though the ASP.NET Core container does not natively support this, we lose the ability to inject dependencies contextually when using the Service Locator pattern because the consumer controls its dependencies. What do I mean by contextually? Let’s assume we have two classes, A and B, implementing interface I. We could inject an instance of A into Consumer1 but an instance of B into Consumer2.Before exploring ways to fix this, here is the Program.cs code that powers this program:
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddSingleton<IMyService, MyServiceImplementation>()
.AddControllers()
;
var app = builder.Build();
app.MapControllers();
app.Run();
The preceding code enables controller support and registers our service.To fix the controller, we must either remove the using statement or even better: move away from the Service Locator pattern and inject our dependencies instead. Of course, you are reading a dependency injection chapter, so I picked moving away from the Service Locator pattern. Here’s what we are about to tackle:
- Method injection
- Constructor injection
- Minimal API
Let’s start with method injection.
Implementing method injection
The following controller uses method injection instead of the Service Locator pattern. Here’s the code that demonstrates this:
public class MethodInjectionController : ControllerBase
{
[Route(“/method-injection”)]
public IActionResult GetUsingMethodInjection([FromServices] IMyService myService)
{
ArgumentNullException.ThrowIfNull(myService, nameof(myService));
myService.Execute();
return Ok(“Success!”);
}
}
Let’s analyze the code:
- The FromServicesAttribute class tells the model binder about method injection.
- We added a guard clause to protect us from null.
- Finally, we kept the original code except for the using statement.
Method injection like this is convenient when a controller has multiple actions but only one uses the service.
Let’s reexplore constructor injection.