Не удается использовать службу с ограниченной областью из одноэлементной службы хоста Windows

#c# #dependency-injection

#c# #внедрение зависимостей

Вопрос:

Я пишу службу Windows, вот мой файл Program.cs:

  public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }
   
    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureServices((hostContext, services) =>
            {
                IConfiguration configuration = hostContext.Configuration;
                services.AddHostedService<Worker>();
                services.AddScoped<IEncryption, Encryption>();
                services.AddScoped<IInsertUser, InsertUser>();
                services.AddScoped<IParseCSVFile, ParseCSVFile>();
                services.AddDbContext<LoginContext>(options => options.UseSqlServer(configuration.GetConnectionString("Previdence")));
                
            }).UseWindowsService();
}
  

Ошибка

{«Ошибка при проверке дескриптора службы ‘ServiceType: Microsoft.Расширения.Хостинг.Срок службы IHostedService: Одноэлементный тип реализации: PrevidenceUserLoader.Рабочий’: не удается использовать службу с ограниченной областью действия ‘PrevidenceUserLoader.Служебные программы.IEncryption «из одноэлементного»Microsoft.Расширения.Хостинг.IHostedService’.»}

Единственное место, где я использую службу IEncryption, вводится в этот класс:

  public class InsertUser : IInsertUser
{
    private readonly LoginContext _context;
    private readonly IEncryption _encryption;
    private readonly IServiceScopeFactory _scopeFactory;

    public static List<string> EncryptedColumns = new List<string> { "SocialSecurityNumber", "CodeDisplay", "FirstName", "LastName", "MiddleName", "PreferredName", "LastNameInitial" };
    public static List<string> CapitalizeColumns = new List<string> { "FirstName", "LastName", "MiddleName", "PreferredName", "LastNameInitial" };

    private Subjects encryptedSubjects;
    
    public InsertPrevidenceUser(LoginContext context,  IEncryption encryption, IServiceScopeFactory scope)
    {
        _scopeFactory = scope;
        _context = _scopeFactory.CreateScope().ServiceProvider.GetRequiredService<LoginContext>();
        
        _encryption = encryption;
    }
    
    public InsertStatus InsertUsers(Subjects user)
    {
        InsertStatus status = new InsertStatus();
        
        
        if(user != null)
        {
                //validate each user record
                if (ValidateUser(user))
                {
                    //good to go, encrypt then save
                    var encryptUser = _encryption.EncryptSubjects(user, EncryptedColumns);
                    if(encryptUser != null)
                    {
                        _context.Subjects.Add(encryptUser); //save it
                        status.Message = "Insert Sucessful";
                        status.UserName = "Test";
                        return status;
                        
                    }
                    
                }
                else
                {
                //something puked, return the list of errors.
                    status.Message = "Record did not insert";
                    status.UserName = "test";
                    return status;

                }

        }
            return null;
    }

    private bool ValidateUser(Subjects ValidateSubject)
    {
        var encryptedFirstName = _encryption.Encrypt(ValidateSubject.FirstName);
        var encryptedLastName = _encryption.Encrypt(ValidateSubject.LastName);

        var firstname = ValidateSubject.FirstName;

        var foundSubject = _context.Subjects.Where(x => x.FirstName == encryptedFirstName amp;amp; x.LastName == encryptedLastName).ToList();
        if(foundSubject.Count > 0)
        {
            //something was found, not good
            //add to messages (inserts, errors)
            return false;
        }
        return false;
    }
  

Затем я удалил IServiceScopeFactory и сбросил _context в context, а затем прочитал сообщение об ошибке:

Некоторые службы не могут быть созданы (ошибка при проверке дескриптора службы ‘ServiceType: Microsoft.Расширения.Хостинг.Срок службы IHostedService: Одноэлементный тип реализации: пользовательский загрузчик.Рабочий’: не удается использовать пользовательский загрузчик службы с ограниченной областью действия.Служебные программы.IParseCSVFile’ from singleton’Microsoft.Расширения.Хостинг.IHostedService’.)

И вот тут я застрял. Вот код ParseCSVFile:

 public class ParseCSVFile : IParseCSVFile
{
    public List<Subjects> ParseSubjectCSV(string inputFilePath)
    {
        using (var reader = new StreamReader(inputFilePath))
        using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture))
        {
            csv.Configuration.RegisterClassMap<SubjectsMap>();
            csv.Configuration.HeaderValidated = null;
            csv.Configuration.MissingFieldFound = null;
            var records = csv.GetRecords<Subjects>().ToList();
            return records;
        }
    }
}
  

И этот файл вызывается из рабочего:

 public class Worker : BackgroundService
{
    private readonly ILogger<Worker> _logger;
   
    private readonly IParseCSVFile _parse;
    private readonly IInsertPrevidenceUser _insert;
   
    
    public IConfiguration Configuration { get; }

    public Worker(ILogger<Worker> logger, IConfiguration configuration, IParseCSVFile parse, IInsertPrevidenceUser insert)
    {
        _logger = logger;
        Configuration = configuration;
        _parse = parse;
        _insert = insert;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            //_logger.LogInformation(_encryption.Encrypt("allentherapy.user"));
            var records = _parse.ParseSubjectCSV("c:\tmp\ImportFiles\Book1.csv");
            if (records.Count > 0)
            {
                SubjectsValidation validator = new SubjectsValidation();

                foreach (var record in records)
                {
                    ValidationResult result = validator.Validate(record);
                    if (!result.IsValid)
                    {
                        foreach (var failure in result.Errors)
                        {
                            _logger.LogInformation($"Failed Validation.  Error: {failure.ErrorMessage}");
                        }
                    }
                    else
                    {
                        
                        _insert.InsertUsers(record);

                        //Encrypt the data
                        //save here
                    }
                }
            }
            _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
            await Task.Delay(10000, stoppingToken);
        }
    }
}
  

Я безуспешно пытался изменить область действия служб и использовать scopeFactory. Я застрял.

Комментарии:

1. Я не думаю, что вы можете использовать ресурсы с ограниченной областью для фоновых служб. Фоновая служба выполняется в отдельном потоке и не знает о текущей области

Ответ №1:

Чтобы упростить задачу, избегайте внедрения IServiceScopeFactory в службы. Службы уже зарегистрированы как с ограниченной областью действия, и поэтому вы можете вводить LoginContext их непосредственно в конструктор. (Который работает, поскольку LoginContext также является службой с ограниченной областью действия).

Также ParseCSVFile может быть зарегистрирован как одноэлементный, поскольку нет зависимостей с ограниченной областью действия.

Проблема сейчас в том, что вы не можете получить доступ к службам непосредственно из синглтона Worker . Вот в IServiceScopeFactory чем фокус.

Идея состоит в том, чтобы создать (и утилизировать) область действия службы ExecuteAsync , как показано ниже:

 protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    while (!stoppingToken.IsCancellationRequested)
    {
        // Create service scope
        using var scope = _scopeFactory.CreateScope();
        // Access scoped services like this:
        var insertUser = scope.ServiceProvider.GetService<IInsertUser>();

        // ....
    }
}
  

В качестве альтернативы описанному выше подходу вы также можете преобразовать все свои службы с ограниченной областью действия в синглтоны и IServiceScopeFactory полностью отказаться от использования заводских интерфейсов для создания недолговечных экземпляров.

Примером этого является DbContextFactory EF Core 5.0<T>

Комментарии:

1. Когда я использую этот метод, я продолжаю получать ссылку на объект, не установленную для экземпляра объекта. РЕДАКТИРОВАТЬ: это исправлено, мне пришлось установить его в интерфейс (IInsertUser) вместо метода. Но теперь, как мне настроить контекст в методе?

2. Извините за опечатку класса и интерфейса. Что касается контекста, просто добавьте его в качестве параметра конструктора, например public InsertUser(LoginContext context) , и он должен просто работать из-за AddDbContext вызова ConfigureServices . Вы получаете какие-либо ошибки, касающиеся контекста?