-2

I'm working on a .NET application where every command/query handler needs access to three shared services: IValidator, ISecurity and ILogger. Rather than manually injecting these into each handler class, I'd like to automate the process.

What I’ve tried:

I attempted to use a source generator to inject these services automatically. However, it doesn’t seem to work. My assumption (also chatting with ChatGPT) is that the issue stems from the fact that each handler already has a primary constructor for handler specific dependencies. The source generator ends up trying to generate a second constructor, which causes conflicts leaving the compiler unsure which one to use.

What I’m looking for:

Is there a way to merge common services like IValidator, ISecurity and ILogger into the existing constructor logic without having to manually update every handler class? I've searched extensively (Bing, Google, AI chats) but haven’t found a clean solution.

Open to DI container tricks, Roslyn workarounds, or any other approach that avoids boilerplate.

10
  • You can add a separate dependencies class with the three properties, and pass that to an abstract base class to unpack them. But sometimes there's no easy way to reduce boilerplate code. Commented Sep 15 at 2:38
  • 1
    If using Autofac, you could opt for property injection, especially with the PropertiesAutowired option to auto-inject designated properties. (autofac.readthedocs.io/en/latest/register/…) If not using Autofac, why not use Autofac. :) Commented Sep 15 at 3:18
  • 1
    As described, I would derive every (one of this type of) "handler" from a base class that implements the 3 interfaces; even if these implementations need to hook themselves up. Commented Sep 15 at 4:56
  • These common services sound like cross-cutting concerns, have you considered something like decoration to solve this issue? Commented Sep 15 at 8:00
  • 1
    Just do it. I bet you would be finished by now if you hadn't spent your time asking LLMs and on SO. Commented Sep 15 at 12:00

4 Answers 4

2

You can centralize the injection of common services like IValidator, ISecurity, and ILogger into your command/query handlers by using a "static service locator" with a delegate-based approach. This avoids modifying each handler's constructor.

Implementation:

public static class AppServices
{
    private static Func<IServiceProvider>? _providerAccessor;

    public static void Init(Func<IServiceProvider> providerAccessor)
    {
        _providerAccessor = providerAccessor ?? throw new ArgumentNullException(nameof(providerAccessor));
    }

    private static IServiceProvider CurrentProvider =>
        _providerAccessor?.Invoke() ?? throw new InvalidOperationException("ServiceLocator not initialized");

    public static IValidator Validator => CurrentProvider.GetRequiredService<IValidator>();
    public static ISecurity Security => CurrentProvider.GetRequiredService<ISecurity>();
    public static ILogger Logger => CurrentProvider.GetRequiredService<ILogger>();
}

Setup with IHttpContextAccessor:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpContextAccessor();
...

var app = builder.Build();

// Initialize the service locator once with a delegate
var accessor = app.Services.GetRequiredService<IHttpContextAccessor>();
AppServices.Init(() => accessor.HttpContext?.RequestServices ?? app.Services); 
// Note: fallback to root provider ensures it works in background services or outside an HTTP request

app.MapControllers();
app.Run();

You can use AppServices in a handler like this:

public class MyCommandHandler
{
    public void Handle(MyCommand command)
    {
        // Use services directly inline
        if (!AppServices.Validator.Validate(command.Input))
        {
            AppServices.Logger.Log("Validation failed");
            throw new InvalidOperationException("Invalid input");
        }

        var user = AppServices.Security.GetCurrentUser();
        AppServices.Logger.Log($"Executing command for user {user.Name}");

        // Command logic here...
    }
}

The above approach will work, but it is not considered best practice. It hides dependencies and makes testing harder.

The recommended approach is to use constructor injection for your services or use patterns like decorator/middleware to provide cross-cutting concerns, as some of the other answers suggest.

Sign up to request clarification or add additional context in comments.

1 Comment

Wait, so on each request you're updating a static variable, with a captured lamda containing the request's scoped httpcontext? That is terrible advice. Using a IHttpContextAccessor to load an active request context from an async local would be better. But still bad.
0

You cannot seamlessly inject new services into existing constructors without modifying them. However, you can work around this limitation using several established patterns, depending on how strict you want to be about avoiding boilerplate code.

Aggregate dependency (“CommonServices”): wrap IValidator, ISecurity, ILogger in a single service:

public class CommonServices
{
    public IValidator Validator { get; }
    public ISecurity Security { get; }
    public ILogger Logger { get; }

    public CommonServices(IValidator validator, ISecurity security, ILogger logger)
    {
        Validator = validator;
        Security = security;
        Logger = logger;
    }
}

1 Comment

As it’s currently written, your answer is unclear. Please edit to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers in the help center.
0

You could use any of the following approaches the solve the problem:

Constructor Injection

Use a DI framework that supports marking the constructor that the IoC container should use with an attribute, and add said attribute to the source-generated constructor.

The generated constructor should also invoke the pre-existing constructor using the this keyword. If there are already multiple pre-existing constructors in your handlers, then you need to come up with some method to resolve the situation (e.g. always invoke the constructor with the largest number of parameters).

You would also need to make sure that none of your pre-existing constructors already contain the attribute that informs the DI framework which constructor to use.

Property Injection

Use a DI framework that supports property injection and source generate properties for all the shared services.

Service Locator

Define extension methods for your command/query handler interfaces and have those internally acquire the shared services using the service locator pattern.

Comments

0

I'm going to say this upfront. I don't like this pattern. IMHO you should just write the constructors.

But here's a way to inject known properties using a factory method. Use ActivatorUtilities.CreateInstance to do the usual parameter injection. Then manually assign the base properties;

public abstract class ServiceBase<T>
    where T:ServiceBase<T>
{
    public ILogger<T> Logger { get; set; }
}

public void Register<T>(IServiceCollection services)
    where T : ServiceBase<T>
{
    services.AddScoped<T>(sp =>
    {
        var instance = ActivatorUtilities.CreateInstance<T>(sp);
        instance.Logger = sp.GetRequiredService<ILogger<T>>();
        return instance;
    });
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.