Telerik blogs
DotNetT2 Light_1200x303

Learn what Worker Services are and where they are commonly used, and then we’ll create a Worker Service project in .NET 6 from scratch to see one in practice.

In many situations, you need to perform background tasks such as status checks and cleaning up old data. To make these tasks easier, we can create a Worker Service that will run at a time interval and do the workloads it was configured for.

Learn in this post what Worker Services are and how to implement them in .NET 6.

What Are Worker Services?

Worker Services are a type of application that can be configured to run in the background according to the pre-defined execution interval, which can be short-term or long-term.

A Worker Service project in .NET is built using a template that provides some useful resources, such as the host that is responsible for maintaining the application’s lifespan. In addition, the host also provides functions such as registration, dependency injection and configuration.

Previously called Windows Service, it was restricted to Windows only. But with the creation of Worker Services, this limitation no longer exists—you can develop cross-platform background services.

Where Can We Use Worker Services?

The use of Worker Services is very common, as it allows the scheduling of tasks so we don’t have to worry about manually notifying a service, we can create a worker to work for us, just informing us of its beginning and end of execution.

Below are some scenarios where using workers is the best option:

  • Sending messages/events
  • Reacting to changes to an entity
  • Manipulating a data store
  • Cleaning unnecessary data
  • Equalizing environments
  • Resilience Services

These are just a few examples of using workers, but there are many other scenarios where we can use workers to automate processes.

Creating a Worker Service

Next, we will create a Worker Service in .NET 6 from scratch. This worker will run every 30 seconds and, when it starts, it will fetch user data from an external API and check if these users exist in the local database. If they do not exist, they will be inserted; if they already exist, it will only display a log that no new records were found to be added. All processing will be displayed through logs, in the beginning, middle and end.

You can access the complete source code of the project at this link.

Prerequisites

  • Visual Studio 2022
  • .NET 6 SDK

Project Dependencies

You need to add the project dependencies—either directly in the project code “UserEqualizerWorkerService.csproj” or by downloading NuGet Packages:

<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />

Creating the Project

To create the project:

  • Open Visual Studio 2022
  • Create a new project
  • Type “worker” in the search bar, and choose the option “C# Worker Service”
  • Hit Next
  • Name the project (“UserEqualizerWorkerService” is suggested)
  • Hit Next
  • Choose “.NET 6 .0 (Long-term support)”
  • Create

Creating the Entities

In this tutorial, fake data from a public API will be used. The official website is Json Place Holder.

Next, let’s create two main entities—the entity responsible for receiving data from the API (PlaceHolderUser) and the entity that will represent the database table (User), which we will create later.

So, create a new folder called “Models” and inside, create a new folder “PlaceHolder.” Inside that, add the class below:

PlaceHolderUser

using System.Text.Json.Serialization;

namespace UserEqualizerWorkerService.PlaceHolder.Models;

public class PlaceHolderUser
{
    [JsonPropertyName("id")]
    public long? Id { get; set; }

    [JsonPropertyName("name")]
    public string? Name { get; set; }

    [JsonPropertyName("username")]
    public string? Username { get; set; }

    [JsonPropertyName("email")]
    public string? Email { get; set; }

    [JsonPropertyName("address")]
    public Address? Address { get; set; }

    [JsonPropertyName("phone")]
    public string? Phone { get; set; }

    [JsonPropertyName("website")]
    public string? Website { get; set; }

    [JsonPropertyName("company")]
    public Company? Company { get; set; }
}

public class Address
{
    [JsonPropertyName("street")]
    public string? Street { get; set; }

    [JsonPropertyName("suite")]
    public string? Suite { get; set; }

    [JsonPropertyName("city")]
    public string? City { get; set; }

    [JsonPropertyName("zipcode")]
    public string? Zipcode { get; set; }

    [JsonPropertyName("geo")]
    public Geo? Geo { get; set; }
}

public class Geo
{
    [JsonPropertyName("lat")]
    public string? Lat { get; set; }

    [JsonPropertyName("lng")]
    public string? Lng { get; set; }
}

public class Company
{
    [JsonPropertyName("name")]
    public string? Name { get; set; }

    [JsonPropertyName("catchphrase")]
    public string? CatchPhrase { get; set; }

    [JsonPropertyName("bs")]
    public string? Bs { get; set; }
}

Now inside the “Models” folder add the class below:

User

namespace UserEqualizerWorkerService.Models;

public class User
{
    public long? Id { get; set; }
    public string? Name { get; set; }
    public string? Username { get; set; }
    public string? Email { get; set; }
    public Address? Address { get; set; }
    public string? Phone { get; set; }
    public string? Website { get; set; }
    public Company? Company { get; set; }
}

public class Address
{
    public long? Id { get; set; }
    public string? Street { get; set; }
    public string? Suite { get; set; }
    public string? City { get; set; }
    public string? Zipcode { get; set; }
    public Geo? Geo { get; set; }
}

public class Geo
{
    public long? Id { get; set; }
    public string? Lat { get; set; }
    public string? Lng { get; set; }
}

public class Company
{
    public long? Id { get; set; }
    public string? Name { get; set; }
    public string? CatchPhrase { get; set; }
    public string? Bs { get; set; }
}

Creating the Data Context

The context class will be used to communicate with the database through Entity Framework Core features.

So, create a new folder called “Data” and inside it create the class below:

UserDbContext

using Microsoft.EntityFrameworkCore;
using UserEqualizerWorkerService.Models;

namespace UserEqualizerWorkerService.Data;

public class UserDbContext : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder options) => 
        options.UseSqlite("DataSource = userBD; Cache=Shared");

    public DbSet<User> Users { get; set; }
    public DbSet<Address> Address { get; set; }
    public DbSet<Geo> Geo { get; set; }
    public DbSet<Company> Company { get; set; }
}

Implementing Communication With the External API

Next, let’s create the class that will fetch the list of users in the fake API. So inside the “Data” folder, create a new folder called “Api” and inside it create the class below:

PlaceHolderClient

using Newtonsoft.Json;
using UserEqualizerWorkerService.PlaceHolder.Models;

namespace UserEqualizerWorkerService.Data.Api
{
    public class PlaceHolderClient
    {
        private readonly HttpClient _httpClient;

        public PlaceHolderClient(HttpClient httpClient)
        {
            _httpClient = httpClient;
        }

        public async Task<List<PlaceHolderUser>> GetPlaceHolderUsers()
        {
            var uri = "/users";

            var responseString = await _httpClient.GetStringAsync(uri);

            var placeHolederUsers = JsonConvert.DeserializeObject<List<PlaceHolderUser>>(responseString);

            return placeHolederUsers ?? new List<PlaceHolderUser>();
        }
    }
}

Creating the Service

The service class will contain the worker’s business rules. It will access the API layer, get the users in the external API, check if these users already exist in the database and if they don’t, they will be added and so both places will be equalized.

So, create a new folder called “Services” and inside a new folder “v1” and inside it add the class below:

UserEqualizerService

using UserEqualizerWorkerService.Data;
using UserEqualizerWorkerService.Data.Api;
using UserEqualizerWorkerService.Models;
using UserEqualizerWorkerService.PlaceHolder.Models;

namespace UserEqualizerWorkerService.Services.v1;

public class UserEqualizerService
{
    private readonly ILogger<UserEqualizerService> _logger;
    private readonly PlaceHolderClient _client;
    private readonly UserDbContext _context;

    public UserEqualizerService(ILogger<UserEqualizerService> logger, PlaceHolderClient client, UserDbContext context)
    {
        _logger = logger;
        _client = client;
        _context = context;
    }

    public virtual async Task<bool> ExecuteService()
    {
        _logger.LogInformation("Starting process");

        var placeHolderUsers = await _client.GetPlaceHolderUsers();

        var result = await EqualizeUsers(placeHolderUsers);

        _logger.LogInformation("Ending process");

        return result;
    }

    public virtual async Task<bool> EqualizeUsers(List<PlaceHolderUser> phUsers)
    {
        var users = _context.Users.ToList();

        var newUsers = phUsers.Where(x => !users.Any(x1 => x1.Username != x.Username && x1.Email != x.Email)).ToList();

        if (!newUsers.Any())
        {
            _logger.LogInformation("No new users to add");
            return true;
        }

        _logger.LogInformation($"Found {newUsers.Count} new users");

        return await SaveNewUsers(newUsers);
    }

    public virtual async Task<bool> SaveNewUsers(List<PlaceHolderUser> newUsers)
    {
        try
        {
            _logger.LogInformation("Saving new users");

            var newUsersEntity = newUsers.Select(x => new User
            {
                Id = x.Id,
                Name = x.Name,
                Username = x.Username,
                Email = x.Email,
                Address = new Models.Address
                {
                    Id = x.Id,
                    City = x.Address?.City,
                    Geo = new Models.Geo { Id = x.Id, Lat = x.Address?.Geo?.Lat, Lng = x.Address?.Geo?.Lng },
                    Street = x.Address?.Street,
                    Suite = x.Address?.Suite,
                    Zipcode = x.Address?.Zipcode,
                },
                Phone = x.Phone,
                Website = x.Website,
                Company = new Models.Company { Id = x.Id, Name = x.Company?.Name, CatchPhrase = x.Company?.CatchPhrase, Bs = x.Company?.Bs }
            }).ToList();

            await _context.Users.AddRangeAsync(newUsersEntity);
            await _context.Address.AddRangeAsync(newUsersEntity.Select(x => x.Address).ToList());
            await _context.Geo.AddRangeAsync(newUsersEntity.Select(x => x.Address.Geo).ToList());
            await _context.Company.AddRangeAsync(newUsersEntity.Select(x => x.Company).ToList());
            await _context.SaveChangesAsync();

            _logger.LogInformation("New users successfully saved");
            return true;
        }
        catch (Exception ex)
        {
            _logger.LogError($"Error saving new users - {ex.Message}");
            return false;
        }
    }
}

Adding the Configurations

In the Program.cs archive, above setting “services.AddHostedService();” add the following code:

        services.AddTransient<UserDbContext>();
        services.AddDbContext<UserDbContext>();
        services.AddTransient<UserEqualizerService>();
        services.AddHttpClient<PlaceHolderClient>(client =>
        {
            client.BaseAddress = new Uri("https://jsonplaceholder.typicode.com/");
        });

The code above adds UserDbContext, UserEqualizerService, PlaceHolderClient class settings.

For the PlaceHolderClient class, a BaseAddress is created that receives the URL from the external JsonPlaceHolder API. So when we invoke the users get method in the PlaceHolderClient class, we just pass the end of the URL, in this case “/users”.

Finally, replace the contents of the “Worker.cs” file with the following code:

using UserEqualizerWorkerService.Services.v1;

namespace UserEqualizerWorkerService
{
    public class Worker : BackgroundService
    {
        private readonly ILogger<Worker> _logger;
        private readonly UserEqualizerService _userService;

        public Worker(ILogger<Worker> logger, UserEqualizerService userService)
        {
            _logger = logger;
            _userService = userService;
        }

        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            _logger.LogInformation("Starting service...");

            while (!stoppingToken.IsCancellationRequested)
            {
                _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);

                var result = await _userService.ExecuteService();

                string resultLogMessage = result ? "Successfully processed" : "Processed with failure";

                _logger.LogInformation(resultLogMessage);

                _logger.LogInformation("Stoping service...");

                await Task.Delay(30000, stoppingToken);
            }
        }
    }
}

The code above executes the method of the service class that we just created. If an error occurs in the processing, an error message will be logged; otherwise, a success message.

The “await Task.Delay(30000, stoppingToken)” method is used to set the timeout for each worker execution. The expected value is in milliseconds, so 30000 milliseconds is equivalent to half a minute, which means our worker will run every 30 seconds. This value is configurable and will depend on each scenario.

Running EF Core Commands

Before running the application and seeing how it works in practice, we need to create the database with EF Core commands.

To run the EF Core commands, the .NET CLI tools must be installed. Otherwise, the commands will result in an error.

The first command will create a migration called InitialModel and the second will have EF create a database and schema from the migration.

More information about Migrations is available in Microsoft’s official documentation.

You can run the commands below in a project root terminal.

  • dotnet ef migrations add InitialModel
  • dotnet ef database update

Alternatively, run the following commands from the Package Manager Console in Visual Studio:

  • Add-Migration InitialModel
  • Update-Database

Executing the Worker

If you followed all the steps above, the worker is ready to run.

On the first run, the worker will fetch the list of users from the external API and check if these users exist in the database. As the database is still empty, they will be inserted, equalizing the two environments.

The image below shows the runtime logs from start to finish.

First Worker Execution

On the second run, the worker will check if the users already exist and then just log a message that there were no new users to add.

Second Worker Execution

Conclusion

Worker Services are a great tool for automating processes—after all, they can do the work without the need for a trigger. Just set a break time, and it will do everything else itself.

In this article, we saw an introduction to Worker Services and developed a working application using the Worker Service template available in .NET 6, which uses very common approaches in everyday life such as recording in a database and requests to an external API. If you want to delve deeper into Worker Services, I suggest reading the official Microsoft documentation on Workers.


assis-zang-bio
About the Author

Assis Zang

Assis Zang is a software developer from Brazil, developing in the .NET platform since 2017. In his free time, he enjoys playing video games and reading good books. You can follow him at: LinkedIn and Github.

Related Posts

Comments

Comments are disabled in preview mode.