简体   繁体   中英

Dependency Injection in Worker Service to classes other than Controllers by IOptions

I know this is a repeated question, went through answers and dont know whats happening here. In this problem we need to transfer the values from appsettings.json to another class other than Controllers here its ServiceSettings.cs .

This is a sample 'hello world' like program, here we need transfer values from appsettings.json to plugins.

This is folder architecture
在此处输入图像描述

appsettings.json

"Application": {
    "TimerInterval": 10000,
    "LogLevel": "Debug"
  }

I created a class based upon this app setting in class library-

ApplicationSettings.cs

 public class ApplicationSettings
    {
        public int TimerInterval { get; set; }
        public string LogLevel { get; set; }
    }

I tried push data from appsettings via the last line code

services.Configure<ApplicationSettings>(hostContext.Configuration.GetSection("Application")); 

Program.cs

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

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
           .UseWindowsService()
                .ConfigureServices((hostContext, services) =>
                {
                    services.AddSingleton<IConfiguration>(hostContext.Configuration);
// Service Settings Injected here
                    services.AddOptions<ServiceSettings>();
                    services.AddHostedService<Worker>();

                    services.Configure<ApplicationSettings>(hostContext.Configuration.GetSection("Application")); 
// for configure application
                   
                });
    }

Here during start method of the worker class i need to get values from ServiceSettings() which always returns null value.

Worker.cs(Re Edited)

 public class Worker : BackgroundService
    {
        private readonly ILogger<Worker> _logger;
        private readonly IConfiguration _configuration;
        private ServiceSettings _settings;

        public Worker(ILogger<Worker> logger, IConfiguration config)
        {
            _logger = logger;
            _configuration = config;            
        }

        public override Task StartAsync(CancellationToken cancellationToken)
        {
            
            Console.WriteLine("Start Asynch Method");

            // Load Settings From Configuration Files
            _settings = new ServiceSettings();
            _settings.Load();

            _logger.LogInformation("Settings: {setting}", _settings.TimerInterval);
            return base.StartAsync(cancellationToken);
        }
        
        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
           var values = _configuration.GetSection("DataSources").Get<List<DataSource>>();           
            while (!stoppingToken.IsCancellationRequested) {                
              await Task.Delay(Convert.ToInt32(_configuration["Application:TimerInterval"]), stoppingToken);
            }
        }
    }

The service settings values are provided below which receives the null value

ServiceSettings.cs

    public class ServiceSettings 
    {
        private readonly IOptions<ApplicationSettings> _appSettings;
        public ServiceSettings(IOptions<ApplicationSettings> appSettings)
        {
            _appSettings = appSettings;
        }
        
        public int TimerInterval { get; set; }

        public string LogLevel { get; set; }

        public void Load()
        {
            // Error is shown here
            try { TimerInterval = Convert.ToInt32(_appSettings.Value.TimerInterval); }
            catch { TimerInterval = 60; }

            try 
            // Here too
            { LogLevel = Convert.ToString(_appSettings.Value.LogLevel).ToLower(); }
            catch { LogLevel = "info"; }
        }
    }

I am pretty new to worker service, What i miss here? kindly guide me with the resources Thank you all.

This appears to be a design issue.

First lets fix the composition root. Avoid injecting IConfiguration . It can be seen as a code smell as IConfiguration should ideally be used in startup.

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

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .UseWindowsService()
            .ConfigureServices((hostContext, services) => {
                IConfiguration config = hostContext.Configuration;
                
                // parse settings
                ApplicationSettings appSettings = config
                    .GetSection("Application").Get<ApplicationSettings>();
                
                //set defaults.
                if(appSettings.TimerInterval == 0)
                    appSettings.TimerInterval = 60;
                
                if(string.IsNullOrWhiteSpace(appSettings.LogLevel))
                    appSettings.LogLevel = "Debug";
                
                services.AddSingleton(appSettings); //<-- register settings run-time data
                services.AddHostedService<Worker>();
            });
}

Note how the settings are extracted from configuration and added to the service collection. Since there is already a strongly defined type ( ApplicationSettings ) There really is no need for the ServiceSettings based on what was shown in the original question.

Update the worker to explicitly depend on the actual object required.

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

    public Worker(ILogger<Worker> logger, ApplicationSettings settings) {
        _logger = logger;
        this.settings = settings; //<-- settings injected.
    }

    public override Task StartAsync(CancellationToken cancellationToken) {
        Console.WriteLine("Start Asynch Method");
        _logger.LogInformation("Settings: {setting}", settings.TimerInterval);
        return base.StartAsync(cancellationToken);
    }
    
    protected override async Task ExecuteAsync(CancellationToken stoppingToken) {
        while (!stoppingToken.IsCancellationRequested) {
          await Task.Delay(settings.TimerInterval), stoppingToken);
        }
    }
}

You always have to get instances from your service collection. You typically do this by injecting them in class constructor.

 // WRONG
 // Worker.cs
 _settings = new ServiceSettings();
  1. This code does not compile because your ServiceSettings class has a constructor that requires one parameter but no parameter is given.
  2. how should your class know anything about the options stored in service collection without any reference?

Then it seems to make no sense to have two classes with the same data ServiceSettings and ApplicationSettings are the same. If you need the application settings in a service inject IOptions<ApplicationSettings> that's all. If you need separate settings classes, provide them as IOption<MyOtherSectionSettings> .

In the end, it could look like so:

 public class Worker {
   private readonly ApplicationSettings _settings;
   private readonly ILogger<Worker> _logger;
   
   public Worker(IOptions<ApplicationSettings> settingsAccessor, ILogger<Worker> logger) {
     _settings = settingsAccessor.Value;
     _logger = logger;
   }

   public override Task StartAsync(CancellationToken cancellationToken) {
        
        Console.WriteLine("Start Asynch Method");
        _logger.LogInformation("Settings: {setting}", _settings.TimerInterval);
        return base.StartAsync(cancellationToken);
   }
 }

Note that reading settingsAccessor.Value is the place where the framework really tries to access the configuration and so here we should think about error conditions (if not validated before).

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM