Creación de un servicio de Windows mediante BackgroundService

Los desarrolladores de .NET Framework probablemente estén familiarizados con las aplicaciones de servicio de Windows. Antes de .NET Core y .NET 5 y versiones posteriores, los desarrolladores que dependían de .NET Framework podían crear servicios de Windows para realizar tareas en segundo plano o ejecutar procesos de larga duración. Esta funcionalidad sigue estando disponible; por tanto, puede crear servicios Worker que se ejecuten como un servicio de Windows.

En este tutorial, aprenderá a:

  • Publicar una aplicación del servicio Worker de .NET como un archivo ejecutable único
  • Crear un servicio de Windows
  • Crear la aplicación BackgroundService como un servicio de Windows
  • Iniciar y detener el servicio de Windows
  • Ver los registros de eventos
  • Eliminar el servicio de Windows

Sugerencia

Todo el ejemplo de "Trabajos en .NET" está disponible en el Explorador de ejemplos para su descarga. Para obtener más información, consulte Examinación de ejemplos de código: Trabajos en .NET.

Importante

La instalación del SDK de .NET también instala Microsoft.NET.Sdk.Worker y la plantilla de trabajo. Es decir, después de instalar el SDK de .NET, puede crear un nuevo trabajo (“new worker”) mediante el comando dotnet new worker. Si usa Visual Studio, la plantilla se oculta hasta que se instala la carga de trabajo opcional de ASP.NET y desarrollo web.

Requisitos previos

Creación de un nuevo proyecto

Para crear un proyecto de Worker Service con Visual Studio, seleccione Archivo>Nuevo>Proyecto... . En el cuadro de diálogo Crear un proyecto, busque "Worker Service" y seleccione la plantilla Worker Service. Si prefiere usar la CLI de .NET, abra su terminal favorito en un directorio de trabajo. Ejecute el comando dotnet new y reemplace <Project.Name> por el nombre del proyecto deseado.

dotnet new worker --name <Project.Name>

Para más información sobre el comando del nuevo proyecto de Worker Service de la CLI de .NET, vea dotnet new worker.

Sugerencia

Si usa Visual Studio Code, puede ejecutar comandos de la CLI de .NET desde el terminal integrado. Para más información, vea Visual Studio Code: terminal integrado.

Instalación de paquetes NuGet

Para interoperar con servicios de Windows desde implementaciones de IHostedService de .NET, deberá instalar el paquete NuGet Microsoft.Extensions.Hosting.WindowsServices.

Para instalarlo desde Visual Studio, use el cuadro de diálogo Administrar paquetes NuGet... . Busque "Microsoft.Extensions.Hosting.WindowsServices" e instálelo. Si prefiere usar la CLI de .NET, ejecute el comando dotnet add package:

dotnet add package Microsoft.Extensions.Hosting.WindowsServices

Para más información sobre el comando add package (“agregar paquete”) de la CLI de .NET, consulte dotnet add package.

Después de agregar correctamente los paquetes, el archivo de proyecto ahora debería contener las siguientes referencias de paquetes:

<ItemGroup>
  <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
  <PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="8.0.0" />
</ItemGroup>

Actualización del archivo del proyecto

Este proyecto de trabajo usa los tipos de referencia que aceptan valores NULL de C#. Para habilitarlos para todo el proyecto, actualice el archivo del proyecto en consecuencia:

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

  <PropertyGroup>
    <TargetFramework>net8.0-windows</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>true</ImplicitUsings>
    <RootNamespace>App.WindowsService</RootNamespace>
  </PropertyGroup>

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

Los cambios del archivo del proyecto anterior agregan el nodo <Nullable>enable<Nullable>. Para obtener más información, vea Establecimiento del contexto que acepta valores NULL.

Creación del servicio

Agregue una nueva clase al proyecto denominado JokeService.cs y reemplace su contenido por el siguiente código de C#:

namespace App.WindowsService;

public sealed class JokeService
{
    public string GetJoke()
    {
        Joke joke = _jokes.ElementAt(
            Random.Shared.Next(_jokes.Count));

        return $"{joke.Setup}{Environment.NewLine}{joke.Punchline}";
    }

    // Programming jokes borrowed from:
    // https://github.com/eklavyadev/karljoke/blob/main/source/jokes.json
    private readonly HashSet<Joke> _jokes = new()
    {
        new Joke("What's the best thing about a Boolean?", "Even if you're wrong, you're only off by a bit."),
        new Joke("What's the object-oriented way to become wealthy?", "Inheritance"),
        new Joke("Why did the programmer quit their job?", "Because they didn't get arrays."),
        new Joke("Why do programmers always mix up Halloween and Christmas?", "Because Oct 31 == Dec 25"),
        new Joke("How many programmers does it take to change a lightbulb?", "None that's a hardware problem"),
        new Joke("If you put a million monkeys at a million keyboards, one of them will eventually write a Java program", "the rest of them will write Perl"),
        new Joke("['hip', 'hip']", "(hip hip array)"),
        new Joke("To understand what recursion is...", "You must first understand what recursion is"),
        new Joke("There are 10 types of people in this world...", "Those who understand binary and those who don't"),
        new Joke("Which song would an exception sing?", "Can't catch me - Avicii"),
        new Joke("Why do Java programmers wear glasses?", "Because they don't C#"),
        new Joke("How do you check if a webpage is HTML5?", "Try it out on Internet Explorer"),
        new Joke("A user interface is like a joke.", "If you have to explain it then it is not that good."),
        new Joke("I was gonna tell you a joke about UDP...", "...but you might not get it."),
        new Joke("The punchline often arrives before the set-up.", "Do you know the problem with UDP jokes?"),
        new Joke("Why do C# and Java developers keep breaking their keyboards?", "Because they use a strongly typed language."),
        new Joke("Knock-knock.", "A race condition. Who is there?"),
        new Joke("What's the best part about TCP jokes?", "I get to keep telling them until you get them."),
        new Joke("A programmer puts two glasses on their bedside table before going to sleep.", "A full one, in case they gets thirsty, and an empty one, in case they don’t."),
        new Joke("There are 10 kinds of people in this world.", "Those who understand binary, those who don't, and those who weren't expecting a base 3 joke."),
        new Joke("What did the router say to the doctor?", "It hurts when IP."),
        new Joke("An IPv6 packet is walking out of the house.", "He goes nowhere."),
        new Joke("3 SQL statements walk into a NoSQL bar. Soon, they walk out", "They couldn't find a table.")
    };
}

readonly record struct Joke(string Setup, string Punchline);

El código fuente del servicio de chistes anterior expone una única funcionalidad, el método GetJoke. Se trata de un método de devolución string, que representa un chiste sobre programación aleatorio. El campo _jokes con ámbito de clase se usa para almacenar la lista de chistes. Se selecciona un chiste aleatorio de la lista y se devuelve.

Vuelva a escribir la clase Worker.

Reemplace la clase Worker de la plantilla por el siguiente código de C# y cambie el nombre del archivo a WindowsBackgroundService.cs:

namespace App.WindowsService;

public sealed class WindowsBackgroundService(
    JokeService jokeService,
    ILogger<WindowsBackgroundService> logger) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        try
        {
            while (!stoppingToken.IsCancellationRequested)
            {
                string joke = jokeService.GetJoke();
                logger.LogWarning("{Joke}", joke);

                await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
            }
        }
        catch (OperationCanceledException)
        {
            // When the stopping token is canceled, for example, a call made from services.msc,
            // we shouldn't exit with a non-zero exit code. In other words, this is expected...
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "{Message}", ex.Message);

            // Terminates this process and returns an exit code to the operating system.
            // This is required to avoid the 'BackgroundServiceExceptionBehavior', which
            // performs one of two scenarios:
            // 1. When set to "Ignore": will do nothing at all, errors cause zombie services.
            // 2. When set to "StopHost": will cleanly stop the host, and log errors.
            //
            // In order for the Windows Service Management system to leverage configured
            // recovery options, we need to terminate the process with a non-zero exit code.
            Environment.Exit(1);
        }
    }
}

En el código anterior, JokeService se inserta junto con ILogger. Ambos están disponibles para la clase como campos private readonly. En el método ExecuteAsync, el servicio de chistes solicita un chiste y lo escribe en el registrador. En este caso, el registrador se implementa mediante el registro de eventos de Windows Microsoft.Extensions.Logging.EventLog.EventLogLogger. Los registros se escriben en el Visor de eventos y están disponibles en este para visualizarlos.

Nota

De forma predeterminada, la gravedad del registro de eventos es Warning. Aunque esto se puede configurar, por motivos de demostración, WindowsBackgroundService ejecuta los registros con el método de extensión LogWarning. Para dirigirse específicamente al nivel EventLog, agregue una entrada en appsettings.{Environment}.json o proporcione un valor EventLogSettings.Filter.

{
  "Logging": {
    "LogLevel": {
      "Default": "Warning"
    },
    "EventLog": {
      "SourceName": "The Joke Service",
      "LogName": "Application",
      "LogLevel": {
        "Microsoft": "Information",
        "Microsoft.Hosting.Lifetime": "Information"
      }
    }
  }
}

Para obtener más información sobre cómo configurar niveles de registro, consulte Proveedores de registro en .NET: Configuración del registro de eventos de Windows.

Vuelva a escribir la clase Program.

Reemplace el contenido del archivo Program.cs de la plantilla por el siguiente código de C#:

using App.WindowsService;
using Microsoft.Extensions.Logging.Configuration;
using Microsoft.Extensions.Logging.EventLog;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddWindowsService(options =>
{
    options.ServiceName = ".NET Joke Service";
});

LoggerProviderOptions.RegisterProviderOptions<
    EventLogSettings, EventLogLoggerProvider>(builder.Services);

builder.Services.AddSingleton<JokeService>();
builder.Services.AddHostedService<WindowsBackgroundService>();

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

El método de extensión AddWindowsService configura la aplicación para que funcione como un servicio de Windows. El nombre del servicio se establece en ".NET Joke Service". El servicio hospedado está registrado para la inserción de dependencias.

Para obtener más información sobre el registro de servicios, consulte Inserción de dependencias en .NET.

Publicación de la aplicación

Para crear la aplicación del servicio Worker de .NET como servicio de Windows, se recomienda que la publique como ejecutable de archivo único. Es menos propenso a errores tener un archivo ejecutable independiente, ya que no hay ningún archivo dependiente en el sistema de archivos. No obstante, puede elegir una modalidad de publicación diferente, que es perfectamente aceptable, siempre y cuando cree un archivo *.exe que pueda ser el destino del Administrador de control de servicios de Windows.

Importante

Un enfoque de publicación alternativo consiste en compilar el archivo *.dll (en lugar de *.exe) y, al instalar la aplicación publicada mediante el Administrador de control de servicios de Windows, delegue en la CLI de .NET y pase el archivo DLL. Para obtener más información, consulte el documento sobre la CLI de .NET: comando dotnet.

sc.exe create ".NET Joke Service" binpath="C:\Path\To\dotnet.exe C:\Path\To\App.WindowsService.dll"
<Project Sdk="Microsoft.NET.Sdk.Worker">

  <PropertyGroup>
    <TargetFramework>net8.0-windows</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>true</ImplicitUsings>
    <RootNamespace>App.WindowsService</RootNamespace>
    <OutputType>exe</OutputType>
    <PublishSingleFile Condition="'$(Configuration)' == 'Release'">true</PublishSingleFile>
    <RuntimeIdentifier>win-x64</RuntimeIdentifier>
    <PlatformTarget>x64</PlatformTarget>
  </PropertyGroup>

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

Las líneas resaltadas anteriores del archivo del proyecto definen los comportamientos siguientes:

  • <OutputType>exe</OutputType>: crea una aplicación de consola.
  • <PublishSingleFile Condition="'$(Configuration)' == 'Release'">true</PublishSingleFile>: habilita la publicación de un solo archivo.
  • <RuntimeIdentifier>win-x64</RuntimeIdentifier>: especifica el RID de win-x64.
  • <PlatformTarget>x64</PlatformTarget>: especifica la CPU de la plataforma de destino de 64 bits.

Para publicar la aplicación desde Visual Studio, puede crear un perfil de publicación que se conserve. El perfil de publicación está basado en XML y tiene la extensión de archivo .pubxml. Visual Studio usa este perfil para publicar la aplicación implícitamente, mientras que, si usa la CLI de .NET, debe especificar explícitamente el perfil de publicación para que este se pueda usar.

Haga clic con el botón derecho en el proyecto, en el Explorador de soluciones, y seleccione Publicar... . A continuación, seleccione Agregar un perfil de publicación para crear un perfil. En el cuadro de diálogo Publicar, seleccione Carpeta como Destino.

The Visual Studio Publish dialog

Deje el valor predeterminado Ubicación y, a continuación, seleccione Finalizar. Una vez creado el perfil, seleccione Mostrar todas las configuraciones y compruebe la configuración del perfil.

The Visual Studio Profile settings

Asegúrese de que se especifican los valores siguientes:

  • Modo de implementación: Independiente
  • Producir un único archivo: activado
  • Habilitar la compilación ReadyToRun: activado
  • Quitar los ensamblados no usados (en versión preliminar) : desactivado

Por último, seleccione Publicar. La aplicación se compila y el archivo .exe resultante se publica en el directorio de salida /publish.

Como alternativa, puede usar la CLI de .NET para publicar la aplicación:

dotnet publish --output "C:\custom\publish\directory"

Para obtener más información, vea dotnet publish.

Importante

Con .NET 6, si intenta depurar la aplicación con la configuración <PublishSingleFile>true</PublishSingleFile>, no podrá depurar la aplicación. Para más información, consulte No se puede asociar a CoreCLR al depurar una aplicación de .NET 6 "PublishSingleFile".

Creación del servicio de Windows

Si no está familiarizado con el uso de PowerShell y prefiere crear un instalador para el servicio, consulte Creación de un instalador de servicio de Windows. En caso contrario, para crear el servicio de Windows, use el comando "create" nativo del Administrador de control de servicios (sc.exe) de Windows. Ejecute PowerShell como administrador.

sc.exe create ".NET Joke Service" binpath="C:\Path\To\App.WindowsService.exe"

Sugerencia

Si necesita cambiar la raíz de contenido de la configuración del host, puede pasarla como argumento de la línea de comandos al especificar el valor de binpath:

sc.exe create "Svc Name" binpath="C:\Path\To\App.exe --contentRoot C:\Other\Path"

Verá un mensaje similar al siguiente:

[SC] CreateService SUCCESS

Para obtener más información, consulte Comando create de sc.exe.

Configuración del servicio de Windows

Una vez creado el servicio, puede configurarlo opcionalmente. Si está bien con los valores predeterminados de servicio, pase a la sección Comprobación de la funcionalidad del servicio.

Los servicios de Windows proporcionan opciones de configuración de recuperación. Puede consultar la configuración actual mediante el comando sc.exe qfailure "<Service Name>" (donde <Service Name> es el nombre de los servicios) para leer los valores de configuración de recuperación actuales:

sc qfailure ".NET Joke Service"
[SC] QueryServiceConfig2 SUCCESS

SERVICE_NAME: .NET Joke Service
        RESET_PERIOD (in seconds)    : 0
        REBOOT_MESSAGE               :
        COMMAND_LINE                 :

El comando generará la configuración de recuperación, que está formada por los valores predeterminados, ya que aún no se han configurado.

The Windows Service recovery configuration properties dialog.

Para configurar la recuperación, use sc.exe failure "<Service Name>", donde <Service Name> es el nombre del servicio:

sc.exe failure ".NET Joke Service" reset=0 actions=restart/60000/restart/60000/run/1000
[SC] ChangeServiceConfig2 SUCCESS

Sugerencia

Para configurar las opciones de recuperación, la sesión de terminal debe ejecutarse como administrador.

Una vez que se han configurado correctamente, puede consultar los valores una vez más mediante el comando sc.exe qfailure "<Service Name>":

sc qfailure ".NET Joke Service"
[SC] QueryServiceConfig2 SUCCESS

SERVICE_NAME: .NET Joke Service
        RESET_PERIOD (in seconds)    : 0
        REBOOT_MESSAGE               :
        COMMAND_LINE                 :
        FAILURE_ACTIONS              : RESTART -- Delay = 60000 milliseconds.
                                       RESTART -- Delay = 60000 milliseconds.
                                       RUN PROCESS -- Delay = 1000 milliseconds.

Verá los valores de reinicio configurados.

The Windows Service recovery configuration properties dialog with restart enabled.

Opciones de recuperación del servicio e instancias de BackgroundService de .NET

Con .NET 6, se han agregado nuevos comportamientos de control de excepciones de hospedaje a .NET. La enumeración BackgroundServiceExceptionBehavior se agregó al espacio de nombres Microsoft.Extensions.Hosting y se usa para especificar el comportamiento del servicio cuando se produce una excepción. En la siguiente tabla se enumeran las opciones disponibles:

Opción Descripción
Ignore Ignore las excepciones producidas en BackgroundService.
StopHost IHost se detendrá cuando se produzca una excepción no controlada.

El comportamiento predeterminado antes de .NET 6 era Ignore, lo que daba lugar a procesos zombis (un proceso en ejecución que no hacía nada). Con .NET 6, el comportamiento predeterminado es StopHost, lo que da como resultado que el host se detenga cuando se produce una excepción. Pero se detiene limpiamente, lo que significa que el sistema de administración de Windows Service no reiniciará el servicio. Para permitir que el servicio se reinicie correctamente, puede llamar a Environment.Exit con un código de salida distinto de cero. Tenga en cuenta el siguiente bloque catch resaltado:

namespace App.WindowsService;

public sealed class WindowsBackgroundService(
    JokeService jokeService,
    ILogger<WindowsBackgroundService> logger) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        try
        {
            while (!stoppingToken.IsCancellationRequested)
            {
                string joke = jokeService.GetJoke();
                logger.LogWarning("{Joke}", joke);

                await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
            }
        }
        catch (OperationCanceledException)
        {
            // When the stopping token is canceled, for example, a call made from services.msc,
            // we shouldn't exit with a non-zero exit code. In other words, this is expected...
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "{Message}", ex.Message);

            // Terminates this process and returns an exit code to the operating system.
            // This is required to avoid the 'BackgroundServiceExceptionBehavior', which
            // performs one of two scenarios:
            // 1. When set to "Ignore": will do nothing at all, errors cause zombie services.
            // 2. When set to "StopHost": will cleanly stop the host, and log errors.
            //
            // In order for the Windows Service Management system to leverage configured
            // recovery options, we need to terminate the process with a non-zero exit code.
            Environment.Exit(1);
        }
    }
}

Comprobación de la funcionalidad del servicio

Para ver la aplicación creada como un servicio de Windows, abra Servicios. Seleccione la tecla de Windows (o Ctrl + Esc) y busque en "Servicios". Desde la aplicación Servicios, debería poder encontrar el servicio por su nombre.

Importante

De forma predeterminada, los usuarios normales (no administradores) no pueden administrar los servicios de Windows. Para comprobar que esta aplicación funciona según lo previsto, deberá usar una cuenta de administrador.

The Services user interface.

Para comprobar que el servicio funciona según lo previsto, debe seguir estos pasos:

  • Inicie el servicio
  • Visualización de los registros
  • Detener el servicio

Importante

Para depurar la aplicación, asegúrese de que no intenta depurar el archivo ejecutable que se ejecuta activamente en el proceso de servicios de Windows.

Unable to start program.

Inicio del servicio de Windows

Para iniciar el servicio de Windows, use el comando sc.exe start:

sc.exe start ".NET Joke Service"

Verá un resultado similar al siguiente:

SERVICE_NAME: .NET Joke Service
    TYPE               : 10  WIN32_OWN_PROCESS
    STATE              : 2  START_PENDING
                            (NOT_STOPPABLE, NOT_PAUSABLE, IGNORES_SHUTDOWN)
    WIN32_EXIT_CODE    : 0  (0x0)
    SERVICE_EXIT_CODE  : 0  (0x0)
    CHECKPOINT         : 0x0
    WAIT_HINT          : 0x7d0
    PID                : 37636
    FLAGS

El estado del servicio pasará de START_PENDING a En ejecución.

Ver registros

Para ver los registros, abra el Visor de eventos. Seleccione la tecla de Windows (o Ctrl + Esc) y busque "Event Viewer". Seleccione el nodo Visor de eventos (locales)>Registros de Windows>Aplicación. Debería ver una entrada del nivel Advertencia con un origen que coincide con el espacio de nombres de las aplicaciones. Haga doble clic en la entrada o haga clic con el botón derecho y seleccione Propiedades de evento para ver los detalles.

The Event Properties dialog, with details logged from the service

Después de ver los registros en el registro de eventos, debería detener el servicio, ya que está diseñado para registrar un chiste aleatorio una vez por minuto. Aunque se trata de un comportamiento intencionado, no resulta práctico para los servicios de producción.

Detención del servicio de Windows

Para detener el servicio de Windows, use el comando sc.exe stop:

sc.exe stop ".NET Joke Service"

Verá un resultado similar al siguiente:

SERVICE_NAME: .NET Joke Service
    TYPE               : 10  WIN32_OWN_PROCESS
    STATE              : 3  STOP_PENDING
                            (STOPPABLE, NOT_PAUSABLE, ACCEPTS_SHUTDOWN)
    WIN32_EXIT_CODE    : 0  (0x0)
    SERVICE_EXIT_CODE  : 0  (0x0)
    CHECKPOINT         : 0x0
    WAIT_HINT          : 0x0

El estado del servicio pasará de STOP_PENDING a Detenido.

Eliminación del servicio de Windows

Para eliminar el servicio de Windows, use el comando "delete" nativo del Administrador de control de servicios (sc.exe) de Windows. Ejecute PowerShell como administrador.

Importante

Si el servicio no tiene el estado Detenido, no se eliminará inmediatamente. Asegúrese de que el servicio se detenga antes de emitir el comando delete.

sc.exe delete ".NET Joke Service"

Verá un mensaje similar al siguiente:

[SC] DeleteService SUCCESS

Para obtener más información, consulte Comando delete de sc.exe.

Vea también

Siguientes