3

Console apps don't use the Startup file with configure services like web apps do and I'm struggling to understand the crucial concept of Dependency Injection.

(Please note the below example does not compile)

Here is a basic example of how I think it should work (please do point out anything unconventional or wrong):

        static void Main(string[] args)
        {
            var builder = new ConfigurationBuilder()
                .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
                .AddUserSecrets<Settings>()
                .Build();

            var services = new ServiceCollection()
                .AddLogging(b => b
                    .AddConsole())
                .AddDbContext<UnderstandingDIContext>(options =>
                    options.UseSqlite(builder.GetConnectionString("DefaultConnection")))
                .BuildServiceProvider();

            var logger = services.GetService<ILoggerFactory>()
                .CreateLogger<Program>();

            logger.LogInformation("Starting Application");

            var worker = new Worker();

            logger.LogInformation("Closing Application");
        }

But how do I use these services inside my 'Worker' class?:

        public Worker(ILogger logger, IConfiguration configuration)
        {
            logger.LogInformation("Inside Worker Class");
            var settings = new Settings()
            {
                Secret1 = configuration["Settings:Secret1"],
                Secret2 = configuration["Settings:Secret2"]
            };
            logger.LogInformation($"Secret 1 is '{settings.Secret1}'");
            logger.LogInformation($"Secret 2 is '{settings.Secret2}'");

            using (var context = new UnderstandingDIContext())
            {
                context.Add(new UnderstandingDIModel()
                {
                    Message = "Adding a message to the database."
                });
            }
        }

UnderstandingDIContext

    public class UnderstandingDIContext : DbContext
    {
        public UnderstandingDIContext(DbContextOptions<UnderstandingDIContext> options)
            : base(options)
        { }

        public DbSet<UnderstandingDIModel> UnderstandingDITable { get; set; }
    }

The problems with this code are as follows:

Worker() is expecting to be passed ILogger and IConfiguration parameters but I thought Dependency Injection should cover that?

I cannot run 'dotnet ef migrations add Initial' because I'm not correctly passing in the connection string (error: 'Unable to create an object of type 'UnderstandingDIContext'.')

'using (var context = new UnderstandingDIContext())' won't compile because I'm misunderstanding the DbContext bit.

I've searched around A LOT and there's lots of examples for web apps but very little for Console apps. Am I just completely misunderstanding the entire concept of Dependency Injection?

poke
  • 369,085
  • 72
  • 557
  • 602
mwade
  • 189
  • 1
  • 4
  • 9
  • Don’t create `new Worker()` yourself. Instead register it in your `ServiceCollection` and let DI instantiate it for you. – Dai May 04 '19 at 14:32
  • Does that mean you HAVE to implement an interface (e.g. IWorker) or is it a recommended practice? – mwade May 04 '19 at 14:43
  • No, you can register a dependency without an interface. Just put the concrete type in both the registration call and as a constructor parameter. – Dai May 04 '19 at 15:11

2 Answers2

4

When using constructor injection, dependencies will only be resolved when the object you are creating is actually created through dependency injection itself. So the key to make dependency injection work within your Worker is to actually resolve Worker through the dependency injection container as well.

This is actually pretty simple:

var services = new ServiceCollection()
    .AddLogging(b => b.AddConsole())
    .AddDbContext<UnderstandingDIContext>(options =>
        options.UseSqlite(builder.GetConnectionString("DefaultConnection")));

// register `Worker` in the service collection
services.AddTransient<Worker>();

// build the service provider
var serviceProvider = services.BuildServiceProvider();

// resolve a `Worker` from the service provider
var worker = serviceProvider.GetService<Worker>();

var logger = serviceProvider.GetService<ILogger<Program>>();
logger.LogInformation("Starting Application");

worker.Run();

logger.LogInformation("Closing Application");

In addition, since you are using a database context which gets registered as a scoped dependency by default, I would recommend you to create a service scope as well—or alternatively change the lifetime of the database context when you register it.

var serviceProvider = services.BuildServiceProvider();

using (var scope = serviceProvider.CreateScope())
{
    var worker = serviceProvider.GetService<Worker>();
    worker.Run();
}

Note that I also made an explicit method Run on your worker, so that you don’t have the logic within the constructor.

public class Worker
{
    private readonly ILogger<Worker> _logger = logger;
    private readonly IConfiguration _configuration = configuration;
    private readonly UnderstandingDIContext _dbContext = dbContext;

    public Worker(ILogger<Worker> logger, IConfiguration configuration, UnderstandingDIContext dbContext)
    {
        _logger = logger;
        _configuration = configuration;
        _dbContext = dbContext;
    }

    public void Run()
    {
        _logger.LogInformation("Inside Worker Class");
        var settings = new Settings()
        {
            Secret1 = configuration["Settings:Secret1"],
            Secret2 = configuration["Settings:Secret2"]
        };

        _logger.LogInformation($"Secret 1 is '{settings.Secret1}'");
        _logger.LogInformation($"Secret 2 is '{settings.Secret2}'");

        _dbContext.Add(new UnderstandingDIModel()
        {
            Message = "Adding a message to the database."
        });
        _dbContext.SaveChanges();
    }
}
poke
  • 369,085
  • 72
  • 557
  • 602
  • 1
    After more research, trying different things and aided by @Dai's advice in the comments above, I was nearly there and remarkably close to the code you've posted here but this is explained and demonstrated well so you get the tick. Thank you very much for your time. – mwade May 04 '19 at 16:01
  • 1
    When trying to create a database migration I'm getting an error 'Unable to create an object of type 'UnderstandingDIContext'.' - Everything compiles correctly and the connection string is being read successfully. Am I missing something simple? I have a feeling it's something to do with Database.EnsureCreate() in Sqlite but not sure where to put that. – mwade May 04 '19 at 17:21
  • @mwade How are you trying to create the database migration? You might need to set up a [design time context factory](https://learn.microsoft.com/en-us/ef/core/miscellaneous/cli/dbcontext-creation) in order to use EF tooling. – poke May 04 '19 at 18:27
  • 1
    Using the CLI command 'dotnet ef migrations add Create'. Why doesn't this work? `services.AddDbContext(options => options.UseSqlite(builder.GetConnectionString("DefaultConnection")));` It doesn't seem to be correctly injecting when using: `public UnderstandingDIContext(DbContextOptions options) : base(options) { }` The only way I can find to do it is to manually put in the connection string on the OnConfiguring method instead of getting from the appsettings.json. – mwade May 04 '19 at 18:40
0

Take a look at this API. https://github.com/Akeraiotitasoft/ConsoleDriving Nuget: Akeraiotitasoft.ConsoleDriving

  • 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](/help/how-to-answer). – Community Sep 23 '21 at 08:39