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
}
// ...
}
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();
});
}
}
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:
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:
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();
}
// ...
}
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>();
});
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;
}
// ..
}
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);
}
}
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>
Configuration
We also need to add our database information to the application configuration in appsettings.json
with our connection string:
{
"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:
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, 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);
}
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);
});
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);
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);
}
}
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.
Kentico Xperience 13 Beta 3 - New Data Access APIs
Sean G. Wright ใป Aug 4 '20
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;
}
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
};
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.
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 ๐!
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:
Top comments (0)