Как переключаться между базами данных с одинаковой схемой, но разными именами, используя EF и внедрение зависимостей?

У меня есть веб-служба Web API, которая использует EF для операций с базой данных и Unity для внедрения зависимостей. У меня есть несколько баз данных с разными именами, но с одной и той же схемой. На каждый розничный магазин приходится одна база данных. Когда пользователь входит в систему, в зависимости от его привилегий, он может выбрать, с каким магазином он хочет работать. Это проблема с использованием внедрения зависимостей, потому что мне нужно изменить базу данных после внедрения репозитория. У меня есть что-то, что работает, но я не уверен, что это лучший подход.

Мои конкретные вопросы:

  • Это хороший подход к этой проблеме? Я видел другие вопросы, в которых упоминается изменение строки подключения во время выполнения, но я думаю, что мне нужно либо иметь строку подключения для каждого хранилища в моем Web.Config, либо каким-то образом динамически создавать строку подключения.

  • Нужна ли мне логика Dispose на моей фабрике? Если бы я вводил репозиторий напрямую, я знаю, что мне это не понадобилось бы. Поскольку я создаю репо из введенной фабрики, могу ли я доверять Unity, чтобы избавиться от репо и закрыть соединения с базой данных в какой-то момент? Должен ли я вместо этого использовать операторы using вокруг сгенерированных репозиториев?

Некоторые вопросы, которые я рассмотрел, пытаясь решить эту проблему, - это этот , этот и < href="https://stackoverflow.com/questions/29113206/change-injected-object-at-runtime">этот. Однако ни один из них напрямую не выполняет то, что я пытаюсь сделать. Ниже мое текущее решение.

Это мой репозиторий и его интерфейс. Я пропустил некоторые методы для краткости:

IGenericRepository

public interface IGenericRepository<T> where T: class
{
    IQueryable<T> Get();
    void ChangeDatabase(string database);
    void Update(T entityToUpdate);
    void Save();
}

Универсальный репозиторий

public class GenericRepository<TDbSet, TDbContext> : 
    IGenericRepository<TDbSet> where TDbSet : class
    where TDbContext : DbContext, new()
{
    internal DbContext Context;
    internal DbSet<TDbSet> DbSet;
    public GenericRepository() : this(new TDbContext())
    {
    }

    public GenericRepository(TDbContext context)
    {
        Context = context;
        DbSet = Context.Set<TDbSet>();
    }

    public virtual IQueryable<TDbSet> Get()
    {
        return DbSet;
    }       

    public void ChangeDatabase(string database)
    {
        var dbConnection = Context.Database.Connection;

        if (database == null || dbConnection.Database == database)
            return;

        if (dbConnection.State == ConnectionState.Closed)
        {
            dbConnection.Open();
        }

        Context.Database.Connection.ChangeDatabase(database);
    }

    public virtual void Update(TDbSet entityToUpdate)
    {
        DbSet.Attach(entityToUpdate);
        Context.Entry(entityToUpdate).State = EntityState.Modified;
    }

    public virtual void Save()
    {
        Context.SaveChanges();
    }
}

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

IRepositoryFactory

public interface IRepositoryFactory     
{
    IGenericRepository<TDbSet> GetRepository<TDbSet>(string dbName) where TDbSet : class;
}

Фабрика StoreEntities

public class StoreEntitiesFactory : IRepositoryFactory
{
    private bool _disposed;
    readonly StoreEntities _context;

    public StoreEntitiesFactory()
    {
        _context = new StoreEntities();
    }

    public IGenericRepository<TDbSet> GetRepository<TDbSet>(string dbName) where TDbSet : class
    {
        var repo = new GenericRepository<TDbSet, StoreEntities>(_context);

        repo.ChangeDatabase(dbName);

        return repo;
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (_disposed)
            return;

        if (disposing)
        {
            _context.Dispose();
        }

        _disposed = true;
    }

    ~StoreEntitiesFactory()
    {
        Dispose(false);
    }
}

Вот как я добавляю фабрику репозитория в свой файл WebApiConfig:

WebApiConfig.cs

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        // Web API configuration and services
        var container = new UnityContainer();       

        container.RegisterType<IRepositoryFactory, StoreEntitiesFactory>(new HierarchicalLifetimeManager());

        config.DependencyResolver = new UnityResolver(container);
    }
}

Наконец, вот как я бы использовал фабрику в своем контроллере:

Контроллер хранилища

public class StoreController : ApiController
{
    private readonly IRepositoryFactory _storeEntitiesRepoFactory;

    public StoreController(IRepositoryFactory storeEntitiesRepoFactory)
    {
        _storeEntitiesRepoFactory = storeEntitiesRepoFactory;        
    }

    [HttpGet]
    public IHttpActionResult Get()
    {
        var dbName = getStoreDbName(storeNumberWeGotFromSomewhere);

        try
        {
            var employeeRepo = _storeEntitiesRepoFactory.GetRepository<Employee>(dbName);
            var inventoryRepo = _storeEntitiesRepoFactory.GetRepository<Inventory>(dbName);

            var employees = employeeRepo.Get().ToList();
            var inventory = inventoryRepo.Get().ToList();
        }
        catch (Exception ex)
        {
            return InternalServerError();
        }
    }
}

person Bruno    schedule 07.06.2019    source источник


Ответы (2)


Я думаю, вы, вероятно, хотите, чтобы ваши реализации IRepositoryFactory возвращали один и тот же репозиторий для одного и того же dbName. Как написано сейчас, вызов StoreEntitesFactory.GetRepository с двумя разными параметрами dbName вызовет проблемы, поскольку он дает один и тот же экземпляр StoreEntites каждому репозиторию.

Проиллюстрировать...

public class DemonstrationController
{
    private readonly IRepositoryFactory _storeEntitiesRepoFactory;

    public DemonstrationController(IRepositoryFactory storeEntitiesRepoFactory)
    {
        _storeEntitiesRepoFactory = storeEntitiesRepoFactory;
    }

    [HttpGet]
    public IHttpActionResult Get()
    {
        var empRepo1 = _storeEntitiesRepoFactory.GetRepository("DB1");
        var empRepo2 = _storeEntitiesRepoFactory.GetRepository("DB2");

        // After the second line, empRepo1 is connected to "DB2" since both repositories are referencing the same
        // instance of StoreEntities
    }
}

Если вы изменили StoreEntitiesFactory, чтобы вернуть тот же репозиторий на основе заданного параметра, это решило бы эту проблему.

public class StoreEntitiesFactory : IRepositoryFactory
{
    private bool _disposed;
    private Dictionary<string, StoreEntities> _contextLookup;

    public StoreEntitiesFactory()
    {
        _contextLookup = new Dictionary<string, StoreEntities>();
    }

    public IGenericRepository<TDbSet> GetRepository<TDbSet>(string dbName) where TDbSet : class
    {
        if (!_contextLookup.TryGetValue(dbName, out StoreEntities context))
        {
            context = new StoreEntities();
            // You would set up the database here instead of in the Repository, and you could eliminate
            // the ChangeDatabase function.

            _contextLookup.Add(dbName, context);
        }
        return new GenericRepository<TDbSet, StoreEntities>(context);
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize();
    }

    protected virtual void Dispose(bool disposing)
    {
        if (!_disposed)
        {
            if (disposing)
            {
                foreach (var context in _contextLookup.Values)
                {
                    context.Dispose();
                }
            }
            _disposed = true;
        }
    }
}

Что касается второго вопроса, вам понадобится логика удаления на фабрике, поскольку она владеет создаваемыми экземплярами StoreEntities. Нет необходимости использовать операторы using для репозиториев, которые он создает, просто позвольте Unity избавиться от фабрики.

person Joshua Robinson    schedule 07.06.2019
comment
Мне очень нравится идея, что фабрика хранит коллекцию репозиториев. Одна вещь, которая мне не ясна, заключается в StoreEntitiesFactory, когда вы говорите, что вместо этого вы настроите базу данных здесь..., вы говорите, что я должен переместить код из метода ChangeDatabase в указанное вами место? Мне все равно придется как-то изменить базу данных, поскольку new StoreEntities() будет использовать базу данных по умолчанию, указанную в строке подключения. - person Bruno; 07.06.2019
comment
Да, переместите код в свой метод ChangeDatabase туда, где этот комментарий, затем удалите метод ChangeDatabase из IGenericRepository. Я думаю, что использование этого метода в самом репозитории рискованно, потому что разработчик может запросить репозиторий для DB1 на заводе, а затем вызвать ChangeDatabase в репозитории. В этот момент все репозитории, созданные на этой фабрике для DB1, указывают на новую базу данных. - person Joshua Robinson; 07.06.2019

Я бы порекомендовал вам использовать шаблон проектирования, называемый шаблоном стратегии, для решения этой проблемы.

Этот шаблон позволяет переключаться между двумя или более стратегиями во время выполнения. Ссылка: https://en.wikipedia.org/wiki/Strategy_pattern

Для внедрения я предлагаю вам зарегистрировать два конкретных класса в Unity, по одному для каждого соединения с БД, и вызвать метод Resolve для того, который вам нужен для передачи строки для создания экземпляра БД.

IUnityContainer container = new UnityContainer();
container.RegisterType<ICar, BMW>();
container.RegisterType<ICar, Audi>("LuxuryCar");

ICar bmw = container.Resolve<ICar>();  // returns the BMW object
ICar audi = container.Resolve<ICar>("LuxuryCar"); // returns the Audi object

Ссылка: https://www.tutorialsteacher.com/ioc/register-and-resolve-in-unity-container

Что касается Dispose, вы можете настроить все эти конкретные классы для БД как Singletons и разрешить открытие всех соединений, но вам нужно будет проверить, возможно ли это для вашего приложения.

person Arthur Lyrio de Oliveira    schedule 07.06.2019
comment
Не могли бы вы уточнить, как шаблон стратегии поможет мне? Насколько я понимаю, шаблон стратегии предназначен для выбора между несколькими алгоритмами. Какие алгоритмы я бы выбрал здесь? - person Bruno; 07.06.2019