Overriding ASP.NET Core Framework-Provided Services
You take the red pill—you stay in Wonderland, and I show you how deep the rabbit hole goes
Overview
In .NET it’s really easy to create your own interfaces and implementations. Likewise, it’s seemingly effortless to register them for dependency injection. But it is not always obvious how to override existing implementations. Let’s discuss various aspects of “dependency injection” and how you can override the “framework-provided services”.
As an example, let’s take a recent story on our product backlog for building a security audit of login attempts. The story involved the capture of attempted usernames along with their corresponding IP addresses. This would allow system administrators to monitor for potential attackers. This would require our ASP.NET Core application to have custom logging implemented.
Logging
Luckily
ASP.NET Core Logging
is simple to use and is a first-class
citizen within ASP.NET Core
.
In the Logging repository there is an extension method namely
AddLogging
, here is what it
looks like:
public static IServiceCollection AddLogging(this IServiceCollection services)
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}
services.TryAdd(ServiceDescriptor.Singleton<ILoggerFactory, LoggerFactory>());
services.TryAdd(ServiceDescriptor.Singleton(typeof(ILogger<>), typeof(Logger<>)));
return services;
}
As you can see, it is rather simple. It adds two ServiceDescriptor
instances to the IServiceCollection
, effectively registering the given service type to the
corresponding implementation type.
Following the rabbit down the hole
When you create a new ASP.NET Core
project from Visual Studio, all the templates follow the same pattern. They have the Program.cs
file with a Main
method that looks
very similar to this:
public static void Main(string[] args)
{
var host = new WebHostBuilder()
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.UseStartup<Startup>()
.UseApplicationInsights()
.Build();
host.Run();
}
Templates Program.cs
Empty | Starter Web | Web API |
One thing that is concerning about a template like this is that the IWebHost
is an IDisposable
, so why then is this statement not wrapped in a using
you ask
? The answer is that the Run
extension method
internally wraps itself in a using
. If you were wondering where the AddLogging
occurs, it is a result of invoking the Build
function.
[ Microsoft.AspNetCore.Hosting.WebHostBuilder ]
public IWebHost Build() ...
private IServiceCollection BuildCommonServices() ...
creates services then invokes services.AddLogging()
A few words on the Service Descriptor
The ServiceDescriptor
class is an object that describes a service, and this is used by dependency injection. In other words, instances of the ServiceDescriptor
are
descriptions of services. The ServiceDescriptor
class exposes several static methods that allow its instantiation.
The ILoggerFactory
interface is registered as a
ServiceLifetime.Singleton
and its implementation is mapped to the LoggerFactory
. Likewise, the generic type typeof(ILogger<>)
is mapped to typeof(Logger<>)
. This is just one of the several key
“Framework-Provided Services” that are registered.
Putting it together
Now we know that the framework is providing all implementations of ILogger<T>
, and resolving them as their Logger<T>
. We also know that we could write our own implementation of
the ILogger<T>
interface. Being that this is open-source
we can look to their implementation
for inspiration.
public class RequestDetailLogger<T> : ILogger<T>
{
private readonly ILogger _logger;
public RequestDetailLogger(ILoggerFactory factory,
IRequestCategoryProvider requestCategoryProvider)
{
if (factory == null)
{
throw new ArgumentNullException(nameof(factory));
}
if (requestCategoryProvider == null)
{
throw new ArgumentNullException(nameof(requestCategoryProvider));
}
var category = requestDetailCategoryProvider.CreateCategory<T>();
_logger = factory.CreateLogger(category);
}
IDisposable ILogger.BeginScope<TState>(TState state)
=> _logger.BeginScope(state);
bool ILogger.IsEnabled(LogLevel logLevel)
=> _logger.IsEnabled(logLevel);
void ILogger.Log<TState>(LogLevel logLevel,
EventId eventId,
TState state,
Exception exception,
Func<TState, Exception, string> formatter)
=> _logger.Log(logLevel, eventId, state, exception, formatter);
}
The IRequestCategoryProvider
is defined and implemented as follows:
using static Microsoft.Extensions.Logging.Abstractions.Internal.TypeNameHelper;
public interface IRequestCategoryProvider
{
string CreateCategory<T>();
}
public class RequestCategoryProvider : IRequestCategoryProvider
{
private readonly IPrincipal _principal;
private readonly IPAddress _ipAddress;
public RequestCategoryProvider(IPrincipal principal,
IPAddress ipAddress)
{
_principal = principal;
_ipAddress = ipAddress;
}
public string CreateCategory<T>()
{
var typeDisplayName = GetTypeDisplayName(typeof(T));
if (_principal == null || _ipAddress == null)
{
return typeDisplayName;
}
var username = _principal?.Identity?.Name;
return $"User: {username}, IP: {_ipAddress} {typeDisplayName}";
}
}
If you’re curious how to get the IPrincipal
and IPAddress
into this implementation (with DI) -
I discussed it here
briefly. It is pretty straight-forward. In the Startup.ConfigureServices
method do the following:
public void ConfigureServices(IServiceCollection services)
{
// ... omitted for brevity
services.AddTransient<IRequestCategoryProvider, RequestCategoryProvider>();
services.AddTransient<IHttpContextAccessor, HttpContextAccessor>();
services.AddTransient<IPrincipal>(
provider => provider.GetService<IHttpContextAccessor>()
?.HttpContext
?.User);
services.AddTransient<IPAddress>(
provider => provider.GetService<IHttpContextAccessor>()
?.HttpContext
?.Connection
?.RemoteIpAddress);
}
Finally, we can
Replace
the implementations for the ILogger<T>
by using the following:
public void ConfigureServices(IServiceCollection services)
{
// ... omitted for brevity
services.Replace(ServiceDescriptor.Transient(typeof(ILogger<>),
typeof(RequestDetailLogger<>)));
}
Notice that we replace the framework-provided service as a ServiceLifetime.Transient
. Opposed to the default ServiceLifetime.Singleton
. This is more or less an extra
precaution. We know that with each request we get the HttpContext
from the IHttpContextAccessor
, and from this we have the User
. This is what is passed to each
ILogger<T>
.
Conclusion
This approach is valid for overriding any of the various framework-provided service implementations. It is simply a matter of knowing the correct ServiceLifetime
for your
specific needs. Likewise, it is a good idea to leverage the open-source libraries of the framework for inspiration. With this you can take finite control of your web-stack.
Share this post
Sponsor
Twitter
Facebook
Reddit
LinkedIn
StumbleUpon
Email