. Net Core: как инициализировать синглтон, которому нужен DBContext? - PullRequest
1 голос
/ 14 марта 2020

У меня есть. Net Базовая служба («MyLookup»), которая выполняет запрос к базе данных, выполняет некоторые операции поиска в Active Directory и сохраняет результаты в кэш-памяти.

Для первого выполнения я сделал .AddService<>() в Startup.cs, внедрил службу в конструкторы каждого из контроллеров и представлений, которые использовали службу ... и все заработало.

Это работало, потому что моя служба - и ее зависимые службы (IMemoryCache и DBContext) все были ограничены . Но теперь я бы хотел сделать эту услугу синглтоном . И я хотел бы инициализировать его (выполнить запрос к БД, поиск в AD и сохранить результат в кэш-памяти), когда приложение инициализируется.

В: Как мне это сделать?

Startup.cs

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddDbContext<MyDBContext>(options =>
            options.UseSqlServer(Configuration.GetConnectionString("MyDBContext")));
        services.AddMemoryCache();
        services.AddSingleton<IMyLookup, MyLookup>(); 
        ...
    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        ...
        // Q: Is this a good place to initialize my singleton (and perform the expensive DB/AD lookups?
        app.ApplicationServices.GetService<IDILookup>();   

OneOfMyClients.cs

    public IndexModel(MyDBContext context, IMyLookup myLookup)
    {
        _context = context;
        _myLookup = myLookup;
        ...

MyLookup.cs

public class MyLookup : IMyLookup
    ...
    public MyLookup (IMemoryCache memoryCache)
    {
        // Perform some expensive lookups, and save the results to this cache
        _cache = memoryCache;  
    }
    ...
    private async void Rebuild()  // This should only get called once, when app starts
    {
        ClearCache();
        var allNames =  QueryNamesFromDB();
        ...

    private List<string>QueryNamesFromDB()
    {
        // Q: ????How do I get "_context" (which is a scoped dependency)????
        var allNames = _context.MyDBContext.Select(e => e.Name).Distinct().ToList<string>();
        return allSInames;

Некоторые из исключений, которые я получал, пытаясь выполнять разные вещи:

InvalidOperationException: Не удается использовать сервис MyDBContext из синглтона MyLookup.

... и ...

InvalidOperationException: Не удалось разрешить службу MyDBContext с областью действия от root провайдера MyLookup

... или ...

System.InvalidOperationException: Невозможно разрешить службу IMyLookup с областью действия от root провайдера.


Благодаря Стиву за большую ценность на виду. Я наконец смог:

  1. Создать «поиск», который мог бы использоваться любым потребителем в любое время, из любого сеанса, в течение всего времени жизни приложения.

  2. Инициализируйте его один раз при запуске программы. К вашему сведению, НЕ будет приемлемо для отсрочки инициализации, пока какой-либо плохой пользователь не запустит ее - инициализация просто занимает слишком много времени.

  3. Использование зависимых сервисов (IMemoryCache и мой DBContext) ), независимо от времени жизни этих служб.

Мой окончательный код:

Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<MyDBContext>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("MyDBContext")));
    services.AddMemoryCache();
    // I got 80% of the way with .AddScoped()...
    // ... but I couldn't invoke it from Startup.Configure().
    services.AddSingleton<IMyLookup, MyLookup>(); 
    ...

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    // This finally worked successfully...
    app.ApplicationServices.GetService<IMyLookup>().Rebuild();

OneOfMyClients.cs

public IndexModel(MyDBContext context, IMyLookup myLookup)
{
    // This remained unchanged (for all consumers)
    _context = context;
    _myLookup = myLookup;
    ...

MyLookup.cs

public interface IMyLookup
{
    Task<List<string>> GetNames(string name);
    Task Rebuild();
}

public class MyLookup : IMyLookup
{
    private readonly IMemoryCache _cache;
    private readonly IServiceScopeFactory _scopeFactory;
    ...

    public MyLookup (IMemoryCache memoryCache, IServiceScopeFactory scopeFactory)
    {
        _cache = memoryCache;
        _scopeFactory = scopeFactory;
    }

    private async void Rebuild()
    {
        ClearCache();
        var allNames =  QueryNamesFromDB();
        ...

    private List<string>QueryNamesFromDB()
    {
        // .CreateScope() -instead of constructor DI - was the key to resolving the problem
        using (var scope = _scopeFactory.CreateScope())
        {
            MyDBContext _context =
                scope.ServiceProvider.GetRequiredService<MyDBContext>();
            var allNames = _context.MyTable.Select(e => e.Name).Distinct().ToList<string>();
            return allNames;
        }
    }

1 Ответ

2 голосов
/ 14 марта 2020

Нет единого решения вашей проблемы. В игру вступают различные принципы, такие как идея предотвращения зависимых зависимостей , которая гласит, что компонент должен зависеть только от услуг с равным или более длительным сроком службы. Эта идея подталкивает к созданию MyLookup класса, который имеет ограниченный или переходный образ жизни.

Эта идея сводится к применению модели составления замыкания , что означает, что вы составляете графы объектов, которые захватывают данные времени выполнения в переменных компонентов графа. Противоположной композиционной моделью является Ambient Composition Model , которая сохраняет состояние вне графа объекта и позволяет извлекать состояние (например, DbContext) по требованию.

Но это все теория. На первых порах может быть трудно реализовать это на практике. (Опять же) теоретически применить модель композиции замыкания просто, потому что это просто означает дать MyLookup более короткий образ жизни, например Scoped. Но когда MyLookup сам захватывает состояние, которое необходимо повторно использовать в течение всего срока действия приложения, это кажется невозможным.

Но это часто не так. Одним из решений является извлечение состояния из MyLookup в зависимость, которая не содержит собственных зависимостей (или зависит только от синглетонов) и затем становится одиночной. MyLookup затем можно «понизить» до Scoped и передать данные времени выполнения в свою одноэлементную зависимость, которая выполняет кэширование. Я бы с удовольствием показал вам пример этого, но ваш вопрос требует более подробной информации, чтобы сделать это.

Но если вы хотите оставить MyLookup синглтоном, определенно есть способы сделать это. Например, вы можете заключить одну операцию в область видимости. Пример:

public class MyLookup : IMyLookup
    ...
    public MyLookup (IMemoryCache memoryCache, IServiceScopeFactory scopeFactory)
    {
        _cache = memoryCache;
        _scopeFactory = scopeFactory;
    }

    private List<string> QueryNamesFromDB()
    {
        using (var scope = _scopeFactory.CreateScope())
        {
            var context = scope.ServiceProvider.GetRequiredService<MyDbContext>();
            var allNames = context.Persons.Select(e => e.Name).Distinct().ToList<string>();
            return allSInames;
        }
    }
}

В этом примере MyLookup вводится с IServiceScopeFactory. Это позволяет создавать (и уничтожать) IServiceScope за один вызов. Недостатком этого подхода является то, что MyLookup теперь требует зависимости от DI-контейнера. Только классы, которые являются частью Composition Root, должны знать о существовании контейнера DI.

Таким образом, вместо этого общий подход заключается во внедрении зависимости Func<MyDbContext>. Но на самом деле это довольно сложно с MS.DI, потому что, когда вы пытаетесь это сделать, фабрика попадает в область действия root, в то время как ваша DbContext всегда должна быть ограничена. Есть способы обойти это, но я не буду go в тех, из-за временных ограничений с моей стороны, и потому что это только усложнит мой ответ.

Чтобы отделить зависимость от контейнера DI от вашего бизнес-логика c, вам нужно будет либо: * переместить этот полный класс внутри вашей композиции Root * или разделить класс на две части, чтобы бизнес-логика c оставалась вне корня композиции; Например, вы можете достичь этого, используя подклассификацию или состав.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...