DEV Community

Costin Manda
Costin Manda

Posted on • Originally published at siderite.dev on

Custom resolve handler in Microsoft's ServiceProvider (enabling property injection)

Original post at: https://siderite.dev/blog/custom-resolve-handler-in-microsofts-serviceprovid/

Intro

Some of the most visited posts on this blog relate to dependency injection in .NET. As you may know, dependency injection has been baked in in ASP.Net almost since the beginning, but it culminated with the MVC framework and the .Net Core rewrite. Dependency injection has been separated into packages from where it can be used everywhere. However, probably because they thought it was such a core concept or maybe because it is code that came along since the days of UnityContainer, the entire mechanism is sealed, internalized and without any hooks on which to add custom code. Which, in my view, is crazy, since dependency injection serves, amongst other things, the purpose of one point of change for class instantiations.

Now, to be fair, I am not an expert in the design patterns used in dependency injection in the .NET code. There might be some weird way in which you can extend the code that I am unaware of. In that case, please illuminate me. But as far as I went in the code, this is the simplest way I found to insert my own hook into the resolution process. If you just want the code, skip to the end.

Using DI

First of all, a recap on how to use dependency injection (from scratch) in a console application:

// you need the nuget packages Microsoft.Extensions.DependencyInjection 
// and Microsoft.Extensions.DependencyInjection.Abstractions
using Microsoft.Extensions.DependencyInjection;
...

// create a service collection
var services = new ServiceCollection();
// add the mappings between interface and implementation
services.AddSingleton<ITest, Test>();
// build the provider
var provider = services.BuildServiceProvider();

// get the instance of a service
var test = provider.GetService<ITest>();

Enter fullscreen mode Exit fullscreen mode

Note that this is a very simplified scenario. For more details, please check Creating a console app with Dependency Injection in .NET Core.

Recommended pattern for DI

Second of all, a recap of the recommended way of using dependency injection (both from Microsoft and myself) which is... constructor injection. It serves two purposes:

  1. It declares all the dependencies of an object in the constructor. You can rest assured that all you would ever need for that thing to work is there.
  2. When the constructor starts to fill a page you get a strong hint that your class may be doing too many things and you should split it up.

But then again, there is the "Learn the rules. Master the rules. Break the rules" concept. I've familiarized myself with it before writing this post so that now I can safely break the second part and not master anything before I break stuff. I am talking now about property injection, which is generally (for good reason) frowned upon, but which one may want to use in scenarios adjacent to the functionality of the class, like logging. One of the things that always bothered me is having to declare a logger in every constructor ever, even if in itself a logger does nothing to the functionality of the class.

So I've had this idea that I would use constructor dependency injection EVERYWHERE, except logging. I would create an ILogger<T> property which would be automatically injected with the correct implementation at resolution time. Only there is a problem: Microsoft's dependency injection does not support property injection or resolution hooks (as far as I could find). So I thought of a solution.

How does it work?

Third of all, a small recap on how ServiceProvider really works.

When one does services.BuildServiceProvider() they actually call an extension method that does new ServiceProvider(services, someServiceProviderOptions). Only that constructor is internal, so you can't use it yourself. Then, inside the provider class, the GetService method is using a ConcurrentDictionary of service accessors to get your service. In case the service accessor is not there, the method from the field _createServiceAccessor is going to be used. So my solution: replace the field value with a wrapper that will also execute our own code.

The solution

Before I show you the code, mind that this applies to .NET 7.0. I guess it will work in most .NET Core versions, but they could change the internal field name or functionality in which case this might break.

Finally, here is the code:

public static class ServiceProviderExtensions
{
    /// <summary>
    /// Adds a custom handler to be executed after service provider resolves a service
    /// </summary>
    /// <param name="provider">The service provider</param>
    /// <param name="handler">An action receiving the service provider, 
    /// the registered type of the service 
    /// and the actual instance of the service</param>
    /// <returns>the same ServiceProvider</returns>
    public static ServiceProvider AddCustomResolveHandler(this ServiceProvider provider,
                 Action<IServiceProvider, Type, object> handler)
    {
        var field = typeof(ServiceProvider).GetField("_createServiceAccessor",
                        BindingFlags.Instance | BindingFlags.NonPublic);
        var accessor = (Delegate)field.GetValue(provider);
        var newAccessor = (Type type) =>
        {
            Func<object, object> newFunc = (object scope) =>
            {
                var resolver = (Delegate)accessor.DynamicInvoke(new[] { type });
                var resolved = resolver.DynamicInvoke(new[] { scope });
                handler(provider, type, resolved);
                return resolved;
            };
            return newFunc;
        };
        field.SetValue(provider, newAccessor);
        return provider;
    }
}
Enter fullscreen mode Exit fullscreen mode

As you can see, we take the original accessor delegate and we replace it with a version that runs our own handler immediately after the service has been instantiated.

Populating a Logger property

And we can use it like this to do property injection now:

static void Main(string[] args)
{
    var services = new ServiceCollection();
    services.AddSingleton<ITest, Test>();
    var provider = services.BuildServiceProvider();
    provider.AddCustomResolveHandler(PopulateLogger);

    var test = (Test)provider.GetService<ITest>();
    Assert.IsNotNull(test.Logger);
}

private static void PopulateLogger(IServiceProvider provider, Type type, object service)
{
    if (service is null) return;
    var propInfo = service.GetType().GetProperty("Logger",BindingFlags.Instance|BindingFlags.Public);
    if (propInfo is null) return;
    var expectedType = typeof(ILogger<>).MakeGenericType(service.GetType());
    if (propInfo.PropertyType != expectedType) return;
    var logger = provider.GetService(expectedType);
    propInfo.SetValue(service, logger);
}

Enter fullscreen mode Exit fullscreen mode

See how I've added the PopulateLogger handler in which I am looking for a property like

public ILogger<Test> Logger { get; private set; }
Enter fullscreen mode Exit fullscreen mode

(where the generic type of ILogger is the same as the class) and populate it.

Populating any decorated property

Of course, this is kind of ugly. If you want to enable property injection, why not use an attribute that makes your intention clear and requires less reflection? Fine. Let's do it like this:

// Add handler
provider.AddCustomResolveHandler(InjectProperties);
...

// the handler populates all properties that are decorated with [Inject]
private static void InjectProperties(IServiceProvider provider, Type type, object service)
{
    if (service is null) return;
    var propInfos = service.GetType()
        .GetProperties(BindingFlags.Instance | BindingFlags.Public)
        .Where(p => p.GetCustomAttribute<InjectAttribute>() != null)
        .ToList();
    foreach (var propInfo in propInfos)
    {
        var instance = provider.GetService(propInfo.PropertyType);
        propInfo.SetValue(service, instance);
    }
}
...

// the attribute class
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class InjectAttribute : Attribute {}

Enter fullscreen mode Exit fullscreen mode

Conclusion

I have demonstrated how to add a custom handler to be executed after any service instance is resolved by the default Microsoft ServiceProvider class, which in turn enables property injection, one point of change to all classes, etc. I once wrote code to wrap any class into a proxy that would trace all property and method calls with their parameters automatically. You can plug that in with the code above, if you so choose.

Be warned that this solution is using reflection to change the functionality of the .NET 7.0 ServiceProvider class and, if the code there changes for some reason, you might need to adapt it to the latest functionality.

If you know of a more elegant way of doing this, please let me know.

Hope it helps!

Top comments (1)

Collapse
 
kade profile image
KaDe

Instead of reflection you can use the following trick:

public static IServiceCollection AddServiceWithCalback<TService>(
    this IServiceCollection ioc, 
    Action<TService> handler, 
    ServiceLifetime serviceLifetime)
{
    var typeToBeRegistered = typeof(TService);
    ioc.Add(
        new ServiceDescriptor(
            serviceType: typeof(object), 
            serviceKey: typeToBeRegistered, 
            implementationType: 
            typeToBeRegistered, serviceLifetime));

    var registration = new ServiceDescriptor(
        serviceType: typeToBeRegistered,
        factory: (IServiceProvider provider) =>
        {
            var raw = provider.GetKeyedServices(typeof(object), typeToBeRegistered)?.FirstOrDefault();
            if (raw is not null
                && raw is TService instance)
            {
                handler(instance);
                return instance;
            }

            throw new ApplicationException($"Type {typeToBeRegistered} not registered");
        },
        lifetime: serviceLifetime);
    ioc.Add(registration);

    return ioc;
}
Enter fullscreen mode Exit fullscreen mode

The same trick for View and ViewModel registration:

public static IServiceCollection AddViewWithViewModel<TView, TViewModel>(
    this IServiceCollection ioc, 
    ServiceLifetime lifetime)
    where TView : FrameworkElement
    where TViewModel : INotifyPropertyChanged
{
    var typeToRegister = typeof(TView);
    ioc.Add(
        new ServiceDescriptor(
            serviceType: typeof(FrameworkElement), 
            serviceKey: typeToRegister, 
            implementationType: typeToRegister, lifetime));

    ioc.Add(
        new ServiceDescriptor(
            serviceType: typeof(TViewModel), 
            implementationType: typeof(TViewModel), 
            lifetime: lifetime));

    var registration = 
        new ServiceDescriptor(
            serviceType: typeToRegister,
            factory: (IServiceProvider provider) =>
            {
                var instance = provider.GetKeyedServices(typeof(FrameworkElement), typeToRegister).FirstOrDefault();
                if (instance is not null &&
                    instance is FrameworkElement view)
                {
                    var viewModel = provider.GetService<TViewModel>();
                    if (viewModel is not null)
                    {
                      view.DataContext = viewModel;
                    }
                }

              return instance ?? throw new ApplicationException("Type not registered");
            },
            lifetime: lifetime);
    ioc.Add(registration);
    return ioc;
}
Enter fullscreen mode Exit fullscreen mode