✅ How to Get DI Services in a Console Application

After some reading of various sources, mainly the official MS docs, I have my console app set up like this and it all appears to be working fine:
var builder = Host.CreateApplicationBuilder(args);

builder.Configuration.Sources.Clear();
IHostEnvironment env = builder.Environment;
builder.Configuration
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, true);

builder.Services.Configure<DbOptions>(builder.Configuration.GetSection("Database"));
builder.Services.AddTransient<EnvironmentService>();

using var serviceProvider = builder.Services.BuildServiceProvider();

var svc = serviceProvider.GetService<EnvironmentService>();
svc.ImportEnvironment(@"C:\Development\WorkProjects\Postman\Environments\seriti-V3-local-small.postman_environment.json");
var builder = Host.CreateApplicationBuilder(args);

builder.Configuration.Sources.Clear();
IHostEnvironment env = builder.Environment;
builder.Configuration
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, true);

builder.Services.Configure<DbOptions>(builder.Configuration.GetSection("Database"));
builder.Services.AddTransient<EnvironmentService>();

using var serviceProvider = builder.Services.BuildServiceProvider();

var svc = serviceProvider.GetService<EnvironmentService>();
svc.ImportEnvironment(@"C:\Development\WorkProjects\Postman\Environments\seriti-V3-local-small.postman_environment.json");
My big question is how do I access a service provider to get a service in other classes in the app? Do I have to have a "root service" singleton that all the other services are injected into, then access that from everywhere else, or, as I hope, is the there a better way to get a service required for a task from other classes in the app?
24 Replies
jcotton42
jcotton423d ago
@VoidPointer you really should be starting everything from the DI root.
VoidPointer
VoidPointerOP3d ago
Do you mean something like this?
builder.Services.AddSingleton<RootService>();
builder.Services.AddSingleton<RootService>();
then provide access to the other services via RootService?
jcotton42
jcotton423d ago
No. That’s service locator, don’t do that.
SleepWellPupper
I think there might be a misunderstanding in the fundamentals of DI here; you define dependencies of classes in the class itself, usually via constructor arguments. The composition root, aka the app, then constructs your classes with their dependencies injected. Crucially, this is not something you usually do manually. Read more here: https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection
Dependency injection - .NET
Learn how to use dependency injection within your .NET apps. Discover how to registration services, define service lifetimes, and express dependencies in C#.
SleepWellPupper
Specifically, the mechanism you should use here is auto wiring, where the DI container wires up all the dependencies to create your service. In a console application, that object graph root (so called as it is the top level object created by the DI container) is usually some implementation of IHostedService and family. A use pattern can be found by creating a new project with the dotnet Worker Service template.
SleepWellPupper
namespace WorkerService1;

public class Program
{
public static void Main(string[] args)
{
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddHostedService<Worker>();

var host = builder.Build();
host.Run();
}
}
namespace WorkerService1;

public class Program
{
public static void Main(string[] args)
{
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddHostedService<Worker>();

var host = builder.Build();
host.Run();
}
}
namespace WorkerService1;

public class Worker : BackgroundService
{
private readonly ILogger<Worker> _logger;

public Worker(ILogger<Worker> logger)
{
_logger = logger;
}

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while(!stoppingToken.IsCancellationRequested)
{
if(_logger.IsEnabled(LogLevel.Information))
{
_logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
}
await Task.Delay(1000, stoppingToken);
}
}
}
namespace WorkerService1;

public class Worker : BackgroundService
{
private readonly ILogger<Worker> _logger;

public Worker(ILogger<Worker> logger)
{
_logger = logger;
}

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while(!stoppingToken.IsCancellationRequested)
{
if(_logger.IsEnabled(LogLevel.Information))
{
_logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
}
await Task.Delay(1000, stoppingToken);
}
}
}
No description
SleepWellPupper
Note specifically how the user code, Worker.cs does not access IServiceProvider directly, instead it relies on auto wiring to inject its dependencies. Also note how the Worker service is not explicitly instantiated by user code; instead the app itself manages its lifetime.
VoidPointer
VoidPointerOP3d ago
Thank you, but I fully understand this, and have been using DI in ASP.NET for ages. I've even played with some quite advanced stuff there, but I've always just been used to getting a service injected into a controller when ASP.NET instantiates the controller, or using [FromServices] on a request parameter in minimal APIs.
Now in a console app I don't have either of these, so it seems to me I either have to register every class with a dependency on a service, even if I don't see it as a service, as a service as well, also with builder.Services.AddTransient, and only ever get instances of these from a service provider, only in Main. Is that what @jcotton42 means in his answer above by "doing everything from the DI root"?
SleepWellPupper
I'm not sure I understand the question then. Yes, you have to register all dependencies of your object graph root to the service collection if you want to resolve that root from it. The term 'Service' does not restrict the kinds of dependencies you may register. Any reference type can be registered to the container. You only need to access the service provider directly to instantiate the root object, not its dependencies.
VoidPointer
VoidPointerOP3d ago
I think I understand you, yet I asked about that above (here RootService and RootObject are the same thing), with:
Do you mean something like this?
builder.Services.AddSingleton<RootService>();
builder.Services.AddSingleton<RootService>();
then provide access to the other services via RootService
and @jcotton42 said not to do that, that's the service locator anti-pattern. Maybe I had the bit about providing access to other services via RootService phrased wrong, but what I meant was use the other services somewhere in an object graph with RootService as its root. I did not mean create RootService in order to provide access to other services. That would clearly be a service locator. So we end up with a sort of layered structure, with objects further from the Main method depending on those closer to it, which eventually depend on services/objects instantiated by the service provider inside the Main method. There is thus no way to instantiate an object that depends on injected services without doing it through a service provider inside the Main method. Unless we do something daft like expose a ServiceProvider property on the Program class, but that would also just be another service locator.
SleepWellPupper
Ah I think I get your meaning now; are you trying to do something like
var root = builder.Services.AddSingleton<RootService>();
root.Foo.DoFooThings();
root.Bar.DoBarThings();
var root = builder.Services.AddSingleton<RootService>();
root.Foo.DoFooThings();
root.Bar.DoBarThings();
maybe? In that case, injecting Foo and Bar via constructor injection into RootService and then accessing them would be fine imo. Are we converging on your intended meaning and question? Sorry if I'm being dense.
VoidPointer
VoidPointerOP3d ago
We are definitely converging, by accessing Foo and Bar from Main, because that is the only place I can get a reference to them, through RootService. My original question started out being, how to I get a reference to RootService , or any other service outside of Main? Although now I'm starting to see a design where I don't have to. I can parse args and determine which command to execute, and get the required service and pass it to the command handler, or even register one big umbrella command handler as transient and for each command invocation, i.e. each time the console app is run, I use something like this in Main:
using var serviceProvider = builder.Services.BuildServiceProvider();
var svc = serviceProvider.GetService<CommandHandler>();
var result = svc.ExecuteCommand(args)'
using var serviceProvider = builder.Services.BuildServiceProvider();
var svc = serviceProvider.GetService<CommandHandler>();
var result = svc.ExecuteCommand(args)'
SleepWellPupper
Is this code snippet the whole purpose of the application? Or is there more code to execute after determining the result?
VoidPointer
VoidPointerOP3d ago
That is basically the whole purpose of the application. The ExecuteCommand method will do all the work, such as call a method on an injected DbService and process the result of that. The result in the var result = svc.ExecuteCommand(args) call in Main may just be written to console to report the result of the command.
SleepWellPupper
I see. In that case, I think the worker template may yet be the kind of application template you could use. With it, you can at least avoid using the service provider directly:
public class Worker(CommandHandler handler) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
var args = Environment.GetCommandLineArgs();
var result = handler.ExecuteCommand(args);
}
}
public class Worker(CommandHandler handler) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
var args = Environment.GetCommandLineArgs();
var result = handler.ExecuteCommand(args);
}
}
VoidPointer
VoidPointerOP3d ago
That's looking good, thanks. I'm currently busy looking at how Spectre.Cli, System.CommandLine, and Cocona integrate with .NET DI, because I'm going to probably end up using one of them instead of manual command parsing.
SleepWellPupper
Configuration providers - .NET
Discover how to configure .NET apps using the configuration provider API and the available configuration providers.
SleepWellPupper
You can parse commandline args via configuration providers and then bind an options object against them
SleepWellPupper
Options pattern - .NET
Learn the options pattern to represent groups of related settings in .NET apps. The options pattern uses classes to provide strongly-typed access to settings.
VoidPointer
VoidPointerOP3d ago
Thanks man. I've explored configuration providers in some depth, but also only in ASP.NET projects. I've just recently been wondering how to bind command line args to an IOptions class for my CLI app.
SleepWellPupper
By default in some host builders, they are included in its configuration.
VoidPointer
VoidPointerOP2d ago
I've decided I'm going to use Cocona for this app. It handles it's own IServiceCollection, but it has the neatest out of the three console libraries I've tried. Thanks for all the input.
SleepWellPupper
You're welcome, I hope some of it helped. $close
MODiX
MODiX2d ago
If you have no further questions, please use /close to mark the forum thread as answered

Did you find this page helpful?