.net-coreglobal-variablesasp.net-core-hosted-services

How to create Global Variable per hosted service in .Net core


How to create a global variable that can be unique per hosted service execution?

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

The problem:

When running hosted services is difficult to keep track of the execution without logs. In order to keep track of the execution of the hosted services I implemented logs with a unique traceId (GUID)

The problem is that the TraceLogId is being created per Task using Static Memory and when 2 task runs at the same time the first TraceLogId gets overridden by the second task.

Is there any way to avoid the traceLogId being overridden?

public static class GlobalVariables
{
    public static Guid TraceLogId { get; set; }
}

public class Task1Service : ITask1Service
{
    public async Task StartAsync(CancellationToken cancellationToken)
    {
        GlobalVariables.TraceLogId = 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}");
    }
}

public class Task2Service : ITask2Service
    {
        public async Task StartAsync(CancellationToken cancellationToken)
        {
            Console.WriteLine("Task2 executing");
            GlobalVariables.TraceLogId = Guid.NewGuid();
            Console.WriteLine($"Task2 executing with traceLogId = {GlobalVariables.TraceLogId}");
            Console.WriteLine($"Task2 ending = {GlobalVariables.TraceLogId}");
        }
    }

When the code is executed Task1 gets a TraceLogId but when it finishes it has the traceLogId that was assigned in Task2. enter image description here

    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
            {

            }
        }
    }
}

TaskServices:

    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);
    }
}

Solution

  • I recommend a scoped value for this; AsyncLocal<T> fits the bill.

    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);
      }
    }
    
    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}");
      }
    }
    

    This uses Disposable from my Disposables library.