Serviços de trabalho no .NET

Há inúmeros motivos para criar serviços de execução longa, como:

  • Processamento de dados com uso intensivo de CPU.
  • Enfileiramento de itens de trabalho em segundo plano.
  • Execução de operação cronometrada segundo agendamento.

O processamento de serviço em segundo plano geralmente não envolve uma interface do usuário, mas ela pode ser criada em torno deles. Nos primeiros dias com .NET Framework, os desenvolvedores do Windows poderiam criar os Serviços do Windows por esses motivos. Agora, com o .NET, você pode usar o BackgroundService, que é uma implementação de IHostedService, ou implementar o seu.

Com o .NET, você não está mais restrito ao Windows. Você pode desenvolver serviços multiplataforma em segundo plano. Os serviços hospedados estão prontos para registro em log, configuração e DI (injeção de dependência). Eles fazem parte do conjunto de extensões de bibliotecas, o que significa que são fundamentais para todas as cargas de trabalho do .NET que funcionam com o host genérico.

Importante

Instalar o SDK do .NET também instala o Microsoft.NET.Sdk.Worker e o modelo de trabalho. Em outras palavras, depois de instalar o SDK do .NET, você pode criar um trabalho usando o novo comando de trabalho do dotnet. Se você estiver usando o Visual Studio, o modelo ficará oculto até que a carga de trabalho opcional de ASP.NET e desenvolvimento da Web seja instalada.

Terminologia

Muitos termos são usados erroneamente sinônimos. Nesta seção, há definições para alguns desses termos para tornar sua intenção mais aparente.

  • Serviço em Segundo Plano: refere-se ao tipo BackgroundService.
  • Serviço hospedado: implementações de IHostedService ou referências a IHostedService, em si.
  • Serviço de execução: qualquer serviço que seja executado continuamente.
  • Serviço Windows: a infraestrutura do Serviço Windows, originalmente centrada no .NET Framework, mas agora acessível por meio do .NET.
  • Serviço de Trabalho: refere-se ao modelo do Serviço de Trabalho.

Modelo de serviço de trabalho

O modelo do Serviço de Trabalho está disponível para a CLI do .NET e o Visual Studio. Para obter mais informações, confira CLI do .NET – dotnet new workerModelo. O modelo consiste em uma classe Program e Worker.

using App.WorkerService;

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

IHost host = builder.Build();
host.Run();
using App.WorkerService;

IHostBuilder builder = Host.CreateDefaultBuilder(args)
    .ConfigureServices(services =>
    {
        services.AddHostedService<Worker>();
    });

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

A classe Program anterior:

Dica

Por padrão, o modelo de trabalho não habilita a GC (coleta de lixo) do servidor. Todos os cenários que exigem serviços de execução longa devem considerar as implicações de desempenho desse padrão. Para habilitar o GC do servidor, adicione o nó ServerGarbageCollection ao arquivo de projeto:

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

Para obter mais informações sobre considerações de desempenho, confira a GC do Servidor. Para obter mais informações sobre como configurar o GC do servidor, confira exemplos de configuração do GC do Servidor.

Quanto ao Worker, o modelo fornece uma implementação simples.

namespace App.WorkerService;

public sealed 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)
        {
            _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
            await Task.Delay(1_000, stoppingToken);
        }
    }
}
namespace App.WorkerService;

public sealed 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)
        {
            _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
            await Task.Delay(1_000, stoppingToken);
        }
    }
}

A classe anterior Worker é uma subclasse de BackgroundService, que implementa IHostedService. O BackgroundService é um abstract class e requer que a subclasse implemente BackgroundService.ExecuteAsync(CancellationToken). Na implementação do modelo, ExecuteAsync completa um ciclo por segundo, registrando a data e a hora atuais até que o processo seja sinalizado para cancelar.

O arquivo de projeto

O modelo de trabalho depende do seguinte arquivo de projeto Sdk:

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

Para obter mais informações, confira SDKs de Projeto do .NET

Pacote NuGet

Um aplicativo baseado no modelo de trabalho usa o SDK Microsoft.NET.Sdk.Worker e tem uma referência de pacote explícita ao pacote Microsoft.Extensions.Hosting.

Contêineres e adaptabilidade de nuvem

Com a maioria das cargas de trabalho modernas do .NET, os contêineres são uma opção viável. Ao criar um serviço de execução prolongada por meio do modelo de trabalho no Visual Studio, é possível aceitar o suporte do Docker. Fazer isso cria um Dockerfile que conteineriza seu aplicativo .NET. Um Dockerfile é um conjunto de instruções para compilar uma imagem. Para aplicativos .NET, o Dockerfile geralmente fica na raiz do diretório ao lado de um arquivo de solução.

# 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:7.0 AS base
WORKDIR /app

FROM mcr.microsoft.com/dotnet/sdk:7.0 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"]

As etapas anteriores do Dockerfile incluem:

  • Definir a imagem base de mcr.microsoft.com/dotnet/runtime:7.0 como o alias base.
  • Alterar o diretório de trabalho para /app.
  • Definir o alias build da imagem mcr.microsoft.com/dotnet/sdk:7.0.
  • Alterar o diretório de trabalho para /src.
  • Copiar conteúdos e publicar o aplicativo .NET:
  • Retransmitir a imagem do SDK do .NET por meio de mcr.microsoft.com/dotnet/runtime:7.0 (alias base).
  • Copiar a saída de compilação publicada de /publish.
  • Definir o ponto de entrada, que delega para dotnet App.BackgroundService.dll.
# 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:6.0 AS base
WORKDIR /app

FROM mcr.microsoft.com/dotnet/sdk:6.0 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"]

As etapas anteriores do Dockerfile incluem:

  • Definir a imagem base de mcr.microsoft.com/dotnet/runtime:6.0 como o alias base.
  • Alterar o diretório de trabalho para /app.
  • Definir o alias build da imagem mcr.microsoft.com/dotnet/sdk:6.0.
  • Alterar o diretório de trabalho para /src.
  • Copiar conteúdos e publicar o aplicativo .NET:
  • Retransmitir a imagem do SDK do .NET por meio de mcr.microsoft.com/dotnet/runtime:6.0 (alias base).
  • Copiar a saída de compilação publicada de /publish.
  • Definir o ponto de entrada, que delega para dotnet App.BackgroundService.dll.

Dica

O MCR em mcr.microsoft.com significa "Registro de Contêiner da Microsoft" e é o catálogo de contêineres sindicalizado da Microsoft do hub oficial do Docker. O artigo do catálogo de contêineres dos sindicatos da Microsoft contém detalhes adicionais.

Quando você direciona o Docker como uma estratégia de implantação para o Serviço de Trabalho do .NET, há algumas considerações no arquivo de projeto:

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

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

  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.1" />
    <PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.18.1" />
  </ItemGroup>
</Project>
<Project Sdk="Microsoft.NET.Sdk.Worker">

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

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

No arquivo de projeto anterior, o elemento <DockerDefaultTargetOS> especifica Linux como seu destino. Para direcionar contêineres do Windows, use Windows. O pacote NuGet Microsoft.VisualStudio.Azure.Containers.Tools.Targets é adicionado automaticamente como uma referência de pacote quando o suporte do Docker é selecionado no modelo.

Para obter mais informações sobre o Docker com o .NET, confira Tutorial: colocar em contêiner um aplicativo .NET. Para obter mais informações sobre como implantar no Azure, confira Tutorial: implantar um serviço de trabalho no Azure.

Importante

Para utilizar os segredos do usuário com o modelo de trabalho, é preciso fazer uma referência explícita ao pacote NuGet Microsoft.Extensions.Configuration.UserSecrets.

Extensibilidade do Serviço Hospedado

A interface IHostedService define dois métodos:

Esses dois métodos servem como métodos de ciclo de vida – são chamados durante eventos de início e parada do host, respectivamente.

Observação

Ao substituir os métodos StartAsync ou StopAsync, você deve chamar e await o método de classe base para garantir que o serviço seja iniciado e/ou desligado corretamente.

Importante

A interface serve como uma restrição de parâmetro de tipo genérico no método de extensão, o AddHostedService<THostedService>(IServiceCollection) que significa que apenas implementações são permitidas. Você é livre para usar o BackgroundService fornecido com uma subclasse ou implementar o seu do zero.

Sinalizar conclusão

Na maioria dos cenários comuns, você não precisa sinalizar explicitamente a conclusão de um serviço hospedado. Os serviços são projetados para serem executados desde quando o host os inicia até que o host seja interrompido. No entanto, em alguns cenários, pode ser necessário sinalizar a conclusão de todo o aplicativo host quando o serviço é concluído. Para sinalizar a conclusão, considere a seguinte classe Worker:

namespace App.SignalCompletionService;

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

    public Worker(
        IHostApplicationLifetime hostApplicationLifetime, ILogger<Worker> logger) =>
        (_hostApplicationLifetime, _logger) = (hostApplicationLifetime, logger);

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        // TODO: implement single execution logic here.
        _logger.LogInformation(
            "Worker running at: {time}", DateTimeOffset.Now);

        await Task.Delay(1000, stoppingToken);

        // When completed, the entire app host will stop.
        _hostApplicationLifetime.StopApplication();
    }
}
namespace App.SignalCompletionService;

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

    public Worker(
        IHostApplicationLifetime hostApplicationLifetime, ILogger<Worker> logger) =>
        (_hostApplicationLifetime, _logger) = (hostApplicationLifetime, logger);

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        // TODO: implement single execution logic here.
        _logger.LogInformation(
            "Worker running at: {time}", DateTimeOffset.Now);

        await Task.Delay(1000, stoppingToken);

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

No código anterior, o método ExecuteAsync não faz loop e, quando concluído, chama IHostApplicationLifetime.StopApplication().

Importante

Isso sinalizará ao host que ele deve parar e, sem essa chamada para StopApplication, o host continuará sendo executado indefinidamente.

Para obter mais informações, consulte:

Confira também