Windows-service maken met BackgroundService

.NET Framework-ontwikkelaars zijn waarschijnlijk bekend met Windows Service-apps. Voordat .NET Core en .NET 5+ waren, konden ontwikkelaars die afhankelijk waren van .NET Framework Windows Services maken om achtergrondtaken uit te voeren of langlopende processen uit te voeren. Deze functionaliteit is nog steeds beschikbaar en u kunt Worker Services maken die worden uitgevoerd als een Windows-service.

In deze zelfstudie leert u het volgende:

  • Publiceer een .NET-werkrol-app als één uitvoerbaar bestand.
  • Maak een Windows-service.
  • Maak de BackgroundService app als een Windows-service.
  • Start en stop de Windows-service.
  • Gebeurtenislogboeken weergeven.
  • Verwijder de Windows-service.

Tip

Alle voorbeeldbroncode 'Workers in .NET' is beschikbaar in de voorbeeldenbrowser om te downloaden. Zie Codevoorbeelden bekijken: Workers in .NET voor meer informatie.

Belangrijk

Als u de .NET SDK installeert, worden ook de Microsoft.NET.Sdk.Worker en de werkrolsjabloon geïnstalleerd. Met andere woorden, nadat u de .NET SDK hebt geïnstalleerd, kunt u een nieuwe werkrol maken met behulp van de opdracht nieuwe werkrol dotnet. Als u Visual Studio gebruikt, wordt de sjabloon verborgen totdat de optionele workload voor ASP.NET en webontwikkeling is geïnstalleerd.

Vereisten

  • De .NET 8.0 SDK of hoger
  • Een Windows-besturingssysteem
  • Een .NET Integrated Development Environment (IDE)
    • U kunt Visual Studio gerust gebruiken

Een nieuw project maken

Als u een nieuw Worker Service-project wilt maken met Visual Studio, selecteert u Bestand>nieuw>project.... Zoek in het dialoogvenster Een nieuw project maken naar 'Worker Service' en selecteer de sjabloon Worker Service. Als u liever de .NET CLI gebruikt, opent u uw favoriete terminal in een werkmap. Voer de dotnet new opdracht uit en vervang de door de <Project.Name> gewenste projectnaam.

dotnet new worker --name <Project.Name>

Zie dotnet new worker voor meer informatie over de opdracht .NET CLI new worker service project.

Tip

Als u Visual Studio Code gebruikt, kunt u .NET CLI-opdrachten uitvoeren vanuit de geïntegreerde terminal. Zie Visual Studio Code: Integrated Terminal voor meer informatie.

NuGet-pakket installeren

Als u wilt samenwerken met systeemeigen Windows Services vanuit .NET-implementaties IHostedService , moet u het Microsoft.Extensions.Hosting.WindowsServices NuGet-pakket installeren.

Als u dit vanuit Visual Studio wilt installeren, gebruikt u het dialoogvenster NuGet-pakketten beheren... . Zoek naar Microsoft.Extensions.Hosting.WindowsServices en installeer deze. Als u liever de .NET CLI gebruikt, voert u de dotnet add package opdracht uit:

dotnet add package Microsoft.Extensions.Hosting.WindowsServices

Zie dotnet add package voor meer informatie over de .NET CLI-opdracht voor het toevoegen van pakketten.

Nadat het toevoegen van de pakketten is geslaagd, moet het projectbestand nu de volgende pakketverwijzingen bevatten:

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

Projectbestand bijwerken

Dit werkrolproject maakt gebruik van de null-referentietypen van C#. Als u deze wilt inschakelen voor het hele project, werkt u het projectbestand dienovereenkomstig bij:

<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>

De wijzigingen in het voorgaande projectbestand voegen het <Nullable>enable<Nullable> knooppunt toe. Zie De null-context instellen voor meer informatie.

De service maken

Voeg een nieuwe klasse toe aan het project met de naam JokeService.cs en vervang de inhoud door de volgende C#-code:

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);

De voorgaande joke-servicebroncode toont één stukje functionaliteit, de GetJoke methode. Dit is een string terugkerende methode die een willekeurige programmeergrap vertegenwoordigt. Het veld met klassenbereik _jokes wordt gebruikt om de lijst met grappen op te slaan. Een willekeurige grap wordt geselecteerd in de lijst en geretourneerd.

De Worker klasse herschrijven

Vervang de bestaande Worker van de sjabloon door de volgende C#-code en wijzig de naam van het bestand in 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);
        }
    }
}

In de voorgaande code wordt de JokeService samen met een ILogger. Beide worden beschikbaar gesteld aan de klasse als private readonly velden. In de ExecuteAsync methode vraagt de grapdienst een grap op en schrijft deze naar de logger. In dit geval wordt de logboekregistratie geïmplementeerd door het Windows-gebeurtenislogboek - Microsoft.Extensions.Logging.EventLog.EventLogLogger. Logboeken worden geschreven naar en zijn beschikbaar voor weergave in de Logboeken.

Notitie

De ernst van het gebeurtenislogboek is Warningstandaard . Dit kan worden geconfigureerd, maar voor demonstratiedoeleinden worden de WindowsBackgroundService logboeken met de LogWarning extensiemethode gebruikt. Als u het EventLog niveau specifiek wilt instellen, voegt u een vermelding toe in de appsettings.{ Environment}.json of geef een EventLogSettings.Filter waarde op.

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

Zie Logboekproviders configureren in .NET voor meer informatie over het configureren van logboekniveaus: Windows EventLog configureren.

De Program klasse herschrijven

Vervang de inhoud van het bestand template Program.cs door de volgende C#-code:

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();

De AddWindowsService extensiemethode configureert de app zo dat deze werkt als een Windows-service. De servicenaam is ingesteld op ".NET Joke Service". De gehoste service is geregistreerd voor afhankelijkheidsinjectie.

Zie Afhankelijkheidsinjectie in .NET voor meer informatie over het registreren van services.

Publiceer de app.

Als u de .NET Worker Service-app als een Windows-service wilt maken, wordt u aangeraden de app te publiceren als één uitvoerbaar bestand. Het is minder foutgevoelig om een zelfstandig uitvoerbaar bestand te hebben, omdat er geen afhankelijke bestanden rond het bestandssysteem liggen. Maar u kunt een andere modaliteit voor publiceren kiezen, wat perfect acceptabel is, zolang u een *.exe-bestand maakt waarop Windows Service Control Manager kan worden toegepast.

Belangrijk

Een alternatieve publicatiebenadering is het bouwen van de *.dll (in plaats van een *.exe) en wanneer u de gepubliceerde app installeert met Behulp van Windows Service Control Manager, delegeert u aan de .NET CLI en geeft u het DLL-bestand door. Zie .NET CLI: dotnet-opdracht voor meer informatie.

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>

De voorgaande gemarkeerde regels van het projectbestand definiëren het volgende gedrag:

  • <OutputType>exe</OutputType>: Hiermee maakt u een consoletoepassing.
  • <PublishSingleFile Condition="'$(Configuration)' == 'Release'">true</PublishSingleFile>: Hiermee schakelt u publicatie van één bestand in.
  • <RuntimeIdentifier>win-x64</RuntimeIdentifier>: Hiermee geeft u de RID van win-x64.
  • <PlatformTarget>x64</PlatformTarget>: Geef de CPU van het doelplatform op van 64-bits.

Als u de app wilt publiceren vanuit Visual Studio, kunt u een publicatieprofiel maken dat behouden blijft. Het publicatieprofiel is gebaseerd op XML en heeft de bestandsextensie .pubxml . Visual Studio gebruikt dit profiel om de app impliciet te publiceren. Als u de .NET CLI gebruikt, moet u expliciet het publicatieprofiel opgeven dat moet worden gebruikt.

Klik met de rechtermuisknop op het project in Solution Explorer en selecteer Publiceren.... Selecteer vervolgens Een publicatieprofiel toevoegen om een profiel te maken. Selecteer in het dialoogvenster Publiceren de optie Map als doel.

The Visual Studio Publish dialog

Laat de standaardlocatie staan en selecteer Voltooien. Zodra het profiel is gemaakt, selecteert u Alle instellingen weergeven en controleert u uw profielinstellingen.

The Visual Studio Profile settings

Zorg ervoor dat de volgende instellingen zijn opgegeven:

  • Implementatiemodus: zelfstandig
  • Eén bestand produceren: ingeschakeld
  • ReadyToRun-compilatie inschakelen: ingeschakeld
  • Ongebruikte assembly's knippen (in preview): uitgeschakeld

Selecteer tot slot Publiceren. De app wordt gecompileerd en het resulterende .exe-bestand wordt gepubliceerd naar de uitvoermap /publish .

U kunt ook de .NET CLI gebruiken om de app te publiceren:

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

Voor meer informatie raadpleegt u dotnet publish.

Belangrijk

Als u met .NET 6 probeert fouten in de app op te sporen met de <PublishSingleFile>true</PublishSingleFile> instelling, kunt u geen fouten in de app opsporen. Zie Kan geen bijlage toevoegen aan CoreCLR bij het opsporen van fouten in een .NET 6-app voor PublishSingleFile voor meer informatie.

De Windows-service maken

Als u niet bekend bent met het gebruik van PowerShell en u liever een installatieprogramma voor uw service maakt, raadpleegt u Een Windows-service-installatieprogramma maken. Als u anders de Windows-service wilt maken, gebruikt u de systeemeigen opdracht voor het maken van Windows Service Control Manager (sc.exe). Voer PowerShell uit als beheerder.

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

Tip

Als u de hoofdmap van de hostconfiguratie wilt wijzigen, kunt u deze doorgeven als een opdrachtregelargument wanneer u het binpathvolgende opgeeft:

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

U ziet een uitvoerbericht:

[SC] CreateService SUCCESS

Zie sc.exe create voor meer informatie.

De Windows-service configureren

Nadat de service is gemaakt, kunt u deze desgewenst configureren. Als u tevreden bent met de standaardinstellingen van de service, gaat u verder met de sectie Servicefunctionaliteit controleren.

Windows Services bieden opties voor herstelconfiguratie. U kunt een query uitvoeren op de huidige configuratie met behulp van de sc.exe qfailure "<Service Name>" opdracht (waar <Service Name> is de naam van uw services) om de huidige herstelconfiguratiewaarden te lezen:

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

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

Met de opdracht wordt de herstelconfiguratie uitgevoerd. Dit zijn de standaardwaarden, omdat ze nog niet zijn geconfigureerd.

The Windows Service recovery configuration properties dialog.

Als u herstel wilt configureren, gebruikt u de sc.exe failure "<Service Name>"<Service Name> naam van uw service:

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

Tip

Als u de herstelopties wilt configureren, moet uw terminalsessie worden uitgevoerd als een Beheer istrator.

Nadat deze is geconfigureerd, kunt u opnieuw query's uitvoeren op de waarden met behulp van de sc.exe qfailure "<Service Name>" opdracht:

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.

U ziet de geconfigureerde waarden voor opnieuw opstarten.

The Windows Service recovery configuration properties dialog with restart enabled.

Opties voor serviceherstel en .NET-exemplaren BackgroundService

Met .NET 6 zijn nieuwe gedrag voor het afhandelen van hosting-uitzonderingen toegevoegd aan .NET. De BackgroundServiceExceptionBehavior opsomming is toegevoegd aan de Microsoft.Extensions.Hosting naamruimte en wordt gebruikt om het gedrag van de service op te geven wanneer er een uitzondering wordt gegenereerd. De volgende tabel bevat de beschikbare opties:

Optie Omschrijving
Ignore Uitzonderingen negeren die zijn opgetreden in BackgroundService.
StopHost De IHost wordt gestopt wanneer er een onverwerkte uitzondering wordt gegenereerd.

Het standaardgedrag voor .NET 6 is Ignore, wat heeft geresulteerd in zombieprocessen (een actief proces dat niets heeft gedaan). Met .NET 6 is StopHosthet standaardgedrag, wat tot gevolg heeft dat de host wordt gestopt wanneer er een uitzondering wordt gegenereerd. Maar het stopt netjes, wat betekent dat het Windows-servicebeheersysteem de service niet opnieuw start. Als u wilt toestaan dat de service opnieuw wordt opgestart, kunt u een afsluitcode zonder nul aanroepen Environment.Exit . Houd rekening met het volgende gemarkeerde catch blok:

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);
        }
    }
}

Servicefunctionaliteit controleren

Als u de app wilt zien die is gemaakt als een Windows-service, opent u Services. Selecteer de Windows-toets (of Ctrl + Esc) en zoek in Services. Vanuit de Services-app moet u uw service kunnen vinden op naam.

Belangrijk

Standaard kunnen gewone gebruikers (niet-beheerders) Geen Windows-services beheren. Als u wilt controleren of deze app werkt zoals verwacht, moet u een Beheer-account gebruiken.

The Services user interface.

Als u wilt controleren of de service werkt zoals verwacht, moet u het volgende doen:

  • De service starten
  • De logboeken weergeven
  • Service stoppen

Belangrijk

Als u fouten in de toepassing wilt opsporen, moet u ervoor zorgen dat u niet probeert fouten op te sporen in het uitvoerbare bestand dat actief wordt uitgevoerd in het Windows Services-proces.

Unable to start program.

De Windows-service starten

Gebruik de sc.exe start opdracht om de Windows-service te starten:

sc.exe start ".NET Joke Service"

De uitvoer ziet er ongeveer als volgt uit:

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

De servicestatus wordt overgestapt START_PENDING op Actief.

Logboeken weergeven

Als u logboeken wilt weergeven, opent u de Logboeken. Selecteer de Windows-toets (of Ctrl + Esc) en zoek naar ."Event Viewer" Selecteer het toepassingsknooppunt Logboeken (lokaal)>Windows-logboeken.> Als het goed is, ziet u een vermelding op waarschuwingsniveau met een bron die overeenkomt met de naamruimte van de apps. Dubbelklik op de vermelding of klik met de rechtermuisknop en selecteer Gebeurteniseigenschappen om de details weer te geven.

The Event Properties dialog, with details logged from the service

Nadat u logboeken in het gebeurtenislogboek hebt weergegeven, moet u de service stoppen. Het is ontworpen om een willekeurige grap eenmaal per minuut te registreren. Dit is opzettelijk gedrag, maar is niet praktisch voor productieservices.

De Windows-service stoppen

Gebruik de volgende opdracht om sc.exe stop de Windows-service te stoppen:

sc.exe stop ".NET Joke Service"

De uitvoer ziet er ongeveer als volgt uit:

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

De servicestatuswordt overgezet van STOP_PENDING gestopt naar Gestopt.

De Windows-service verwijderen

Als u de Windows-service wilt verwijderen, gebruikt u de systeemeigen opdracht voor verwijderen van Windows Service Control Manager (sc.exe). Voer PowerShell uit als beheerder.

Belangrijk

Als de service niet de status Gestopt heeft, wordt deze niet onmiddellijk verwijderd. Zorg ervoor dat de service is gestopt voordat u de verwijderopdracht uitgeeft.

sc.exe delete ".NET Joke Service"

U ziet een uitvoerbericht:

[SC] DeleteService SUCCESS

Zie sc.exe delete voor meer informatie.

Zie ook

Volgende