Hosting β Usage Guide
π Language: English | EspaΓ±ol
Contents
- Architecture Overview
- Building a Host
- Configuration Pipeline
- Sharing data between startup pieces
- Registering Services
- Hosted Services (Background Workers)
- Startup Tasks
- Environment
- Application Lifetime
- Accessing Services After Build
- Graceful Shutdown
- AppModule Pattern with Hosting
Architecture Overview
THost.CreateBuilder βββΊ IGenericHostBuilder
βββ Configuration: IConfigurationManager β sources, and reading them back
βββ Environment: IHostEnvironment β resolved when read, derived from host config
βββ Services: IServiceCollection β DI registrations
βββ Logging: ILoggingBuilder β only composed if you touch it
βββ Properties β data the startup pieces share
βββ Build βββΊ IHost
βββ Services: IServiceProvider
βββ IHostEnvironment
βββ IHostApplicationLifetime
βββ [IHostedService, ...]
The host is built once, started once, and shut down once. All services are managed by the DI container.
Building a Host
uses Daf.Hosting, Daf.Extensions.Hosting, Daf.Extensions.DependencyInjection;
var Builder := THost.CreateBuilder;
Builder.Services.AddSingleton<IMyService, TMyService>;
var Host := Builder.Build;
try
Host.Start;
Host.WaitForShutdown; // blocks until shutdown signal
finally
StopAndNil(Host);
end;THost.CreateBuilder hands back an IGenericHostBuilder, so a single variable owns the builder and reaches Build. Each builder is independent: several builders and several hosts can be alive at once in one process.
Build can only be called once per builder; it freezes configuration and creates the DI container.
StopAndNil(Host) stops the host and leaves the variable empty, like FreeAndNil with an object: stopping releases the host, and a variable still pointing at it touches returned memory when it goes out of scope.
Configuration Pipeline
There is one configuration, and it is populated the first time it is read. Builder.Configuration is an IConfigurationManager β a builder and a reader at the same time β so a source can be chosen from what is already configured.
1. What the builder seeds
The first time the composition reads Configuration or Environment β or at Build, if it reads neither β the builder loads the sources Host.CreateApplicationBuilder loads, in this order, each one winning over the ones before it:
- the process environment variables prefixed
DAF_, with the prefix stripped βDAF_ENVIRONMENTsetsenvironmentβ and, if it was given arguments, the command line; appsettings.json,appsettings.{Environment}.json,{ApplicationName}.settings.jsonand{ApplicationName}.settings.{Environment}.jsonfrom the content root, all optional; a path separator in the application name becomes_;- every environment variable, with
__as the section separator:Db__ServerisDb:Server; - the command line, again.
var Builder := THost.CreateBuilder(CommandLineConfig.ProcessArgs);Those first sources are where the environment comes from, through the keys .NET uses and THostDefaults names: environment, applicationName and contentRoot. So --environment Staging and DAF_ENVIRONMENT=Staging say the same thing, and the argument wins.
The content root is the current directory, as in .NET, unless contentRoot names another one; a relative one is resolved against the folder of the executable. The exception is the system directory, which is where a Windows service starts: there the content root is the folder of the executable. Environment.BinPath is always that folder, whatever the working directory.
The files are read once, at that first read: a change to them is not picked up while the process runs, and creating the builder opens none of them.
A default file that cannot be read β a malformed appsettings.json β is reported through the startup channel when it is opened, and the read goes on with the other sources; Build then fails with that same exception. A source you add yourself fails when you add it, in your code.
2. The environment, already resolved
Builder.Environment is derived from that seed as it loads. That is what lets the next step depend on it: there is no phase to wait for.
3. Your own sources
Add them where you stand, between the builder and Build. They come after the defaults, so they win over them, the command line included, as in .NET:
JsonConfig.AddFile(Builder.Configuration,
TPath.Combine(Builder.Environment.ContentRootPath, 'features.json'), [csoOptional]);And because the manager reads as well as builds, a registration consumes configuration on the spot:
var Db := Builder.Configuration['Database:ConnectionString'];
Builder.Services.AddSingleton<IDatabase>(TDatabase.Create(Db));That is what the deferred callbacks existed for, and why there are none: a configuration that can be read while it is being composed leaves nothing to defer.
Build freezes it. From there on the same configuration is what IHostBuilderContext.Configuration hands out.
Registering Services
The full IServiceCollection API is available on Builder.Services:
var Services := Builder.Services;
// Lifetimes
Services.AddSingleton<ICache, TMemoryCache>;
Services.AddScoped<IUnitOfWork, TDbUnitOfWork>;
Services.AddTransient<IEmailSender, TSmtpSender>;
// Hosted service (started/stopped with the host)
Services.AddHostedService<TBackgroundIndexer>;See DependencyInjection for the full registration reference.
Hosted Services (Background Workers)
Implement IHostedService for any work that should run alongside the host:
type
TOrderProcessor = class(TInterfacedObject, IHostedService)
private
FTimer: TTimer; // or use TThread, etc.
public
procedure Start;
procedure Stop;
end;
procedure TOrderProcessor.Start;
begin
// set up periodic work, open connections, etc.
end;
procedure TOrderProcessor.Stop;
begin
// flush, close, clean up
end;Register:
Services.AddHostedService<TOrderProcessor>;Startis called for each hosted service whenHost.Startruns, in registration order.Stopis called in the reverse order during shutdown β the order the container releases in β, so a service stops before the ones registered ahead of it, which it may be using.AddHostedService<T>registersTunderIHostedServiceand nothing else. To reach the same instance asIOrderProcessor, register that too and have the hosted-service registration resolve it.
Startup Tasks
An IStartupTask runs once at startup, with the provider already built. It has a Run and nothing to stop: it is initialization, not a service with a lifetime.
type
TWarmUpCache = class(TInterfacedObject, IStartupTask)
public
procedure Run;
end;
procedure TWarmUpCache.Run;
begin
// read what the application needs before it serves anything
end;Register it on the collection:
Services.AddStartupTask<TWarmUpCache>;THost.Start runs the tasks in registration order, before every IHostedService and before ApplicationStarted β a hosted service may depend on the state a task prepares, so the order is the point and not an accident.
A task that raises aborts the startup. The exception is reported and re-raised, so no hosted service starts and the application is never declared started. That is what makes a startup task the place for a precondition: a cache that cannot be filled, a folder that is not writable, a schema that is not the expected one.
When the helper is shadowed
Only one record helper per type is visible in Delphi, so a unit that brings a second helper for IServiceCollection hides this one. The class facade is the way through, and it takes the implementation as a metaclass:
Hosting.AddStartupTask(Services, TWarmUpCache);It is the same registration: a package contributes its own initialization with it without the .dpr naming the task β see PackageInit.
Choosing between an IStartupTask and an IHostedService is decided in Startup composition.
Environment
The IHostEnvironment is pre-registered as a Singleton. Access it anywhere via the DI container:
var Env := Host.Services.GetRequiredService<IHostEnvironment>;
Env.ApplicationName // e.g. 'MyApp'
Env.EnvironmentName // e.g. 'Development'
Env.ContentRootPath // e.g. 'C:\MyApp\'
Env.BinPath // always the binary directory
Env.IsDevelopment // True in Development
Env.IsProduction // True in Production
Env.IsTesting // True in Testing (useful for test runners)Set environment via OS variable before launching:
DAF_ENVIRONMENT=Development
Any other name, such as QA, is one more environment, with its own appsettings.QA.json. The Is⦠questions compare the name without regard to case.
In Development the host also validates the container when it builds it: a registration whose dependencies cannot be satisfied, or a Singleton that captures a Scoped, stops Build with every finding listed, and a Scoped asked of the root provider is rejected when it is resolved. ServiceProviderOptions on the builder fixes that choice whatever the environment β see Container validation in the module README.
You can also read arbitrary environment variables:
var DbHost := Env['MY_DB_HOST'];Application Lifetime
IHostApplicationLifetime publishes three cancellation tokens you can react to:
var Lifetime := Services.GetRequiredService<IHostApplicationLifetime>;
// Fired after all hosted services have started
Lifetime.ApplicationStarted.Register(procedure begin
Logger.LogInformation('Application ready');
end);
// Fired when a shutdown has been requested (before services stop)
Lifetime.ApplicationStopping.Register(procedure begin
Logger.LogInformation('Shutdown initiated');
end);
// Fired after all hosted services have stopped
Lifetime.ApplicationStopped.Register(procedure begin
Logger.LogInformation('Application stopped');
end);
// Request a graceful shutdown programmatically
Lifetime.StopApplication;StopApplication triggers the same shutdown sequence as Ctrl+C / SIGTERM.
Accessing Services After Build
var Host := Builder.Build;
try
Host.Start;
// Access services from outside the DI graph
var Svc := Host.Services.GetRequiredService<IMyService>;
Svc.Execute;
Host.WaitForShutdown;
finally
StopAndNil(Host);
end;For scoped operations (e.g., a background job that needs a unit of work):
var Scope := Host.Services.CreateScope;
try
var UoW := Scope.ServiceProvider.GetRequiredService<IUnitOfWork>;
UoW.DoWork;
UoW.Commit;
finally
Scope := nil;
end;Graceful Shutdown
Host.WaitForShutdown blocks until the process receives a shutdown signal (Ctrl+C, SIGTERM, or Lifetime.StopApplication). Host.Stop runs the shutdown sequence:
IHostApplicationLifetime.ApplicationStoppingcallbacks fireIHostedService.Stopcalled for each service, in reverse registration orderIHostApplicationLifetime.ApplicationStoppedcallbacks fire- The DI container releases each Singleton once nothing but itself holds it, so a service goes after everything that depends on it
A step that fails does not stop the ones after it. A hosted service whose Stop raises is reported through the logger when it happens, and the rest of the sequence still runs. Host.Stop raises what failed at the end, with the host already released:
- one failure: that same exception, unwrapped;
- several: an
EAggregateException(unitSystem.Threading) holding all of them, whose message says how many steps failed.
StopAndNil(Host) leaves the variable empty also when Stop raises, and raises the same. It empties that variable only: a copy of the reference kept elsewhere still points at the released host. Release the copy in ApplicationStopped, which fires with the host still alive β the HostLifetime sample keeps one and releases it there.
Freeing a host that was never stopped runs the same sequence once. What fails there is reported, and the destructor does not raise it.
Stop reaches every hosted service of a host whose Start began, also those that did not start or whose Start raised, so a serviceβs Stop tolerates that its Start never ran. A host that was never started notifies the shutdown and closes the container without calling any serviceβs Stop, so Host.Start goes inside the try, as in the examples above.
WaitForShutdown integrates with the OS signal handler via TShutdownHook.
AppModule Pattern with Hosting
Split registrations across modules for large apps:
uses Daf.AppModule, Daf.Extensions.DependencyInjection;
type
TInfraModule = class(TInterfacedObject, IAppModule)
public
procedure AddServices(const Services: IServiceCollection);
end;
procedure TInfraModule.AddServices(const Services: IServiceCollection);
begin
Services.AddSingleton<IDatabase, TPostgresDatabase>;
Services.AddHostedService<TBackgroundWorker>;
end;Load the modules on Builder.Services, one AddModule<T> per module:
Builder.Services
.AddModule<TInfraModule>
.AddModule<TDomainModule>;AddModule<T> instantiates the module, calls its AddServices on the spot, and owns its lifetime. Do not call TInfraModule.Create.AddServices(Services) by hand: the module is an TInterfacedObject created with a reference count of zero, so nobody frees it.
When a module needs configuration, construct it yourself and pass the instance: Services.AddModule(TInfraModule.Create(...)). Handing it over as an IAppModule is what gives it an owner, and it is the only way to pass a module any configuration.
There is no automatic discovery. AddModules(Package), the RTTI scan for IAppModule, was removed: it hid the declaration in a uses clause, left startup silently blind to a trimmed RTTI, and put the registration order in the hands of reflection. No variant of the application family registers a module on its own either β its modules are declared wherever the services are composed, exactly as above. See Startup composition for the two phases and the ordering rules.