DEV Community

Cover image for Kentico Xperience Xplorations: Using Xperience in a .NET Core Worker Service
Sean G. Wright
Sean G. Wright

Posted on

Kentico Xperience Xplorations: Using Xperience in a .NET Core Worker Service

In this post we'll be exploring how to integrate Kentico Xperience 13 with a .NET Core Worker Service to tail log the Xperience Event Log to a console window ๐Ÿ’ป.

Starting Our Journey: Xperience + ASP.NET Core

Kentico Xperience provides a great integration into ASP.NET Core.

The 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 container by calling services.AddKentico();:

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

        // ... Other DI registration
    }

    // ...
}
Enter fullscreen mode Exit fullscreen mode

We can enabled Xperience's Page Builder functionality by calling app.UseKentico(...) and have it manage routing through it's endpoint routing middleware integration by a call to endpoints.Kentico().MapRoutes();:

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();
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

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 ๐Ÿ˜ฎ.

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

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

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:

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

    // ...
}
Enter fullscreen mode Exit fullscreen mode

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:

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

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:

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

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

    // ..
}
Enter fullscreen mode Exit fullscreen mode

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

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

        await Task.Delay(1000, stoppingToken);
    }
}
Enter fullscreen mode Exit fullscreen mode

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 to our .csproj file so we have access to all of Xperience's APIs in our Worker Service:

<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>
Enter fullscreen mode Exit fullscreen mode

Configuration

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

{
  "Logging": {
    // ...
  },
  "ConnectionStrings": {
    "CMSConnectionString": "..."
  },
}
Enter fullscreen mode Exit fullscreen mode

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:

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

        Service.Use<IConfiguration>(
            () => hostContext.Configuration);
    });
Enter fullscreen mode Exit fullscreen mode

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

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

    return base.StartAsync(cancellationToken);
}
Enter fullscreen mode Exit fullscreen mode

StartAsync is only run once, when the application has finished its startup and is ready to execute the Worker.

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:

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);
    });
Enter fullscreen mode Exit fullscreen mode

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:

private int latestLogId = 0;

private delegate void LogAction(string s, params object[] args);
Enter fullscreen mode Exit fullscreen mode

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:

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

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

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;
}
Enter fullscreen mode Exit fullscreen mode

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 ๐Ÿค“ to pick the correct ILogger method:

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

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

1MB animated GIF showing Xperience Event Log tailiing in the console

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

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. 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 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 ๐Ÿ™!


Photo by Jordan Madrid on Unsplash

We've put together a list over on Kentico's GitHub account of developer resources. Go check it out!

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

Or my Kentico Xperience blog series, like:

Discussion (0)