Home > OS >  How can I avoid repeated derived class constructors that are identical to the base class?
How can I avoid repeated derived class constructors that are identical to the base class?

Time:10-27

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}");
    }
}

CodePudding user response:

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

  • Related