Compartir a través de


Creación de un servicio de Windows con BackgroundService

Es probable que los desarrolladores de .NET Framework estén familiarizados con las aplicaciones de servicio de Windows. Antes de .NET Core y .NET 5+, los desarrolladores que dependían de .NET Framework podrían crear servicios de Windows para realizar tareas en segundo plano o ejecutar procesos de ejecución prolongada. Esta funcionalidad sigue estando disponible y puede crear servicios de trabajo que se ejecuten como un servicio de Windows.

En este tutorial, aprenderá a:

  • Publique una aplicación de trabajo de .NET como un único archivo ejecutable.
  • Cree un servicio de Windows.
  • Cree la aplicación BackgroundService como servicio de Windows.
  • Inicie y detenga el servicio de Windows.
  • Vea los registros de eventos.
  • Elimine el servicio de Windows.

Sugerencia

Todo el código fuente de ejemplo de "Workers in .NET" está disponible en el Explorador de Muestras 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, puedes crear un nuevo trabajador mediante el comando dotnet new worker. Si usa Visual Studio, la plantilla se oculta hasta que se instale la carga de trabajo opcional ASP.NET y desarrollo web.

Prerrequisitos

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 el <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 obtener más información, vea Visual Studio Code: Terminal integrado.

Instalación del paquete NuGet

Para interoperar con servicios nativos de Windows desde las implementaciones de .NET IHostedService, 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 obtener más información sobre el comando add package de la CLI de .NET, consulte dotnet add package.

Después de agregar correctamente los paquetes, el archivo del proyecto ahora debe contener las siguientes referencias de paquete:

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

Actualizar archivo de 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="9.0.2" />
    <PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.2" />
  </ItemGroup>
</Project>

Los cambios del archivo de proyecto anteriores 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.csy 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 broma anterior expone una sola parte de la funcionalidad, el método GetJoke. Se trata de un método que devuelve string y representa una broma de programación aleatoria. El campo _jokes con ámbito de clase se usa para almacenar la lista de chistes. Se selecciona una broma aleatoria de la lista y se devuelve.

Reescritura de la clase Worker

Reemplace el Worker existente 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, el JokeService se inserta junto con un ILogger. Ambos están disponibles para la clase como campos. 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.EventLogLoggerProvider. 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 los niveles de registro, vea Proveedores de Registro en .NET: Configurar el Registro de eventos de Windows.

Reescritura de la clase Program

Reemplace la plantilla Program.cs contenido del archivo 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 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 de trabajo de .NET como servicio de Windows, se recomienda publicar la aplicación como un único archivo ejecutable. Es menos propenso a errores tener un ejecutable autocontenido, ya que no hay archivos dependientes en el sistema de archivos. Pero puede elegir una modalidad de publicación diferente, que es perfectamente aceptable, siempre que cree un archivo *.exe que pueda ser dirigido por el 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 .NET CLI: 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="9.0.2" />
    <PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.2" />
  </ItemGroup>
</Project>

Las líneas resaltadas anteriores del archivo de 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 archivos únicos.
  • <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 se use.

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.

Cuadro de diálogo Publicar de Visual Studio

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

la configuración del perfil de Visual Studio

Asegúrese de que se especifican las siguientes opciones de configuración:

  • modo de implementación: independiente
  • Producir un único archivo: activado
  • Habilitar la compilación ReadyToRun: activado
  • Recortar ensamblados sin usar (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, consulte dotnet publish.

Importante

Con .NET 6, si intenta depurar la aplicación con la configuración de <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 Crear un instalador de servicio de Windows. De lo contrario, para crear el servicio de Windows, use el comando nativo del Administrador de control de servicios de Windows (sc.exe). 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 en la configuración del host , puede pasarla como argumento de línea de comandos al especificar el binpath.

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

Verá un mensaje de salida:

[SC] CreateService SUCCESS

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

Configurar el servicio de Windows

Después de crear 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 es los valores predeterminados, ya que aún no se han configurado.

Cuadro de diálogo de propiedades de configuración de recuperación del servicio de Windows.

Para configurar la recuperación, use el 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 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.

Cuadro de diálogo de propiedades de configuración de recuperación del servicio de Windows con el reinicio habilitado.

Opciones de recuperación de servicios e instancias de .NET BackgroundService

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 tabla siguiente se enumeran las opciones disponibles:

Opción Descripción
Ignore Ignore las excepciones producidas en BackgroundService.
StopHost El 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 hace 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. Considere el bloque destacado catch siguiente:

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 servicio de Windows, abra Services. Seleccione la tecla Windows (o Ctrl + Esc) y busque en "Servicios". Desde la aplicación Services, deberías poder encontrar tu 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.

la interfaz de usuario de Los servicios.

Para comprobar que el servicio funciona según lo previsto, debe hacer lo siguiente:

  • Iniciar 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.

No se puede iniciar el programa.

Iniciar el servicio de Windows

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

sc.exe start ".NET Joke Service"

Verá una salida similar a la 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.

Visualización de registros

Para ver los registros, abra el Visor de eventos. Seleccione la tecla 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.

Cuadro de diálogo Propiedades de evento, con detalles registrados desde el servicio

Después de ver los registros en el registro de eventos, debería detener el servicio, Está diseñado para registrar una broma aleatoria una vez por minuto. Este es un comportamiento intencional, pero no es práctico para los servicios de producción.

Detener el servicio de Windows

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

sc.exe stop ".NET Joke Service"

Verá una salida similar a la 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.

Eliminar el servicio de Windows

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

Importante

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

sc.exe delete ".NET Joke Service"

Verá un mensaje de salida:

[SC] DeleteService SUCCESS

Para obtener más información, vea sc.exe eliminar.

Consulte también

Próximo