Miroslav Holec
Premium

Nový generic host v .NET Core 3.0

Miroslav Holec   19. září 2019

Článek se vztahuje k verzi produktu .NET Core 3.0

Tento článek je již zastaralý. Článek nemusí popisovat aktuální stav technologie, ideální řešení a můj současný pohled na dané téma.

Šablony webových projektů ASP.NET Core 3.0 používají nově tzv. HostBuilder pro výchozí konfiguraci aplikace. Zatímco původní IWebHostBuilder měl omezené použití pouze pro účely webových aplikací, nový IHostBuilder lze použít i pro další druhy aplikací, jako například Worker Service.

K čemu je HostBuilder

Třída HostBuilder umožňuje konfiguraci základních komponent aplikace, mezi které patří zejména konfigurační nastavení, dependency injection a logování. Toto nastavení je provedeno při startu aplikace ve statické metodě Main().

Konfigurace WorkerService

WorkerService je .NET Core aplikace, určená pro odbavení dlouhotrvajících úloh (i jednorázových). Příkladem může být kontinuální běh a odbavování požadavků z fronty zpráv. Výchozí nastavení takové služby vypadá následovně:

Host.CreateDefaultBuilder(args)
    .ConfigureServices((hostContext, services) =>
    {
        services.AddHostedService<Worker>();
    })
    .Build()
    .Run();

Klíčové je volání CreateDefaultBuilder(), které na pozadí provede výchozí konfiguraci aplikace. Součástí takové konfigurace je registrace klíčových služeb do kontejneru, podpora logování do debug window + konzole + event logu OS a samozřejmě načtení konfigurace ze souborů appsettings.json + appsettings.[environment].json + konfigurace z proměnných prostředí nebo argumentů předaných do programu. Později v tomto článku je výchozí nastavení naznačeno kódem.

Pro worker service je signifikantní přidání tzv. hosted service do DI pomocí services.AddHostedService. HostedServices nejsou nic nového a již v minulosti jste je mohli používat společně s ASP.NET Core webovými aplikacemi.

Konfigurace webové aplikace

Díky generickému chování lze velmi podobně nastavit i webovou aplikaci.

Host.CreateDefaultBuilder(args)
    .ConfigureWebHostDefaults(builder =>
    {
        builder.UseStartup<Startup>();
    })
    .Build()
    .Run();

V případě webové aplikace je zde nepatrný rozdíl, protože webová aplikace je specifická. Vyžaduje konfiguraci tzv. request pipeline, aby bylo jasné, jak mají být odbaveny požadavky proti webovému serveru. Proto je zde ještě metoda ConfigureWebHostDefaults(), která zajistí výchozí nastavení webového serveru. Konfigurace request pipeline pak probíhá v metodě Configure(), která je odsunuta pro přehlednost do třídy Startup.cs. Ta je registrována pomocí metody UseStartup().

Ačkoliv by i zde mohla být použita metoda ConfigureServices() přímo nad hostem, typicky se tato metoda taktéž přesouvá do Startup.cs, aby bylo vše pospolu.

Generická konfigurace

V praxi lze tedy generický host použít pro konfiguraci jak konzolových, tak webových aplikací. Pro ilustraci zkombinujeme dva příklady z článku a spustíme nejen webovou aplikaci, ale i souběžnou worker service. Místo třídy Startup.cs navíc použijeme přímo provolání metod Configure() a ConfigureServices() nad tradičním WebHost builderem.

Nastavení by mohlo vypadat takto:

await Host.CreateDefaultBuilder(args)
    .ConfigureWebHostDefaults(webHostBuilder =>
    {
        webHostBuilder
            .Configure(applicationBuilder =>
            {
                applicationBuilder.UseRouting();
                applicationBuilder.UseEndpoints(endpointBuilder =>
                {
                    endpointBuilder.MapControllers(); 

                });
            });

        webHostBuilder.ConfigureServices(collection =>
        {
            collection.AddControllers(); 
        });
    })
    .ConfigureServices(collection =>
    {
        collection.AddHostedService<Worker>(); 
    })
    .ConfigureAppConfiguration((host, builder) =>
    {
        builder.AddJsonFile("appsettings.json");
        builder.AddJsonFile($"appsettings.{host.HostingEnvironment.EnvironmentName}.json");
        builder.AddEnvironmentVariables();
        builder.AddCommandLine(args);
    })
    .ConfigureLogging((host, builder) =>
    {
        builder.AddConfiguration(host.Configuration);
        builder.AddDebug();
        builder.AddConsole();
        builder.AddEventSourceLogger();
    })
    .Build()
    .RunAsync();

Jak si můžete všimnout, máme generické nastavení konfigurace pro oba typy aplikací, společné logování i registraci služeb do DI. Pro webový host nicméně máme dodatečnou konfiguraci routování v metodě Configure().

Application Startup

Co se při takové konfiguraci děje po startu aplikace?

  • Provede se výchozí nastavení host prostředí. Díky tomu se zjistí název prostředí (development/production), obvykle s environment variables + command line args.
  • Provede se ConfigureAppConfiguration(), protože již známe prostředí a konfiguraci budeme potřebovat v dalších krocích.
  • Provede se metoda ConfigureServices() pro webhost. Vyplývá z toho, že v ní již máte dostupné veškeré konfigurace z bodu 2.
  • Provede se metoda ConfigureServices() pro generic host.
  • Provede se metoda ConfigureLogging() pro generic host.
  • Provede se konfigurace request pipeline provoláním Configure() metody ve webhost.
  • Konečně se provede Build() generic hostu a v případě úspěchu i Run().
  • Spustí se StartAsync() pro registrovanou Hosted Service a také se spustí webový server na vybraném portu.

Co to znamená pro vývojáře

Úžasnou věc! Když budete chtít vytvořit konzolovku s podporou DI, logování a konfigurace, můžete nyní použít generic host a registrovat si vlastní hosted service, která zasupluje to, co jste doposud měli v metodě Main(). Klíčová výhoda je v tom, že zmíněná hosted service již plně podporuje DI a můžete si do ní injectnout služby dle potřeby. Navíc takových hosted services můžete spustit hned několik souběžně.

class Program
{
    static void Main(string[] args)
    {
        Host.CreateDefaultBuilder()
            .ConfigureServices(collection => collection.AddHostedService<Worker>())
            .Build()
            .Run();
......

public class Worker : IHostedService
    {
        private readonly ILogger<Worker> logger;

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

        public async Task StartAsync(CancellationToken cancellationToken)
        {
            logger.LogInformation(nameof(StartAsync) + " runs");
            Console.WriteLine("Hello World!");
        }

Výsledkem bude v konzoli:

Generic Host Worker Service