2

I'm working on a .Net Core Console Application with two classes(GenerateCsv,GenerateTxt) which inherit from the same interface (GenerateFile). They Both take IReadFile,IWriteFile interfaces as constructor parameters.

I want to use Dependency Injection to create the right class to generate a file and return a string value when finished.

static void Main()
    {
        string result="Processed Nothing";

        Console.WriteLine("To Generate: \n");
        Console.WriteLine("1) English CSV File Type 1 \n"+
                          "2) Englsih Txt File Type 2 \n" +
                          "3) To Exit Type 3 \n");

        var userInput = Convert.ToInt32(Console.ReadLine());

        if (userInput == 10)
            Environment.Exit(0);

        var service = BuildDependencies.Build();

        Console.WriteLine("Started Generating");
        Console.WriteLine("Working, please wait");

        switch (userInput)
        {
            case 1:
                result = service.GetService<IGenerateFile>().GenerateFile();
                break;
            case 2:
                result = service.GetService<IGenerateFile>().GenerateFile();
                break;
        }
        Console.WriteLine(result);

class BuildDependencies

using Microsoft.Extensions.DependencyInjection;
public static class BuildDependencies
{
    public static ServiceProvider Build()
    {
        var services = new ServiceCollection()
            .AddTransient<IReadFile, ReaFile>()
            .AddTransient<IWriteFile, WriteFile>()
            .AddTransient<IGenerateFile,GenerateCsv>()
            .AddTransient<IGenerateFile,GenerateTxt>()
            .BuildServiceProvider();

        return services;
    }
Daina Hodges
  • 823
  • 3
  • 12
  • 37
  • Possible duplicate of [How can I use DI with Email and SMS classes](https://stackoverflow.com/questions/54402192/how-can-i-use-di-with-email-and-sms-classes) – mjwills Feb 17 '19 at 20:22

2 Answers2

2

You should replace the switch(userinput) block with a call to a factory class.

That class's contract might look something like:

interface IFileGeneratorFactory
{
    // I used int because it's in your example code, but an enum would be a stronger contract
    IGenerateFile GetFileGenerator(int userInput);
}

and the implementation like:

class FileGeneratorFactory : IFileGeneratorFactory
{
    readonly IServiceProvider serviceProvider;

    public FileGeneratorFactory(IServiceProvider serviceProvider)
        => this.serviceProvider = serviceProvider;

    public IGenerateFile GetFileGenerator(int userInput)
    {
        switch (userInput)
        {
            // The Factory has explicit knowledge of the different generator types,
            // but returns them using a common interface. The consumer remains
            // unconcerned with which exact implementation it's using.
            case 1:
                return serviceProvider.GetService<GenerateCsv>();
            case 2:
                return serviceProvider.GetService<GenerateTxt>();
            default:
                throw new InvalidOperationException($"No generator available for user input {userInput}");
        }
    }
}

Register the classes like:

var services = new ServiceCollection()
    .AddTransient<GenerateCsv>()
    .AddTransient<GenerateTxt>()
    .AddTransient<IFileGeneratorFactory, FileGeneratorFactory>();

Then, consume like:

IGenerateFile fileGenerator = service
    .GetService<IFileGeneratorFactory>()
    .GetFileGenerator(userInput);

string result = fileGenerator.GenerateFile();

This factory implementation is pretty simplistic. I wrote it that way to keep it approachable, but I highly recommend looking at the more elegant examples in the Simple Injector docs. Since those examples target Simple Injector, and not ASP.Net DI, they might need some tweaking to work for you--but the basic patterns should work for any DI framework.

xander
  • 1,689
  • 10
  • 18
  • When I execute the code, it goes to FileGeneratorFactory execute the code inside but fileGenerator is always null. – Daina Hodges Feb 18 '19 at 13:55
  • 1
    You may need to adjust the service registrations a bit--maybe register `GenerateCsv` and `GenerateTxt` based on their concrete types instead of their interfaces. Let me know if you figure it out and I'll update the example to be more complete. If you're still stuck, I'll double-check my code later today to see what's missing. – xander Feb 18 '19 at 15:00
  • Thanks, that worked. But is that the correct way to do it? – Daina Hodges Feb 18 '19 at 19:28
  • 1
    You mean, is it correct to register the concrete types? In this case, I'd say that it's correct. The DI container needs to be told about the types before it can resolve them. You *could* add unique interfaces (`IGenerateCsv`/`IGenerateTxt`) and register/resolve using those, but I don't think it gains you anything when you're only resolving them through the factory. – xander Feb 18 '19 at 20:47
  • 1
    I've updated my answer to include an example registration. – xander Feb 19 '19 at 04:09
1

You could create two interfaces

interface IGenerateCsv : IGenerateFile{}
interface IGenerateTxt: IGenerateFile{}

then register both with your DI provider:

public static ServiceProvider Build()
{
    var services = new ServiceCollection()
        .AddTransient<IReadFile, ReaFile>()
        .AddTransient<IWriteFile, WriteFile>()
        .AddTransient<IGenerateCsv ,GenerateCsv>()
        .AddTransient<IGenerateTxt,GenerateTxt>()
        .BuildServiceProvider();

    return services;
}

Then you could create another class which has both instances and decides which to call. Then inject that class into your consumer.

enum OutputType
{
    Csv,
    Text
}

interface IFileGenerator
{
    void GenerateFile(OutputType outputType);
}

class FileGenerator : IFileGenerator
{
    // inject both or your file services into the constructor


    public void GenerateFile(OutputType outputType)
    {
         switch(outputType)
         {
              // call the correct service here
         }
    }

Then register IFileGenerator

public static ServiceProvider Build()
{
    var services = new ServiceCollection()
    .AddTransient<IReadFile, ReaFile>()
    .AddTransient<IWriteFile, WriteFile>()
    .AddTransient<IGenerateCsv ,GenerateCsv>()
    .AddTransient<IGenerateTxt, GenerateTxt>()
    .AddTransient<IFileGenerator, FileGenerator>()
    .BuildServiceProvider();

    return services;
}

And do something like this in your consumer class:

class Consumer
{
    private readonly IFileGenerator fileGenerator;

    Consumer(IFileGenerator, fileGenerator)
    {
        this.fileGenerator = fileGenerator;
    }

    public void SomeMethod(string userInput)
    {
          switch(userInput)
          {
                case 1: 
                    fileGenerator.GenerateFile(OutputType.Csv);
                    break;

                case 2: 
                    fileGenerator.GenerateFile(OutputType.Text);
                    break;

                default:
                    break;
          }
    }
}

That is assuming you have some kind of consumer class. If you're just working in the Main method, do something like this:

var serviceProvider = BuildDependencies.Build();
var fileGenerator = serviceProvider.GetService<IFileGenerator>();
Connell.O'Donnell
  • 3,603
  • 11
  • 27
  • 61