C# & .NET (C# 14, .NET 10 LTS) Module 20 – Command-line apps, hosting, DI, configuration and telemetry
Generic Host and dependency injection
Compose a C# console app on the .NET Generic Host: register services for dependency injection, choose their lifetimes and catch lifetime mistakes at start-up.
What you will learn
- Compose a console app on the Generic Host with Host.CreateApplicationBuilder, and start and stop it in order
- Choose singleton, scoped or transient for a service, and create a scope for each unit of work
- Detect a captive dependency with ValidateScopes and ValidateOnBuild before the app handles its first message
- Resolve one of several implementations by key, or all of them as IEnumerable<T>, through constructor injection
Before you start
What you’ll learn · Before you start
What you will learn
- Compose a console app on the Generic Host with Host.CreateApplicationBuilder, and start and stop it in order
- Choose singleton, scoped or transient for a service, and create a scope for each unit of work
- Detect a captive dependency with ValidateScopes and ValidateOnBuild before the app handles its first message
- Resolve one of several implementations by key, or all of them as IEnumerable<T>, through constructor injection
Before you start
On this page
A class that makes its own dependencies with new decides in code which clock, store and sender it uses, so a test
cannot replace them and every change to them reaches into the class. The .NET
Generic Host: The host of Microsoft.Extensions.Hosting: one object that owns an app's services, configuration, logging and lifetime, and starts and stops its hosted services. Host.CreateApplicationBuilder sets one up. turns that around: in this lesson’s first program, one call gives a console
app configuration, console logging, an orderly start and stop, and a service container that builds every object by
dependency injection: Giving an object the objects it depends on, usually as constructor parameters, instead of letting it create them. In .NET a service container reads the registrations and builds each object with its dependencies..
You will compose an app on the Generic Host, give each service the lifetime it needs, catch a lifetime mistake before the app handles its first message, and choose between implementations by key.
The host builds every object your app needs
The Generic Host, from the Microsoft.Extensions.Hosting package, is one object that owns an app’s services, its
configuration, its logging and its lifetime. Dependency injection is how it builds the services: a class lists what it
needs as constructor parameters, here with a primary constructor, and the container passes them in. The program
greeting.cs registers two services, a greeter and a hosted service: A class that implements IHostedService, often by deriving from BackgroundService, registered with AddHostedService. The host starts it after the app is built and stops it when the app shuts down. that the host
starts and stops:
#:package Microsoft.Extensions.Hosting@10.0.12
#:property PublishAot=false
// A console app on the Generic Host: one call sets up configuration, logging and a service container; the greeting
// service gets what it needs through its constructor. Everything is written through the logger, so lines keep their order.
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddSingleton<IGreeter, Greeter>();
builder.Services.AddHostedService<GreetingService>();
using (IHost host = builder.Build())
{
await host.StartAsync(); // starts every hosted service, then reports that the app has started
await host.StopAsync(); // what Run does after Ctrl+C or SIGTERM: stops the services in reverse order
} // disposing the host disposes its services and flushes the console logger
Console.WriteLine("host stopped and disposed");
interface IGreeter { string Greet(string city); }
sealed class Greeter : IGreeter
{
public string Greet(string city) => $"Namaste, {city}!";
}
sealed class GreetingService(IGreeter greeter, IHostEnvironment environment, ILogger<GreetingService> logger) : IHostedService
{
public Task StartAsync(CancellationToken cancellationToken)
{
logger.LogInformation("{Greeting} (environment {Environment})", greeter.Greet("Pune"), environment.EnvironmentName);
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
logger.LogInformation("Greeting service stopped");
return Task.CompletedTask;
}
} from session import run
# network: true. greeting.cs names Microsoft.Extensions.Hosting 10.0.12 in its #:package line, and dotnet run
# restores it, with the packages it depends on, from nuget.org into the session's own, empty home folder. The second
# run sets DOTNET_ENVIRONMENT, the variable the host reads for the environment's name.
run("""
cd greeting
dotnet run greeting.cs
DOTNET_ENVIRONMENT=Development dotnet run greeting.cs
""", network=True) Output
$ cd greeting
$ dotnet run greeting.cs
info: GreetingService[0]
Namaste, Pune! (environment Production)
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
Content root path: greeting
info: Microsoft.Hosting.Lifetime[0]
Application is shutting down...
info: GreetingService[0]
Greeting service stopped
host stopped and disposed
$ DOTNET_ENVIRONMENT=Development dotnet run greeting.cs
info: GreetingService[0]
Namaste, Pune! (environment Development)
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
Content root path: greeting
info: Microsoft.Hosting.Lifetime[0]
Application is shutting down...
info: GreetingService[0]
Greeting service stopped
host stopped and disposed
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 greeting_session.py
The class GreetingService never created an IGreeter, an environment or a logger: it asked for them and got them.
Because every dependency arrives through the constructor, a unit test can create the service with a fake greeter and
no host at all, and a change of greeter never touches the class. The container also calls Dispose on the disposable
objects it created, when the host is disposed, so a service never disposes what it was given. The host reported
Production because nothing set DOTNET_ENVIRONMENT, and the same program reported Development when the variable
said so. Run does what this program did by hand: it calls StartAsync, waits for Ctrl+C or SIGTERM, calls
StopAsync and disposes the host, as the using block did here. Step through the run:
The drawing shows the whole scene; its 7 steps are listed below.
Every step as text
- The builder. Host.CreateApplicationBuilder(args) sets up configuration from files, environment variables and the command line, console logging, and the environment name, which is Production unless DOTNET_ENVIRONMENT says otherwise.
- Two registrations. The program adds two entries to builder.Services, IGreeter with its class Greeter, and GreetingService as a hosted service, which the host starts and stops for you.
- The container. builder.Build() turns the registrations into a container. Nothing has been created yet; each object is made when something first asks for it.
- The hosted service starts. StartAsync needs a GreetingService. Its constructor asks for an IGreeter, the environment and a logger, and the container supplies all three. The service logs Namaste, Pune! with the environment.
- Started. With every hosted service started, the host logs Application started, the environment and the content root. A program that calls Run would now wait for Ctrl+C or SIGTERM; this one asks for the stop at once.
- The stop. StopAsync, which Run calls after a stop signal, logs that the application is shutting down and stops the hosted services in reverse order, so the greeting service logs that it stopped.
- Disposal. Disposing the host disposes the objects the container made and flushes the console logger. Only then does the program print its last line.
Every registration also says how long its object lives. A singleton is created once and shared by the whole app. A
scoped service is created once per scope, such as one message, and disposed when the scope ends; code opens a scope
with IServiceScopeFactory.CreateScope(). A transient service is created every time something asks for it. A captive
dependency is a long-lived service that holds a shorter-lived one, and the host’s ValidateScopes and ValidateOnBuild
checks report it when the host is built. Keyed services register several implementations of one interface under keys
such as sms and email, and a constructor asks for one with [FromKeyedServices]. A parameter of type IEnumerable<T>
receives every registration of T. An open generic registration serves every closed type of a generic interface, and
a factory registration builds an object in code when a constructor is not enough.
Checkpoint
Check: The Generic Host and dependency injection
Three lifetimes decide when objects are made and disposed Pro: opens with a Pro, Premium or Ultimate pass
In this part:
- A captive dependency passes silently until validation refuses it
- A key picks one implementation, a list takes them all
- Practise: register a notification app
- Interview questions
- A message consumer is registered as a singleton and takes the app's data context, which is registered as scoped. In production, changes from one message sometimes appear in the next, and threading errors show up under load. What is happening, how do you fix it, and how do you keep it from coming back?
- A checkout pays each order by card, bank transfer or wallet, chosen at run time from the order, and new payment methods are added every few months. How would you register and resolve the payment services, and what should happen when an order names a method that is not registered?
- A reporting class takes IServiceProvider in its constructor and calls GetRequiredService in five methods. Tests need a full container, and a missing registration showed up only when a rarely used report ran in production. How would you refactor it, and which uses of IServiceProvider would you keep?
- Key takeaways
1 diagram · 8 runnable examples (C#, Python) · 3 interview answers · about 27 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Terms in this lesson
- captive dependency
- A service with a long lifetime that holds one with a shorter lifetime, such as a singleton that takes a scoped service, so the shorter-lived object lives far longer than it should.
- dependency injection
- Giving an object the objects it depends on, usually as constructor parameters, instead of letting it create them. In .NET a service container reads the registrations and builds each object with its dependencies.
- Generic Host
- The host of Microsoft.Extensions.Hosting: one object that owns an app's services, configuration, logging and lifetime, and starts and stops its hosted services. Host.CreateApplicationBuilder sets one up.
- hosted service
- A class that implements IHostedService, often by deriving from BackgroundService, registered with AddHostedService. The host starts it after the app is built and stops it when the app shuts down.
- keyed service
- A registration with a key, such as "sms" or "email", so one interface can have several implementations that code asks for by key, with [FromKeyedServices] or GetRequiredKeyedService.
- service lifetime
- How long the container keeps a registered service: singleton, one for the whole app; scoped, one per scope; or transient, a new one every time it is asked for.
- service scope
- An IServiceScope, made with IServiceScopeFactory.CreateScope for one unit of work such as a message or a request. Scoped services are made once inside it and disposed when it is disposed.
References
- .NET Generic Host (Microsoft Learn)
- Dependency injection in .NET (Microsoft Learn)
- Service lifetimes (dependency injection) (Microsoft Learn)
- Dependency injection guidelines (Microsoft Learn)
- ServiceProviderOptions.ValidateOnBuild property (Microsoft Learn)
- Breaking change: Fix issues in GetKeyedService() and GetKeyedServices() with AnyKey (Microsoft Learn)
- Microsoft.Extensions.Hosting package (NuGet Gallery (.NET Foundation))
- UPI: product overview (National Payments Corporation of India)
Report a problem with this lesson
Kept only in this browser. Your Learn progress