Sin categoría

HostedServices .Net Core

Hosted services in .net are classes that implement the IHostedService interface and run background tasks.

These tasks can be started as a part of the host’s lifecycle. That means that they start when the application starts.

The hosted services are useful for performing background tasks such as scheduling, long-running operations, or recurring tasks that need to run independently of the main application.

To start using the hosted service go to the next link and download the code template

https://github.com/matvi/dotnet-hosted-services

Let’s start by describing the base class CronJobServiceBase

https://github.com/matvi/dotnet-hosted-services/blob/main/HostedServicesPoc/Tasks/CronJobServiceBase.cs

using System;
using System.Threading;
using System.Threading.Tasks;
using Cronos;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace HostedServicesPoc.Tasks
{
    public abstract class CronJobServiceBase : IHostedService, IDisposable
    {
        private readonly ILogger _log;
        private readonly HostedServiceTaskSettingsBase _hostedServiceTaskSettingsBase;
        private System.Timers.Timer _timer;
        private readonly CronExpression _expression;
        private readonly TimeZoneInfo _timeZoneInfo;

        protected CronJobServiceBase(IOptions<HostedServiceTaskSettingsBase> hostedServiceSettings, ILogger<CronJobServiceBase> log)
        {
            _log = log;
            _hostedServiceTaskSettingsBase = hostedServiceSettings?.Value;
            _expression = CronExpression.Parse(_hostedServiceTaskSettingsBase.CronExpressionTimer, CronFormat.Standard);
            _timeZoneInfo = TimeZoneInfo.Local;
        }

        public virtual async Task StartAsync(CancellationToken cancellationToken)
        {
            _log.LogInformation($"{GetType()} is Starting");
            if (_hostedServiceTaskSettingsBase.Active)
            {
                await ScheduleJob(cancellationToken);
            }
        }

        public Task StopAsync(CancellationToken cancellationToken)
        {
            _log.LogInformation($"{GetType()} is Stopping");
            return Task.CompletedTask;
        }

        private async Task ScheduleJob(CancellationToken cancellationToken)
        {
            var next = _expression.GetNextOccurrence(DateTimeOffset.Now, _timeZoneInfo);
            if (next.HasValue)
            {
                var delay = next.Value - DateTimeOffset.Now;
                if (delay.TotalMilliseconds <= 0)   // prevent non-positive values from being passed into Timer
                {
                    await ScheduleJob(cancellationToken);
                }
                _timer = new System.Timers.Timer(delay.TotalMilliseconds);
                _timer.Elapsed += async (sender, args) =>
                {
                    _timer.Dispose();  // reset and dispose timer
                    _timer = null;

                    if (!cancellationToken.IsCancellationRequested)
                    {
                        await ExecuteTaskAsync(cancellationToken);
                    }

                    if (!cancellationToken.IsCancellationRequested)
                    {
                        await ScheduleJob(cancellationToken);    // reschedule next
                    }
                };
                _timer.Start();
            }
            await Task.CompletedTask;
        }

        protected virtual async Task ExecuteTaskAsync(CancellationToken cancellationToken)
        {
            await Task.Delay(5000, cancellationToken);
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool dispose)
        {
            try
            {
                if (dispose)
                {
                    _timer?.Dispose();
                }
            }
            finally
            {

            }
        }
    }
}

This code defines an abstract base class for a scheduled job service in a .NET environment using the ASP.NET Core Hosted Services framework. The class implements the IHostedService and IDisposable interfaces, and serves as a base for other scheduled job services in the application.

The class has a constructor that takes in an IOptions<HostedServiceTaskSettingsBase> object, which is used to obtain the task’s settings, and an ILogger<CronJobServiceBase> object, which is used to log information during the execution of the service.

The class also has a private field _timer, which is used to schedule the execution of the job. The timer is set to fire at the next occurrence of the cron expression specified in the settings.

The StartAsync method is called when the service is started and it will schedule the job to run by calling the ScheduleJob method.

The StopAsync method is called when the service is stopped, and will log that the service is stopping.

The ScheduleJob method calculates the next occurrence of the cron expression and sets the timer to fire at that time. The method is called every time the job is executed and reschedules the next execution.

The ExecuteTaskAsync method is designed to be overridden by derived classes to perform the specific task of the job service. The method will wait for 5 seconds before completing.

The Disposemethod releases resources used by the class, such as disposing the timer.

It is important to note that this class is abstract and it doesn’t execute any task yet because it has the

ExecuteTaskAsync method is empty and abstract, it’s purpose is to be overridden by the derived classes.

HostedServiceTaskSettingsBase

https://github.com/matvi/dotnet-hosted-services/blob/main/HostedServicesPoc/Tasks/HostedServiceTaskSettingsBase.cs

namespace HostedServicesPoc.Tasks
{
    public abstract class HostedServiceTaskSettingsBase
    {
        public bool Active { get; set; }
        public string CronExpressionTimer { get; set; }
    }
}

HostedServiceTaskSettingsBase is a simple class that serves as a base for other classes that hold the settings for a scheduled job service.

The class defines two properties:

  • Active is a boolean property that indicates whether the scheduled job service is active or not.
  • CronExpressionTimer is a string property that holds the cron expression that defines the schedule of the job service. The cron expression is used to calculate the next occurrence of the job.

HostedServiceTaskSettingsBase is being used in CronJobServiceBase constructor, as it is passed as IOptions<HostedServiceTaskSettingsBase> and it gives the values of the Active and CronExpressionTimer for the service.

Task1HostedService

Task1HostedService is a concrete implementation of the CronJobServiceBase abstract class, it represents a specific Job service (Task1).

using System;
using System.Threading;
using System.Threading.Tasks;
using HostedServicesPoc.TaskServices;
using HostedServicesPoc.TaskSettings;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace HostedServicesPoc.Tasks
{
    public class Task1HostedService : CronJobServiceBase
    {
        private readonly IServiceProvider _serviceProvider;

        public Task1HostedService(
            IOptions<Task1HostedServiceSettings> hostedServiceSettings,
            ILogger<CronJobServiceBase> log,
            IServiceProvider serviceProvider) : base(hostedServiceSettings, log)
        {
            _serviceProvider = serviceProvider;
        }
        
        protected override async Task ExecuteTaskAsync(CancellationToken cancellationToken)
        {
            using var scope = _serviceProvider.CreateScope();
            var task1Service = scope.ServiceProvider.GetRequiredService<ITask1Service>();
            await task1Service.StartAsync(cancellationToken);
        }
    }
}

It defines a private readonly field _serviceProvider of type IServiceProvider, which is used to resolve dependencies required by the service.

The class has a constructor that takes in three arguments:

  • IOptions<Task1HostedServiceSettings>: An object that holds the settings for the service, it is used to obtain the value of Active and CronExpressionTimer properties.
  • ILogger<CronJobServiceBase>: A logger object used to log information during the execution of the service.
  • IServiceProvider: The service provider object that is used to resolve dependencies required by the service.

The constructor of the Task1HostedService class calls the constructor of the base class CronJobServiceBase and pass the hostedServiceSettings and log, this way the Task1HostedService class inherits the same functionality and benefits of the CronJobServiceBase.

The class overrides the ExecuteTaskAsync method of the base class, it creates a new scope using the _serviceProvider field, and then it uses this scope to resolve an instance of ITask1Service. Then it calls StartAsync method on the resolved instance of the service and pass the cancellation token to it. This way the Task1HostedService class is able to execute the specific functionality of the Task1 service.

This specific Task1 service is implemented as an ITask1Service

interface, which is decoupled from the scheduled service, making the service more testable and reusable.

In summary, the Task1HostedService is using the CronJobServiceBase as a base class and schedule the Job, also it uses the

IServiceProvider to inject the ITask1Service interface, and call its StartAsync method. This way the Task1 service is executed.

Task1HostedServiceSettings

https://github.com/matvi/dotnet-hosted-services/blob/main/HostedServicesPoc/TaskSettings/Task1HostedServiceSettings.cs

using HostedServicesPoc.Tasks;

namespace HostedServicesPoc.TaskSettings
{
    public class Task1HostedServiceSettings : HostedServiceTaskSettingsBase
    {
        
    }
}

Task1HostedServiceSettings is a simple class that serves as a settings class specifically for the Task1HostedService scheduled job service.

It inherits from the base class HostedServiceTaskSettingsBase which defines the Active and CronExpressionTimer properties, and therefore this class has those properties as well. This way, the Task1HostedServiceSettings class has access to the same properties of the base class to set or get the values of Active and CronExpressionTimer for the task.

This class is being used in the Task1HostedService constructor, it’s passed as IOptions<Task1HostedServiceSettings>, so the Task1HostedService class can retrieve the values of Active and CronExpressionTimer for the task.

It is important to note that it doesn’t define any additional properties or methods, but it’s a simple example, it could be decorated with more properties that are specific to the Job’s task1 service.

Due to the nature of background services is very difficult to trace errors.

In order to be able to track errors a static class is used:

public static class GlobalVariables
    {
        private static AsyncLocal<Guid> _TraceLogId = new();
        public static Guid TraceLogId => _TraceLogId.Value;
        public static IDisposable SetTraceLogId(Guid value)
        {
            var oldValue = _TraceLogId.Value;
            _TraceLogId.Value = value;
            return Disposable.Create(() => _TraceLogId.Value = oldValue);
        }
    }

The GlobalVariable class is very important because it allows the different task services to share a static variable without interfering with each other.

GlobalVariables is a static class that defines a single static property and a single static method.

The TraceLogId property is an instance of AsyncLocal<Guid>which is a thread-safe way to store a value that is local to a specific asynchronous flow and is disposed when the flow is completed.

This property is read-only and it returns the current value of the _TraceLogId, it’s useful for tracking the flow of execution through the system, for example for tracing or logging purpose.

The SetTraceLogId method is used to set the value of the _TraceLogId field. It takes in a Guid as a parameter, and assigns it to the _TraceLogId field. It also keeps track of the previous value of the field before it’s being replaced, and returns an IDisposable object, when it’s disposed it will restore the previous value.

This allows to set a value for the TraceLogId and use it in different parts of the application, then when that specific flow or task is finished it will automatically restore the previous value, so that other async flows/tasks don’t interfere with each other.

It’s worth noting that this class is defined as static, so it can be accessed from any part of the application without having to create an instance.

how does it work if static variables are shared among all instances of a class?

Static variables are unique in memory and are shared across all instances of the class, but AsyncLocal<T> is a special type that provides thread-safe access to values that are local to a specific asynchronous flow. It is a way to store a value that is specific to a particular execution context, such as an async method or a Task.

Each async flow, such as an async method or Task gets its own copy of the AsyncLocal<T> variable.

Understanding AsyncLocal<T>

AsyncLocal<T> is build on top of the CallContext class, which is a mechanism provided by the .Net runtime to store data that is local to the current execution context. The CallContext is a flow-sensitive data structure that can be used to store data that is local to the current thread, call, or asynchronous flow.

How does AsyncLocal<T> work internally?

AsyncLocal<T> is built on top of the CallContext class, which is a mechanism provided by the .NET runtime to store data that is local to the current execution context. The CallContext is a flow-sensitive data structure that can be used to store data that is local to the current thread, call, or asynchronous flow.

When an AsyncLocal<T> variable is accessed, it looks up the current value of the variable in the CallContext for the current execution context. If a value is not found, it will return the default value for the type TWhen you use the SetTraceLogId method, it will store the value passed as a parameter in the CallContext, using the ExecutionContext.Capture method.

When the execution context changes (for example, when an asynchronous flow is started or completed), the CallContext data is flowed along with the execution context. This allows different asynchronous flows to have their own separate copies of the same AsyncLocal<T> variable, so they can store different values without interfering with each other.

In addition, AsyncLocal<T> provides an additional feature of automatically resetting the value of an AsyncLocal<T> variable when an async flow is completed, which is why it is able to reset the previous value automatically.

It’s important to keep in mind that AsyncLocal<T> works only within the same thread. If you need to store a value that is local to the current thread, across threads, you would need to use ThreadStatic attribute to make the variable thread static or thread local storage.

Task1Service: the logic executed by the hosted service

public class Task1Service : ITask1Service
    {
        public async Task StartAsync(CancellationToken cancellationToken)
        {
            using var traceIdScope = GlobalVariables.SetTraceLogId(Guid.NewGuid());
            Console.WriteLine($"Task1 executing with traceLogId = {GlobalVariables.TraceLogId}");
            Console.WriteLine($"Task1 will wait 5 seconds = {GlobalVariables.TraceLogId}");
            await Task.Delay(5000, cancellationToken);
            Console.WriteLine($"Task1 ending = {GlobalVariables.TraceLogId}");
        }
    }

Task1Service is an implementation of the ITask1Service interface, it’s a class that implements the functionality of the Task1 service.

It defines a single method StartAsync which accepts a CancellationToken as a parameter, it’s the method that is going to be executed when the Job service is scheduled.

The method creates a new variable traceIdScope, it calls SetTraceLogId function which generates a new GUID, and store it in the traceIdScope variable, this way it will set the trace log id to the current flow.

It then writes a message to the console, logging that the task is executing, including the value of the trace log id.

The StartAsync the method then uses the Task.Delay method to wait for 5 seconds, passing the cancellationToken parameter to it, which allows the task to be cancelled if the token is in a cancelled state.

After the delay, the method writes another message to the console, indicating that the task is ending, again including the value of the trace log id, this way you can track the flow of execution through the system and log the execution, because you have a reference id of the flow.

When the traceIdScope variable is disposed, at the end of the block, it will restore the previous value of TraceLogId, this way it doesn’t affect other flows.

It’s important to note that the StartAsync method will be executed by the schedule job service, it’s a simple example of a task and it doesn’t do anything important by itself.

Don’t forget to register the Settings, Services, and hosted services in the Startup Project.

 public void ConfigureServices(IServiceCollection services)
        {

            
            //register settings
            services.Configure<Task1HostedServiceSettings>(Configuration?.GetSection("Task1HostedServiceSettings"));
            services.Configure<Task2HostedServiceSettings>(Configuration?.GetSection("Task2HostedServiceSettings"));
            
            //register services
            services.AddScoped<ITask1Service, Task1Service>();
            services.AddScoped<ITask2Service, Task2Service>();
            
            //register hosted services
            services.AddHostedService<Task1HostedService>();
            services.AddHostedService<Task2HostedService>();
        }

And define the settings in the appSettings.json file.

The “Task1HostedServiceSettings” and “Task2HostedServiceSettings” sections are used to configure the settings for the Task1 and Task2 scheduled jobs services, respectively. They have two properties each:

  • “Active” which is set to true, this means that the task is active and will be executed by the Schedule job service.
  • “CronExpressionTimer” which is set to “*/1 * * * *”, this is a cron expression that represents a schedule, in this case, it is set to execute every minute.

It’s worth noting that each Task has its own settings, and these settings are used by the scheduled job service to determine when to execute the corresponding task.

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "Task1HostedServiceSettings": {
    "Active": true,
    "CronExpressionTimer" : "*/1 * * * *"

  },
  "Task2HostedServiceSettings": {
    "Active": true,
    "CronExpressionTimer" : "*/1 * * * *"
  }
}

Leave a comment