#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
. Вы получаете какие-либо ошибки, касающиеся контекста?