Почему в приложении .net core 3.0 BackgroundService мой объект конфигурации пуст при работе в качестве службы, а не в качестве консольного приложения? - PullRequest
0 голосов
/ 06 ноября 2019

У меня есть приложение .net Core 3.0 BackgroundService, которое отлично работает при работе в режиме консоли, но после развертывания в качестве службы объект конфигурации, который должен быть загружен из appsettings.json, становится пустым. Что дает?

Program.cs

public class Program
{
    public static async System.Threading.Tasks.Task Main(string[] args)
    {
        var hostbuilder = new HostBuilder()
            .UseWindowsService()
            .ConfigureAppConfiguration((hostingContext, config) =>
            {
                config
                .SetBasePath(Path.GetDirectoryName(System.Reflection.Assembly.GetEntryAssembly().Location))
                .AddJsonFile("appsettings.json");
            })
            .ConfigureLogging(
                options => options.AddFilter<EventLogLoggerProvider>(level => level >= LogLevel.Information))
            .ConfigureServices((hostContext, services) =>
            {
                services.AddHostedService<Importer>().Configure<EventLogSettings>(config =>
                {
                    config.LogName = "Application";
                    config.SourceName = "Importer";
                });
            });
#if (DEBUG)
        await hostbuilder.RunConsoleAsync();
#else
        await hostbuilder.RunAsServiceAsync();
#endif
    }
}

Метод расширения для IhostBuilder для запуска службы

public static class ServiceBaseLifetimeHostExtensions
{
    public static IHostBuilder UseServiceBaseLifetime(this IHostBuilder hostBuilder)
    {
        return hostBuilder.ConfigureServices((hostContext, services) => services.AddSingleton<IHostLifetime, ServiceBaseLifetime>());
    }

    public static Task RunAsServiceAsync(this IHostBuilder hostBuilder, CancellationToken cancellationToken = default)
    {
        return hostBuilder.UseServiceBaseLifetime().Build().RunAsync(cancellationToken);
    }
}

Класс ServiceBaseLifetime для обработки жизненного цикла службы

public class ServiceBaseLifetime : ServiceBase, IHostLifetime
{
    private readonly TaskCompletionSource<object> _delayStart = new TaskCompletionSource<object>();

    public ServiceBaseLifetime(IHostApplicationLifetime applicationLifetime)
    {
        ApplicationLifetime = applicationLifetime ?? throw new ArgumentNullException(nameof(applicationLifetime));
    }

    private IHostApplicationLifetime ApplicationLifetime { get; }

    public Task WaitForStartAsync(CancellationToken cancellationToken)
    {
        cancellationToken.Register(() => _delayStart.TrySetCanceled());
        ApplicationLifetime.ApplicationStopping.Register(Stop);

        new Thread(Run).Start(); // Otherwise this would block and prevent IHost.StartAsync from finishing.
        return _delayStart.Task;
    }

    private void Run()
    {
        try
        {
            Run(this); // This blocks until the service is stopped.
            _delayStart.TrySetException(new InvalidOperationException("Stopped without starting"));
        }
        catch (Exception ex)
        {
            _delayStart.TrySetException(ex);
        }
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        Stop();
        return Task.CompletedTask;
    }

    // Called by base.Run when the service is ready to start.
    protected override void OnStart(string[] args)
    {
        _delayStart.TrySetResult(null);
        base.OnStart(args);
    }

    // Called by base.Stop. This may be called multiple times by service Stop, ApplicationStopping, and StopAsync.
    // That's OK because StopApplication uses a CancellationTokenSource and prevents any recursion.
    protected override void OnStop()
    {
        ApplicationLifetime.StopApplication();
        base.OnStop();
    }
}

Фактическая реализация службы не имеет значения, кроме конструктора, который принимает регистратор и конфигурацию через DI.

private readonly ILogger<Importer> _logger;
private readonly IConfiguration _configuration;

public Importer(IConfiguration configuration, ILogger<Importer> logger)
{
    _logger = logger;
    _configuration = configuration;
}

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    _logger.LogInformation($"Why is {_configuration["Key1"]} empty?");
}

appsettings.json

{
    "Key1":"some value"
}

Когда я запускаю отладку, консольное приложение запускается и запускается, регистрирует и загружает конфигурацию из настроек приложений. При развертывании в качестве службы объект конфигурации пуст.

Примечания. Файл appsettings читается, я могу сказать это, изменив его имя, и он выдает исключение для файла, который не найден. Файл appsettings также не пустой.

Ответы [ 2 ]

0 голосов
/ 06 ноября 2019

Моя проблема, кажется, какая-то проблема асинхронного состояния гонки (я полагаю, не положительная). Первый тик через ExecuteAsync конфигурация не загружается, но второй раз через нее есть. У меня была служба, умирающая, если она столкнулась с этим исключением, поэтому я никогда не заставлял ее отмечать второй раз.

0 голосов
/ 06 ноября 2019

Это, похоже, проблема XY и заслуживает рефакторинга

Создание строго типизированной модели для хранения желаемых настроек

public class ImporterSettings {
    public string Key1 { get; set; }
}

Служба Refactor hostedзависеть от настроек, так как, по моему мнению, сильное связывание служб с IConfiguration является запахом кода

private readonly ILogger<Importer> _logger;
private readonly ImporterSettnigs settings;

public Importer(ImporterSettnigs settings, ILogger<Importer> logger) {
    _logger = logger;
    this.settings = settings;
}

protected override async Task ExecuteAsync(CancellationToken stoppingToken) {
    _logger.LogInformation($"This is Key1: {settings.Key1}");
}

Теперь правильно сконфигурируйте запуск для использования предоставленной конфигурации

public class Program {
    public static async System.Threading.Tasks.Task Main(string[] args) {
        var hostbuilder = new HostBuilder()
            .UseWindowsService()
            .ConfigureAppConfiguration((hostingContext, config) => {
                var path = Path.GetDirectoryName(System.Reflection.Assembly.GetEntryAssembly().Location);
                config
                    .SetBasePath(path)
                    .AddJsonFile("appsettings.json");
            })
            .ConfigureLogging(
                options => options.AddFilter<EventLogLoggerProvider>(level => 
                    level >= LogLevel.Information)
            )
            .ConfigureServices((hostContext, services) => {
                //get settings from app configuration.
                ImporterSettings settings = hostContext.Configuration.Get<ImporterSettings>();

                services
                    .AddSingleton(settings) //add to service collection
                    .AddHostedService<Importer>()
                    .Configure<EventLogSettings>(config => {
                        config.LogName = "Application";
                        config.SourceName = "Importer";
                    });
            });
#if (DEBUG)
        await hostbuilder.RunConsoleAsync();
#else
        await hostbuilder..Build().RunAsync();
#endif
    }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...