Condividi tramite


Servizi di lavoro in .NET

Esistono numerosi motivi per la creazione di servizi a esecuzione prolungata, ad esempio:

  • Elaborazione di dati a elevato utilizzo di CPU.
  • Accodamento degli elementi di lavoro in background.
  • Esecuzione di un'operazione basata sul tempo in base a una pianificazione.

L'elaborazione del servizio in background in genere non comporta un'interfaccia utente, ma le interfacce utente possono essere compilate intorno a esse. Nei primi giorni con .NET Framework, gli sviluppatori Windows potrebbero creare servizi Windows per questi scopi. Ora con .NET è possibile usare l'BackgroundService, che è un'implementazione di IHostedServiceo implementare il proprio.

Con .NET non si è più limitati a Windows. È possibile sviluppare servizi multipiattaforma in background. I servizi ospitati sono abilitati per il logging, la configurazione e l'iniezione di dipendenze (DI). Fanno parte della suite di librerie delle estensioni, ovvero sono fondamentali per tutti i carichi di lavoro .NET che funzionano con l'host generico .

Importante

L'installazione di .NET SDK installa anche il Microsoft.NET.Sdk.Worker e il template di lavoro. In altre parole, dopo aver installato .NET SDK, è possibile creare un nuovo ruolo di lavoro usando il comando dotnet new worker. Se si usa Visual Studio, il modello viene nascosto fino a quando non viene installato il carico di lavoro facoltativo ASP.NET e sviluppo Web.

Terminologia

Molti termini vengono usati erroneamente come sinonimi. Questa sezione definisce alcuni di questi termini per renderli più evidenti in questo articolo.

  • Servizio in background: il tipo di BackgroundService.
  • servizio ospitato: implementazioni di IHostedServiceo del IHostedService stesso.
  • servizio a esecuzione prolungata: Qualsiasi servizio eseguito in maniera continuativa.
  • servizio Windows: l'infrastruttura del servizio Windows, in origine incentrata su .NET Framework, ma ora accessibile tramite .NET.
  • Servizio lavoratore: Il modello Servizio lavoratore.

Modello di servizio di lavoro

Il modello del servizio di lavoro è disponibile nell'interfaccia della riga di comando di .NET e in Visual Studio. Per altre informazioni, vedere - interfaccia della riga di comando di .NET, dotnet new worker - modello. Il modello è costituito da una classe Program e Worker.

using App.WorkerService;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddHostedService<Worker>();

IHost host = builder.Build();
host.Run();

Classe Program precedente:

Impostazioni predefinite del modello

Il modello Worker non abilita il Garbage Collection (GC) del server per impostazione predefinita, poiché esistono numerosi fattori che svolgono un ruolo nella determinazione della necessità. Gli scenari che richiedono servizi di lunga durata devono considerare le implicazioni sulle prestazioni di questa impostazione predefinita. Per abilitare il server GC, aggiungere il nodo ServerGarbageCollection al file di progetto:

<PropertyGroup>
    <ServerGarbageCollection>true</ServerGarbageCollection>
</PropertyGroup>

compromessi e considerazioni

Abilitato Disabile
Gestione efficiente della memoria: recupera automaticamente la memoria inutilizzata per evitare perdite di memoria e ottimizzare l'utilizzo delle risorse. Miglioramento delle prestazioni in tempo reale: evita potenziali pause o interruzioni causate da Garbage Collection nelle applicazioni sensibili alla latenza.
Stabilità a lungo termine: consente di mantenere prestazioni stabili nei servizi a esecuzione prolungata gestendo la memoria in periodi prolungati. Efficienza delle risorse: può risparmiare risorse di CPU e memoria in ambienti con vincoli di risorse.
Manutenzione ridotta: riduce al minimo la necessità di gestione manuale della memoria, semplificando la manutenzione. Controllo della memoria manuale: fornisce un controllo granulare sulla memoria per applicazioni specializzate.
Comportamento prevedibile: contribuisce al comportamento coerente e prevedibile dell'applicazione. Adatto per processi di breve durata: riduce al minimo il sovraccarico della raccolta dei rifiuti per processi effimeri o di breve durata.

Per altre informazioni sulle considerazioni sulle prestazioni, vedere Server GC. Per ulteriori informazioni sulla configurazione del server GC, vedere esempi di configurazione del server GC.

Classe di lavoro

Per quanto riguarda il Worker, il modello fornisce un'implementazione semplice.

namespace App.WorkerService;

public sealed class Worker(ILogger<Worker> logger) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
            await Task.Delay(1_000, stoppingToken);
        }
    }
}

La classe Worker precedente è una sottoclasse di BackgroundService, che implementa IHostedService. Il BackgroundService è un abstract class e richiede la sottoclasse per implementare BackgroundService.ExecuteAsync(CancellationToken). Nell'implementazione del modello, il ExecuteAsync esegue un ciclo una volta al secondo, registrando la data e l'ora correnti fino a quando il processo non viene segnalato per annullare.

File di progetto

Il modello worker si basa sul file di progetto seguente Sdk:

<Project Sdk="Microsoft.NET.Sdk.Worker">

Per altre informazioni, vedere SDK di progetto .NET.

Pacchetto NuGet

Un'app basata sul modello Worker usa il Microsoft.NET.Sdk.Worker SDK e ha un riferimento esplicito al pacchetto Microsoft.Extensions.Hosting.

Contenitori e adattabilità nel cloud

Con la maggior parte dei carichi di lavoro .NET moderni, i contenitori sono un'opzione praticabile. Quando si crea un servizio a esecuzione prolungata dal modello di ruolo di lavoro in Visual Studio, è possibile acconsentire esplicitamente al supporto di Docker. In questo modo viene creato un Dockerfile che crea un contenitore per l'app .NET. Un Dockerfile è un set di istruzioni per compilare un'immagine. Per le app .NET, il Dockerfile in genere si trova nella radice della directory accanto a un file di soluzione.

# See https://aka.ms/containerfastmode to understand how Visual Studio uses this
# Dockerfile to build your images for faster debugging.

FROM mcr.microsoft.com/dotnet/runtime:8.0@sha256:e6b552fd7a0302e4db30661b16537f7efcdc0b67790a47dbf67a5e798582d3a5 AS base
WORKDIR /app

FROM mcr.microsoft.com/dotnet/sdk:8.0@sha256:35792ea4ad1db051981f62b313f1be3b46b1f45cadbaa3c288cd0d3056eefb83 AS build
WORKDIR /src
COPY ["background-service/App.WorkerService.csproj", "background-service/"]
RUN dotnet restore "background-service/App.WorkerService.csproj"
COPY . .
WORKDIR "/src/background-service"
RUN dotnet build "App.WorkerService.csproj" -c Release -o /app/build

FROM build AS publish
RUN dotnet publish "App.WorkerService.csproj" -c Release -o /app/publish

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "App.WorkerService.dll"]

I passaggi precedenti Dockerfile includono:

  • Impostazione dell'immagine di base da mcr.microsoft.com/dotnet/runtime:8.0 come alias base.
  • Cambiamento della directory di lavoro a /app.
  • Impostazione dell'alias build dall'immagine mcr.microsoft.com/dotnet/sdk:8.0.
  • Modifica della directory di lavoro in /src.
  • Copia del contenuto e pubblicazione dell'app .NET:
  • Inoltro dell'immagine .NET SDK da mcr.microsoft.com/dotnet/runtime:8.0 (alias base).
  • Copiare l'output della compilazione pubblicato dalla cartella /publish.
  • Definendo il punto di ingresso, che viene delegato a dotnet App.BackgroundService.dll.

Consiglio

McR in mcr.microsoft.com è l'acronimo di "Microsoft Container Registry" ed è il catalogo dei contenitori diffuso di Microsoft dall'hub Docker ufficiale. L'articolo del catalogo dei contenitori sindacato da Microsoft contiene ulteriori dettagli.

Quando si usa Docker come strategia di distribuzione per il servizio di lavoro .NET, nel file di progetto sono presenti alcune considerazioni:

<Project Sdk="Microsoft.NET.Sdk.Worker">

  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>true</ImplicitUsings>
    <RootNamespace>App.WorkerService</RootNamespace>
    <DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.6" />
    <PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.21.0" />
  </ItemGroup>
</Project>

Nel file di progetto precedente, l'elemento <DockerDefaultTargetOS> specifica Linux come destinazione. Per impostare come destinazione i contenitori di Windows, usare invece Windows. Quando il supporto Docker Microsoft.VisualStudio.Azure.Containers.Tools.Targets viene selezionato dal modello, il pacchetto NuGet viene aggiunto automaticamente come riferimento al pacchetto.

Per altre informazioni su Docker con .NET, vedere Guida: trasformare un'app .NET in un container. Per altre informazioni sulla distribuzione in Azure, vedere Esercitazione: Distribuire un servizio di lavoro in Azure.

Importante

Se si vuole sfruttare segreti utente con il modello Worker, è necessario fare riferimento in modo esplicito al pacchetto NuGet Microsoft.Extensions.Configuration.UserSecrets.

Estendibilità del servizio ospitato

L'interfaccia IHostedService definisce due metodi:

Questi due metodi fungono da metodi ciclo di vita e vengono chiamati durante gli eventi di avvio e arresto dell'host, rispettivamente.

Nota

Per garantire che il servizio inizi e/o si arresti correttamente, quando si esegue l'override dei metodi StartAsync o StopAsync, è necessario chiamare e await il metodo di classe base.

Importante

L'interfaccia funge da vincolo di parametro di tipo generico nel metodo di estensione AddHostedService<THostedService>(IServiceCollection), ovvero sono consentite solo le implementazioni. È possibile usare il BackgroundService fornito con una sottoclasse o implementare completamente il proprio.

Completamento del segnale

Negli scenari più comuni non è necessario segnalare in modo esplicito il completamento di un servizio ospitato. Quando l'host avvia i servizi, questi sono progettati per funzionare fino a quando l'host non viene arrestato. In alcuni scenari, tuttavia, potrebbe essere necessario segnalare il completamento dell'intera applicazione host al termine del servizio. Per segnalare il completamento, considerare la classe Worker seguente:

namespace App.SignalCompletionService;

public sealed class Worker(
    IHostApplicationLifetime hostApplicationLifetime,
    ILogger<Worker> logger) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        // TODO: implement single execution logic here.
        logger.LogInformation(
            "Worker running at: {Time}", DateTimeOffset.Now);

        await Task.Delay(1_000, stoppingToken);

        // When completed, the entire app host will stop.
        hostApplicationLifetime.StopApplication();
    }
}

Nel codice precedente il metodo BackgroundService.ExecuteAsync(CancellationToken) non esegue il ciclo e al termine chiama IHostApplicationLifetime.StopApplication().

Importante

Questo segnalerà all'host che deve arrestarsi e, senza questa chiamata a StopApplication, l'host continuerà a funzionare indefinitamente. Se si prevede di eseguire un servizio ospitato di breve durata (scenario eseguito una sola volta) e si vuole usare il modello di lavoro, è necessario chiamare StopApplication per segnalare l'arresto dell'host.

Per altre informazioni, vedere:

Approccio alternativo

Per un'app di breve durata che richiede inserimento, registrazione e configurazione delle dipendenze, usare l'host generico .NET anziché il modello Worker. In questo modo è possibile usare queste funzionalità senza la Worker classe . Un semplice esempio di un'app di breve durata che usa l'host generico potrebbe definire un file di progetto simile al seguente:

<Project Sdk="Microsoft.NET.Sdk.Worker">

  <PropertyGroup>
    <TargetFramework>net9.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>true</ImplicitUsings>
    <RootNamespace>ShortLived.App</RootNamespace>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.6" />
  </ItemGroup>
</Project>

È possibile che la Program classe sia simile alla seguente:

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddSingleton<JobRunner>();

using var host = builder.Build();

try
{
    var runner = host.Services.GetRequiredService<JobRunner>();

    await runner.RunAsync();

    return 0; // success
}
catch (Exception ex)
{
    var logger = host.Services.GetRequiredService<ILogger<Program>>();
    
    logger.LogError(ex, "Unhandled exception occurred during job execution.");

    return 1; // failure
}

Il codice precedente crea un JobRunner servizio, ovvero una classe personalizzata che contiene la logica per l'esecuzione del processo. Il RunAsync metodo viene chiamato su JobRunnere, se viene completato correttamente, l'app restituisce 0. Se si verifica un'eccezione non gestita, registra l'errore e restituisce 1.

In questo scenario semplice, la JobRunner classe potrebbe essere simile alla seguente:

using Microsoft.Extensions.Logging;

internal sealed class JobRunner(ILogger<JobRunner> logger)
{
    public async Task RunAsync()
    {
        logger.LogInformation("Starting job...");

        // Simulate work
        await Task.Delay(1000);

        // Simulate failure
        // throw new InvalidOperationException("Something went wrong!");

        logger.LogInformation("Job completed successfully.");
    }
}

Ovviamente è necessario aggiungere logica reale al RunAsync metodo , ma questo esempio dimostra come usare l'host generico per un'app di breve durata senza la necessità di una Worker classe e senza la necessità di segnalare in modo esplicito il completamento dell'host.

Vedere anche