1

I have an interface and two classes

 public interface IReportGenerator
 {
    void Process(ActivityReport activityReport);
 }    

 class APReportGenerator : IReportGenerator
 {
    void Process(ActivityReport activityReport);
 }

 class ARReportGenerator : IReportGenerator
 {
    void Process(ActivityReport activityReport);
 }

In my controller, based on user input, I am instantiating the child classes like this

 public class AccountController : Controller
 {
      private IReportGenerator reportGenerator;
      [HttpPost]
      public IActionResult Submit(ActivityReport activityReport)
      {
          switch(activityReport.Type)
          {
               case "AR":
                         reportGenerator = new ARReportGenerator();
                         break;
               case "AP":
                         reportGenerator = new APReportGenerator();
                         break;
          }
          reportGenerator.Process(activityReport);
          ...
      }
 }

Can someone please help me rewrite the code without the use of new?

In startup.cs, can I have something like this?

 services.AddScoped<IReportGenerator, APReportGenerator>();
 services.AddScoped<IReportGenerator, ARReportGenerator>();

And in controller?

 public class AccountController : Controller
 {
    private readonly IReportGenerator reportGenerator;
    public AccounterController(IReportGenerator reportGenerator)
    {
        this.reportGenerator = reportGenerator;
    }

Sorry but I am stuck after this and dont know if this is correct or wrong.

blue piranha
  • 3,706
  • 13
  • 57
  • 98
  • 1
    Please refer to this question: [Conditional Dependency Resolution Question](https://stackoverflow.com/questions/57758285/conditional-dependency-resolver-on-run-time-net-core) Hope this helps – Shahzaib Hassan Apr 19 '23 at 18:23

2 Answers2

4

In some containers this can be achieved with keyed/named registrations but build-in one does not support this. So the usual approach is to use factory. Simple one using Func delegate can look something like the following:

services.AddScoped<APReportGenerator>();
services.AddScoped<ARReportGenerator>();

services.AddScoped<Func<string, IReportGenerator>>(sp => keyStr => keyStr switch
{
    "AR" => sp.GetRequiredService<ARReportGenerator>(),
    "AP" => sp.GetRequiredService<APReportGenerator>(),
    _ => throw new InvalidOperationException()
});

And in controller:

public class AccountController : Controller
{
    private readonly IReportGenerator reportGeneratorFactory;
    public AccounterController(Func<string, IReportGenerator> reportGeneratorFactory)
    {
        this.reportGeneratorFactory = reportGeneratorFactory;
    }

    [HttpPost]
    public IActionResult Submit(ActivityReport activityReport)
    {
         var generator = reportGeneratorFactory(activityReport.Type);
         // use generator 
    }
}

P.S.

Your approach will lead to ARReportGenerator always being used (because it was added last).

Guru Stron
  • 102,774
  • 10
  • 95
  • 132
2

The Java doctrine applies in this case, being: when in doubt, use a factory:

public interface IReportGenerator
{
    string Prefix { get; }

    void Process();
}

public interface IReportGeneratorFactory
{
    IReportGenerator GetReportGenerator(string prefix);
}

You then let each report generator report which prefixes it can handle:

internal class APReportGenerator : IReportGenerator
{
    public string Prefix => "AP";

    public void Process(ActivityReport activityReport) { }
}

And create a factory that stores all registered generators with their prefix:

internal class ReportGeneratorFactory : IReportGeneratorFactory
{
    private readonly Dictionary<string, IReportGenerator> _generators;

    public ReportFactory(IEnumerable<IReportGenerator> generators)
    {
        _generators = generators.ToDictionary(g => g.Prefix, g => g);
    }

    public IReportGenerator GetReportGenerator(string prefix)
    {
        return _generators[prefix];
    }
}

The trick here is to accept an IEnumerable<IReportGenerator>, which causes the Microsoft DI to inject all registered IReportGenerators.

You register this as such:

services.AddScoped<IReportGenerator, APReportGenerator>();
services.AddScoped<IReportGenerator, ARReportGenerator>();

services.AddScoped<IReportGeneratorFactory, ReportGeneratorFactory>();

Then where you need a generator, you inject the factory instead, and based on the report type, obtain the relevant generator:

public class AccountController : Controller
{
    private readonly IReportGeneratorFactory _reportGeneratorFactory;

    public AccounterController(IReportGeneratorFactory reportGeneratorFactory)
    {
        _reportGeneratorFactory = reportGeneratorFactory;
    }

    [HttpPost]
    public IActionResult Submit(ActivityReport activityReport)
    {
        var generator = _reportGeneratorFactory.GetReportGenerator(activityReport.Type);

        generator.Process();
    }
}

This code still needs to handle two exceptional cases, namely multiple generators reporting the same prefix and a generator being requested whose prefix isn't known. The code shown will throw an exception in both cases.

CodeCaster
  • 147,647
  • 23
  • 218
  • 272
  • 1
    "The Java doctrine", that's a good one. That's indeed what they do best in Java, factories building factories building factories. Although there are cases where the use of factory is the best solution, I experienced this to be often not the best solution. I, therefore, consider factories a code smell. Read [this](https://blogs.cuttingedge.it/steven/posts/2016/abstract-factories-are-a-code-smell/) to understand my reasoning. – Steven Apr 20 '23 at 07:35
  • @Steven nice writeup, interesting, thanks. The approach displayed in my answer does indeed not delay `IReportGenerator` instantiation, instead all of them get instantiated to feed into the factory. GuruStron's answer does that better. The statically-typed handler approach won't fly when the selector is a string instead of a typed message though (`ActivityReport.Type`). – CodeCaster Apr 20 '23 at 08:05