Dependency Injection (DI) helps us to change the behavior of parts of our application on the fly. This is especially neat when you want to test your domain services against a mocked data-store. But what if you need to change the behavior in your API based on a request header?
Yesterday I had a discussion with my colleague Robert Kranenburg about this subject. He showed an example of a console application changing its behavior based on an argument. I took the idea and converted it into .NET Core 3.1 code for a Web API.
- Let's change the message
- (3) lifetime types
- Accessing request headers
- DI config
- The result
- Final thoughts
- Further reading
Let's change the message
Our objective: change the response message based on the presence of a cookie named "hidden", using dependency injection.
First, we need to define the controller and the service interface:
public interface IMessageService
{
string GetMessage();
}
[ApiController]
[Route("")]
public class MyController : ControllerBase
{
private readonly IMessageService _messageService;
public MyController(IMessageService messageService)
{
_messageService = messageService;
}
[HttpGet]
public string Get()
{
return _messageService.GetMessage();
}
}
.NET Core uses constructor injection and will provide an instance for each constructor parameter. It will inject an IMessageService
instance into the controller.
Now, let's define 2 implementations of the IMessageService
:
-
DefaultMessageService
just returns a "Hello world!" message -
HiddenMessageService
returns a different message. To make things more interesting, it uses a dependency to construct that message.
Let's view the code:
public class DefaultMessageService : IMessageService
{
public string GetMessage() => "Hello world!";
}
public class HiddenMessageService : IMessageService
{
private readonly ISecretKey _key;
public HiddenMessageService(ISecretKey key)
{
_key = key;
}
public string GetMessage() =>
"The answer to life the universe and everything: " +
_key.GetKey();
}
public interface ISecretKey
{
public string GetKey();
}
public class SecretKey : ISecretKey
{
public string GetKey() => "42";
}
Before we set up the dependency injection, let's dive deeper into lifetimes.
3 lifetime types
A DI service is added with a lifetime:
-
AddTransient
: Adding a transient service means that each time the service is requested, a new instance is created. -
AddSingleton
: A singleton is an instance that will last the entire lifetime of the application. In web terms, it means that after the initial request of the service, every subsequent request will use that same instance, across all requests. -
AddScoped
: A scoped service is instantiated per scope. For web this means the same instance per request, but you can actually create your own scopes.
Because we want to change the behavior based on request information, it makes sense to use a scoped service.
Accessing request headers
In the olden days, we could do anything we wanted with the static HttpContext.Current
and be done with it. In .NET Core we use the IHttpContextAccessor
and dependency injection to interact with the HttpContext
. We can use the AddHttpContextAccessor
to set this up.
DI config
Let's go to the Startup.cs
and set up the dependency injection:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddHttpContextAccessor();
services.AddTransient<DefaultMessageService>();
services.AddTransient<ISecretKey, SecretKey>();
services.AddTransient<HiddenMessageService>();
services.AddScoped<IMessageService>(provider =>
{
var context = provider.GetRequiredService<IHttpContextAccessor>();
var isHidden = context.HttpContext?.Request.Cookies.ContainsKey("hidden") == true;
if (isHidden)
{
return provider.GetRequiredService<HiddenMessageService>();
}
return provider.GetRequiredService<DefaultMessageService>();
});
}
Why do we add DefaultMessageService
and HiddenMessageService
without interfaces to the service collection? Well, this helps us to use DI in the classes themselves. HiddenMessageService
needs this, because this is how we get an ISecretKey
instance. Adding these classes will make our lives way easier when we need to instantiate them. If we would "new them up" ourselves, we would need to resolve all the dependencies as well, leading to a lot of unnecessary code.
The IMessageService
is resolved via a factory method. This method gets an IServiceProvider
as input to locate any services that are required. We use this provider to locate the HTTP accessor and to check if a cookie named hidden is present. If so, we resolve the IMessageService
as HiddenMessageService
; otherwise as DefaultMessageService
.
The result
When we add the code together we get this behavior:
The contents of the message is changed based on the presence of a cookie named hidden.
Final thoughts
It is easy to change de behavior of your program based on a request header or cookie. But things can get messy quite fast when you are doing dependency injection. Startup classes get large quickly when you need to inject many classes.
To make things easier, you can use an extension method like this:
public static class HiddenCookieExtensions
{
public static IServiceCollection AddScopedByHiddenCookie<TService, THiddenImplementation, TDefaultImplementation>(this IServiceCollection services)
where TService: class
where THiddenImplementation: TService
where TDefaultImplementation: TService
{
services.AddScoped<TService>(provider =>
{
var context = provider.GetRequiredService<IHttpContextAccessor>();
var isHidden = context?.HttpContext?.Request.Cookies.ContainsKey("hidden") == true;
if (isHidden)
{
return provider.GetRequiredService<THiddenImplementation>();
}
return provider.GetRequiredService<TDefaultImplementation>();
});
return services;
}
}
Which can be used like this:
services.AddScopedByHiddenCookie<IMessageService, HiddenMessageService, DefaultMessageService>();
In this example, we used a cookie, but any header can be used to change the behavior of your application.
Further reading
While working on this topic I found some excellent sources for reading:
Top comments (0)