4

I'm using .NET 7 Preview with ASP.NET to wrap a bunch of WCF services so they're REST. Each service has its own controller and I'm using dependency injection with the constructor.

Each WCF service is automatically generated with its own client and a couple methods that I use delegates on the base class (MyBaseController) to handle all the logic and things.

The problem is I need the injected dependencies, but if I change the base constructor, I have to modify 40+ derived classes.

Below is the basic concept I'm using, but ideally the derived classes would only contain delegate overrides and the class definition providing the generics.

You can see for derived classes A/B, I have to have a constructor.

Questions:

  1. Is there another method of dependency injection where I don't have to declare them in the constructor? This would allow me to handle it in the base class and then keep a simple constructor in the derived classes.

  2. Can I inherit the base constructor somehow? - I don't think there is with older versions of .NET so I was hoping .NET 7 might have some new magic I didn't know about.

  3. Should I be moving all of my constructor needs into a service class and then just passing that everywhere to the controllers?

Code:

[ApiController]
[Route("[controller]")]
public abstract class MyBaseController<DerivedClient, MyRequest, MyResponse> : ControllerBase
    where DerivedClient : HttpClient, new()
{
    protected DerivedClient _derivedClient;
    protected readonly ILogger _logger;
    public IConfiguration _configuration;
    protected IOptions<MyOptions> _options;

    public abstract Func<MyRequest, Task<MyResponse>> MyClientDelegate { get; }

    protected MyBaseController(ILogger<DerivedClient> logger, IOptions<MyOptions> options, IConfiguration configuration)
    {
        // I need these dependencies though, and with a simple constructor (i.e. MyBaseController()),
        // I can't access them.
        _derivedClient = new();
        _logger = logger;
        _options = options;
        _configuration = configuration;
    }

    [HttpGet, Route("GetTheData")]
    public Task<MyResponse> GetTheData(MyRequest request)
    {
        return MyClientDelegate(request);
    }
}

public class A : MyBaseController<AClient, string, string>
{
    // How can I avoid having this in every derived class when they're all essentially identical?
    public A(ILogger<AClient> logger, IOptions<MyOptions> options, IConfiguration configuration) : base(logger, options, configuration) { }

    // I only want my derived classes to have the delegate
    public override Func<string, Task<string>> MyClientDelegate => _derivedClient.GetDataA;
}
public class B : MyBaseController<BClient, string, string>
{
    // How can I avoid having this in every derived class when they're all essentially identical?
    public B(ILogger<BClient> logger, IOptions<MyOptions> options, IConfiguration configuration) : base(logger, options, configuration){ }

    // I only want my derived classes to have the delegate
    public override Func<string, Task<string>> MyClientDelegate => _derivedClient.GetDataB;
}

public class AClient : HttpClient
{
    public AClient() { }

    public Task<string> GetDataA(string request)
    {
        return Task.FromResult($"A Request: {request}");
    }
}

public class BClient : HttpClient
{
    public BClient() { }

    public Task<string> GetDataB(string request)
    {
        return Task.FromResult($"B Request: {request}");
    }
}
William YK
  • 1,025
  • 12
  • 26

1 Answers1

1

I don't think there's a way to avoid having all the constructors with the same declarations.

I believe it's a language design decision, since depending on how it is implemented it could bring some other issues*, although I feel it would be nice to have some other way to do it.

A possible improvement might be to encapsulate all of your dependencies in a new class, so that would be the class injected everywhere. If any new dependency appears you'd only change that class.

Something like this:

public abstract class Base<DerivedClient> : where DerivedClient : HttpClient, new() 
{
    protected DependencyContainer _dependencyContainer;
    protected DerivedClient _derivedClient;

    protected MyBaseController(DependencyContainer<DerivedClient> dependencyContainer)
    {
        _dependencyContainer = dependencyContainer;
        _derivedClient = new();
    }
}

The DependencyContainer would be:

public class DependencyContainer<DerivedClient> : where DerivedClient : HttpClient
{
    public readonly ILogger<DerivedClient> logger;
    public IConfiguration configuration;
    public IOptions<MyOptions> options;
    
    public DependencyContainer(
        ILogger<DerivedClient> logger,
        IOptions<MyOptions> options,
        IConfiguration configuration,
        ... future new ones)
    {
        this.logger = logger;
        this.options = options;
        this.configuration = configuration;
        ... future new ones
    }
}

And every derived class would remain like this:

public A : Base<AClient>
{
    public A(DependencyContainer<AClient> dependencyContainer) : base(dependencyContainer) { }

    public void Method()
    {
        _dependencyContainer.logger.log(":)");
        _dependencyContainer. ... all other dependencies ...
        _derivedClient.GetDataA("test"); // Method from AClient is accessible because we inherit from Base<AClient>
    }
}

Then you should remember to add the new DependencyContainer to the service collection in Program.cs:

builder.Services.AddScoped(typeof(DependencyContainer<>), typeof(DependencyContainer<>));

I don't love it, but it makes only one class change when you want to inject something new.

* I kept digging into this and found a comment in another question that links to a MSDN post that might explain this better

Maximiliano
  • 148
  • 1
  • 9
  • Have you been able to get that encapsulation method to work with the dependency injection? I'm getting `System.InvalidOperationException: Unable to resolve service for type 'DerivedConstructors.DependencyContainer' while attempting to activate 'DerivedConstructors.Controllers.A'.` when I tried it. It would definitely solve my need if I could do that but it doesn't seem like the framework is able to determine the injections unless I do _something_ else? Service? – William YK Oct 25 '22 at 15:46
  • Ah I just had to do `builder.Services.AddScoped(...)` and it worked! If you can edit your post (it won't let me) to include a line or something about adding it in `Program.cs` then your answer will be complete and I can accept it. – William YK Oct 25 '22 at 15:53
  • One thing I'm realizing is I can't seem to do generics (``), but I'm still experimenting. This looks promising - https://stackoverflow.com/questions/56271832/how-to-register-dependency-injection-with-generic-types-net-core – William YK Oct 25 '22 at 16:08
  • Give me a moment and I'll update and see if something could be done with that. I missed the generics when I answered – Maximiliano Oct 25 '22 at 19:35
  • @WilliamYK There we go. I added at least part of the solution for the DerivedClient issue. And regarding your first comment, yes, you need to let the framework know what do you want to inject. You should probably also look into AddScoped and AddTransient and how each of these work. – Maximiliano Oct 25 '22 at 20:39