Kentico Xperience Xplorations: Using Xperience in a .NET Core Worker Service
Tags: dotnet, xperience, cms, csharp
Published: 2020-08-24 13:56:05

---
title: Kentico Xperience Xplorations: Using Xperience in a .NET Core Worker Service
published: true
description: Learn how Kentico Xperience 13 unlocks new .NET Core features like the Worker Service by creating an Event Log tail logger.
tags: dotnet, xperience, cms, csharp
series: Kentico Xperience Xplorations
cover_image: https://dev-to-uploads.s3.amazonaws.com/i/algcgsbuf2z4hv65yrw7.jpg
---

> In this post we'll be exploring how to integrate Kentico Xperience 13 with a .NET Core Worker Service to [tail log](https://stackoverflow.com/questions/36430524/is-there-an-equivalent-of-tail-f-on-windows) the Xperience Event Log to a console window 💻.

## Starting Our Journey: Xperience + ASP.NET Core

[Kentico Xperience](https://xperience.io/) provides a great integration into ASP.NET Core.

The [Kentico.Xperience.AspNetCore.WebApp](https://www.nuget.org/packages/Kentico.Xperience.AspNetCore.WebApp/) NuGet package provides us with these integration points.

We can register all of Xperience's services in ASP.NET Core's [dependency injection](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-3.1) container by calling `services.AddKentico();`:

```csharp
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddKentico();

        // ... Other DI registration
    }

    // ...
}
```

We can enabled Xperience's Page Builder functionality by calling `app.UseKentico(...)` and have it manage routing through it's [endpoint routing](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/routing?view=aspnetcore-3.1) middleware integration by a call to `endpoints.Kentico().MapRoutes();`:

```csharp
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // ...
    }

    public void Configure(
        IApplicationBuilder app, 
        IWebHostEnvironment env)
    {
        // ... earlier middleware

        app.UseKentico(features =>
        {
            features.UsePreview();
            features.UsePageBuilder();
        });

        // ... more middleware

        app.UseEndpoints(endpoints =>
        {
            endpoints.Kentico().MapRoutes();
        });
    }
}
```

However, what if we want to use Xperience on .NET Core without all of this setup 🤔? What if we want to use it without configuring and running an entire ASP.NET Core web application 😕?

Fortunately we aren't so limited and we can even use Xperience's APIs in a [.NET Core console application](https://docs.microsoft.com/en-us/dotnet/core/tutorials/with-visual-studio) 😮. 

There's nothing wrong with using a console application, though they seem to swing the pendulum of application complexity in the opposite direction compared to an ASP.NET Core web application.

What if we want to use values defined in an `appsettings.json`? What if we need logging or dependency injection? Application lifetime management would be nice! How do we get that?

Maybe we still want an application that runs continuously like an ASP.NET Core app, but without all the 'web' things.

A .NET Core [Worker Service](https://devblogs.microsoft.com/premier-developer/demystifying-the-new-net-core-3-worker-service/) sits in this happy middle ground, giving us the infrastructure we like from ASP.NET Core without the requirement for web application framework pieces 👍.

---

## Our First Stop: Creating a new .NET Core Work Service

Visual Studio provides a template for creating Worker Services in its New Project dialog:

![Visual Studio new project dialog](https://dev-to-uploads.s3.amazonaws.com/i/mpuiejgdbr716db80bkc.jpg)

This template will give us the foundation we need to get started.

After creating our new project we will see a small list of files in the Solution Explorer:

![Visual Studio Solution Explorer view](https://dev-to-uploads.s3.amazonaws.com/i/4doei1hfkhfdboyiwxzo.jpg)

Most of this will look very similar to an ASP.NET Core project, with far fewer files. Key among the files missing is `Startup.cs` and instead we have a `Worker.cs` file, but we still have `appsettings.json` for specifying configuration 💪🏾!

If we open the `Program.cs`, we notice our `Main()` method looks the same as an ASP.NET Core app:

```csharp
public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

    // ...
}
```

The difference between an ASP.NET Core application and a Worker Service app lies in the host configuration. While ASP.NET Core uses `.ConfigureWebHostDefaults(...)`, here we jump straight to configuring our dependency injection with a call to `.ConfigureServices(...)`, which is normally found in `Startup.cs`:

```csharp
public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureServices((hostContext, services) =>
        {
            services.AddHostedService<Worker>();
        });
```

This means our `Program.cs` contains everything we need to configure and start our application 😎.

So where's the code that actually does any meaningful work in our app? That's all in the `Worker.cs` file!

### Digging Deeper: The Worker

Opening up `Worker.cs` we can see a single class that inherits from `BackgroundService`:

```csharp
public class Worker : BackgroundService
{
    private readonly ILogger<Worker> _logger;

    public Worker(ILogger<Worker> logger)
    {
        _logger = logger;
    }

    // ..
}
```

The `Worker` class overrides 1 method from the `BackgroundService` base class - `ExecuteAsync(CancellationToken stoppingToken)`:

```csharp
protected override async Task ExecuteAsync(
    CancellationToken stoppingToken)
{
    while (!stoppingToken.IsCancellationRequested)
    {
        _logger.LogInformation(
           "Worker running at: {time}", 
           DateTimeOffset.Now);

        await Task.Delay(1000, stoppingToken);
    }
}
```

While this method doesn't _have_ to contain a `while (...)` loop, this is typically how you will see it implemented.

Since Workers are just services, their processing is not initiated by data being **pushed** to them, unlike ASP.NET Core applications which initiate processing when sent an HTTP request.

Instead, Workers **pull** data from other sources, running their processing only if there is data found and some conditions about that data are fulfilled 🧐.

Since the Worker doesn't know when data is available, it runs in a loop until the service is stopped.

## Continuing Our Journey: Integrating Kentico Xperience

Now that we've surveyed what makes up a Worker Service application, let's integrate Kentico Xperience!

### NuGet dependencies

We need to add the [Kentico.Xperience.Libraries NuGet package](https://www.nuget.org/packages/Kentico.Xperience.Libraries) to our `.csproj` file so we have access to all of Xperience's APIs in our Worker Service:

```xml
<ItemGroup>
    <!-- ... other packages -->

    <!-- Xperience 13 is still in beta at the time
         of this blog post -->
    <PackageReference 
        Include="Kentico.Xperience.Libraries" 
        Version="13.0.0-b7472.17674" />
</ItemGroup>
```

### Configuration

We also need to add our database information to the application configuration in `appsettings.json` with our connection string:

```json
{
  "Logging": {
    // ...
  },
  "ConnectionStrings": {
    "CMSConnectionString": "..."
  },
}
```

The application is already set up to read configuration from this `json` file, but Xperience doesn't know about it, so we need to tell it that the configuration it needs can be found there.

We open our `Program.cs` and add a line to the `.ConfigureServices()` call:

```csharp
Host.CreateDefaultBuilder(args)
    .ConfigureServices((hostContext, services) =>
    {
        services.AddHostedService<Worker>();

        Service.Use<IConfiguration>(
            () => hostContext.Configuration);
    });
```

This call to `Service.Use()` tells Xperience's internal infrastructure (dependency injection container) where to find important configuration, like the connection string we defined 😲.

In a .NET Core [hosted service](https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.hosting.hostbuilder.configureservices?view=dotnet-plat-ext-3.1), `hostContext.Configuration` contains all the configuration from the standard configuration sources.

For example, `hostContext.Configuration.GetConnectionString("CMSConnectionString");` would return our `appsettings.json` connection string.

Now that we've bridged the configuration gap between .NET Core and Xperience, we can move on to Worker 👍🏽!

### Initialization

Just because the .NET Core application starts up and runs doesn't mean Xperience's internals are up and running.

We need to explicitly initialize Xperience. Fortunately this can be done with a single call which we make inside another one of the methods provided by `BackgroundService` which we can override in our `Worker` class:

```csharp
public override Task StartAsync(CancellationToken cancellationToken)
{
    CMSApplication.Init();

    return base.StartAsync(cancellationToken);
}
```

`StartAsync` is only run once, when the application has finished its startup and [is ready to execute the Worker](https://docs.microsoft.com/en-us/dotnet/architecture/microservices/multi-container-microservice-net-applications/background-tasks-with-ihostedservice#implementing-ihostedservice-with-a-custom-hosted-service-class-deriving-from-the-backgroundservice-base-class).

This is the perfect place to initialize Xperience with the call to `CMSApplication.Init();` 😃.

Now that we've configured and initialized, we're ready to do some work!

## Our Destination: Creating a Tail Logger for the Xperience Event Log

At the beginning of this post I said we were going to build a tail logger for the Xperience Event Log. Since we have the basic Xperience + Worker Server integration set up, we can start that now.

### Adding Console Logging

First, we need to update the host configuration in `Program.cs` to include console logging and [disable the default application startup logs](https://andrewlock.net/new-in-aspnetcore-3-structured-logging-for-startup-messages/#suppressing-the-startup-messages-with-the-consolelifetime):

```csharp
Host.CreateDefaultBuilder(args)
    .ConfigureLogging(logging =>
    {
        // Adds console output for logs
        logging.AddConsole();
    })
    .ConfigureServices((hostContext, services) =>
    {
        // Disables the startup logs
        services.Configure<ConsoleLifetimeOptions>(opts => 
            opts.SuppressStatusMessages = true);

        services.AddHostedService<Worker>();

        Service.Use<IConfiguration>(
            () => hostContext.Configuration);
    });
```

Now, we can head back to `Worker.cs` and start coding our log tailing.

### Updating our Worker

We will define 2 private members at the top of the `Worker` class:

```csharp
private int latestLogId = 0;

private delegate void LogAction(string s, params object[] args);
```

The `latestLogId` keeps track of the latest `EventLogInfo` that we've logged out to the console, so we don't spam the output with duplicate information 😉.

`LogAction` is a named function signature to make our logging code a little cleaner later on - it matches all of the various `ILogger` methods we will want to use.

Next, let's update `ExecuteAsync` with the following:

```csharp
protected override async Task ExecuteAsync(
    CancellationToken stoppingToken)
{
    while (!stoppingToken.IsCancellationRequested)
    {
        var eventLog = (await EventLogProvider
            .GetEvents()
            .OrderByDescending(
                nameof(EventLogInfo.EventID))
            .TopN(1)
            .GetEnumerableTypedResultAsync(
                cancellationToken: stoppingToken))
            .FirstOrDefault();

        LogEvent(eventLog);

        await Task.Delay(5000, stoppingToken);
    }
}
```

Above, we are using the awesome new `async` querying in Xperience 13 🔥🔥🔥 and grabbing the most recent `EventLogInfo` from the database (if it exists) every 5 seconds.

> If you want to learn more about the new data access APIs in Kentico Xperience 13, check out my post _Kentico Xperience 13 Beta 3 - New Data Access APIs_.

{% post seangwright/kentico-xperience-13-beta-3-new-data-access-apis-1oha %}

Let's peak into that `LogEvent()` call and see what's going on there:

```csharp
private void LogEvent(EventLogInfo eventLog)
{
    if (eventLog is null || 
        eventLog.EventID == latestLogId)
    {
        return;
    }

    var action = GetAction(eventLog.EventType);

    action("Xperience Event {id} {type} {time} {source} {code} {description}",
        eventLog.EventID,
        eventLog.EventType,
        eventLog.EventTime,
        eventLog.Source,
        eventLog.EventCode,
        eventLog.EventDescription
    );

    latestLogId = eventLog.EventID;
}
```

First, we ensure the `eventLog` isn't `null` (maybe the table was just truncated 🤷🏽‍♀️) and the log record returned by our query isn't the one we just displayed.

Then we call `GetAction()` to select the correct `ILogger` method based on the type of log we found in the database.

Once we have a reference to the action (`ILogger` method) we want to to call, we invoke it with all the `eventLog` data and the log message template.

Finally, we update our `latestLogId` with the id from the the `eventLog` so we don't repeat ourselves on the next loop.

The last bit of code is the `GetAction()` method which uses a [C# 8 switch expression](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/switch-expression) 🤓 to pick the correct `ILogger` method:

```csharp
private LogAction GetAction(string eventLogType) =>
    eventLogType switch
    {
        "W" => logger.LogWarning,
        "E" => logger.LogError,
        "I" => logger.LogInformation,
        _ => logger.LogInformation
    };
```

Xperience `EventLogInfo` records use `"W"`, `"E"`, and `"I"` to denote the type of log, and we default to the `ILogger.Information` method if for some reason there isn't a match.

### Tailing the Xperience Event Log!

We're finally finished and can now see what we've accomplished.

![Interactions with Xperience administration UI and live site and logs appearing in the console window](https://dev-to-uploads.s3.amazonaws.com/i/jfto8n5teqvvic8zvaf0.gif)
<figcaption>1MB animated GIF showing Xperience Event Log tailiing in the console</figcaption>

We can see in the above recording that each time I do something in the Xperience content management application that generates an Event Log, the information is displayed in the console window below.

Also, a conveniently placed `throw new System.Exception("Kaboom 💣 💥 🔥 🧨");` in my `HomeController` of the content delivery application results in an error message (and stack trace) being displayed in the console, when I try to visit the site's home page.

(Too bad my cool emojis 😏 [don't display in the console](https://github.com/Microsoft/Terminal/issues/190)!)

## Conclusion

While this demo is fun, it's also not something we're likely to all go out and deploy to our production environments!

What's most important is that Kentico Xperience supports integrating into .NET Core Worker Services, which means it's now another powerful tool we can add to our software development tool belt 💪🏿.

One thing we didn't look at in this post was running a Worker Service [inside an ASP.NET Core application](https://docs.microsoft.com/en-us/dotnet/architecture/microservices/multi-container-microservice-net-applications/background-tasks-with-ihostedservice#summary-class-diagram). This would enable passing work off to a background thread, similar to how Scheduled Tasks work in the Xperience content management application, except it could be fully `async`.

> We could even use [channels](https://devblogs.microsoft.com/dotnet/an-introduction-to-system-threading-channels/) which are powerful abstractions of the classic publish/subscribe pattern 🧐.

I'm excited 😄 for all the creative ways that the Kentico Xperience community finds for using new tech like Worker Services and after reading this post I hope you are too.

Do you have any thoughts on how you might use Worker Services with Xperience? Share ideas below in the comments!

...

As always, thanks for reading 🙏!

---

<figcaption>Photo by <a href="https://unsplash.com/@jordanmadrid">Jordan Madrid
</a> on <a href="https://unsplash.com">Unsplash</a></figcaption>

---

We've put together a list over on [Kentico's GitHub account](https://github.com/Kentico/Home/blob/master/RESOURCES.md) of developer resources. Go check it out!

If you are looking for additional Kentico content, checkout the Kentico tag here on DEV:

{% tag kentico %}

Or my [Kentico Xperience blog series](https://dev.to/seangwright/series), like:

- [Kentico Xperience 13 Beta](https://dev.to/seangwright/series/7366)
- [Kentico EMS - MVC Widget Experiments](https://dev.to/seangwright/series/5038)
- [Kentico 12 - Design Patterns](https://dev.to/search?q=Kentico%2012%3A%20Design%20Patterns)