Динамический DbContext для каждого пользователя, вошедшего в систему с помощью Dependency Injection с интерфейсами - PullRequest
0 голосов
/ 26 сентября 2018

Я работаю над созданием мультитенантного веб-приложения (API), которое будет обращаться к различным базам данных в зависимости от пользователя, который входит в систему. Будет одна база данных для управления входами пользователей, которая содержит информацию о сервере и базе данных, необходимую для созданиясторона кода строки подключения для построения DbContext для конкретного пользователя, который вошел в систему. Вот проводка вверх от самого низкого уровня к контроллеру:

GenericRepository:

public class GenericRepository<TEntity> : GenericRepositoryBase<TEntity> where TEntity : class
{
    public GenericRepository(DbContext context) : base(context)
    { 
        //GenericRepositoryBase has the CRUD operations
    }
}

IRepository:

public interface IRepository<TEntity> where TEntity : class
{
    IQueryable<TEntity> Get(Expression<Func<TEntity, bool>> filter = null, Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null, string includeProperties = "");

    Task<TEntity> GetByID(object id);

    Task<ContextReturn> Insert(TEntity entity);

    Task<ContextReturn> Update(TEntity entityToDelete);

    Task<ContextReturn> Remove(TEntity entityToUpdate);
}

Репозиторий конкретной модели:

public class EmployeeRepository : GenericRepository<Employee>, IEmployeeRepository
{
    public EmployeeRepository(DbContext context) : base(context)
    {

    }
}

Файл Startup.cs в API содержит:

services.AddScoped<IEmployeeRepository, EmployeeRepository>();

и, наконец, подключение контроллера:

public class EmployeeController : ControllerBase
{
    IEmployeeRepository EmployeeRepository { get; }

    public EmployeeController(
        IEmployeeRepository employeeRepository)
    {
        EmployeeRepository = employeeRepository;
    }

    public IActionResult GetEmployees()
    {
        var data = EmployeeRepository.GetAll().ToList();

        return Ok(data);
    }
}

Все это прекрасно работает при работе с одним DbContext, объявленным при запуске как:

services.AddDbContextPool<DbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("SystemManagementConnection"), sqlServerOptions => sqlServerOptions.CommandTimeout(300)));

(На самом деле я использую этот точный контекст DI с соответствующим именем для доступа к базе данных управления пользователями).

Проблема возникает при попытке динамически создать DbContext на основе входа пользователя из-за парыиз причин:

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

  2. Startup.cs не знает никакой пользовательской информации, поэтому его нельзя передать никаким методом для создания DbContext на основе этой информации.

Я попытался разрешить первый, используя следующее:

services.AddScoped<IEmployeeRepository, EmployeeRepository>((ctx) => { DbContext context = DbContextFactory.Create(); return new EmployeeRepository(context); });

, привязанный к:

public class DbContextFactory
{
    public static DbContext Create()
    {
        var conn = GetConnectionString();

        if (!string.IsNullOrEmpty(conn))
        {
            var optionsBuilder = new DbContextOptionsBuilder<DbContext>();
            optionsBuilder.UseSqlServer(conn);
            return new DbContext(optionsBuilder.Options);
        }
        else
        {
            throw new ArgumentNullException("ConnectionId");
        }
    }

    public static string GetConnectionString()
    {
        var data = "";

        return data;
    }
}

, а затем передал IHttpContextAccessor в конструктор для доступа кпользователь (я уверен, что вы уже знаете, куда это идет ...), но, конечно, это требует, чтобы класс не был статическим, что затем выдает ошибки в файле Startup.cs.По сути, это не решает мою проблему.

Единственное другое решение, которое я могу придумать, - это объявить ALL моих DbContexts в сервисах файла Startup.cs, но опять же,мои репозитории все еще не будут знать, какой из них использовать, основываясь на пользователе.

Конечно, я не первый, кто сталкивается с этим.Если кто-то может помочь, я был бы очень признателен.

1 Ответ

0 голосов
/ 26 сентября 2018

Здесь представлено пошаговое решение на основе опубликованных источников :

  1. Зарегистрируйте IHttpContextAccessor в качестве одиночной службы.
  2. Измените DbContextFactory класс и зарегистрируйтесь как одиночный сервис, теперь вы готовы запросить сервис IHttpContextAccessor у конструктора класса;затем используйте его для динамического построения строки подключения.
  3. Добавьте каждую реализацию репозитория в режиме Scoped.И измените функцию фабрики реализации для загрузки службы DbContextFactory.

Вот пример реализации:

DbContextFactory класс:

public class DbContextFactory
{
    private readonly IHttpContextAccessor _httpContext;

    public DbContextFactory(IHttpContextAccessor httpContext)
    {
        _httpContext = httpContext;
    }

    public DbContext Create()
    {
        var conn = GetConnectionString();

        var optionsBuilder = new DbContextOptionsBuilder<DbContext>();
        optionsBuilder.UseSqlServer(conn);
        return new DbContext(optionsBuilder.Options);
    }

    private string GetConnectionString()
    {
        //--use the `_httpContext` to build the connection string dynamic
        var user = _httpContext?.HttpContext?.User;
        //...
        return "TODO";
    }
}

И частичные модификации метода ConfigureServices, расположенные в Startup классе:

public void ConfigureServices(IServiceCollection services)
{
    ...
    services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
    services.AddSingleton<DbContextFactory>();
    services.AddScoped<IEmployeeRepository, EmployeeRepository>(
        (servicesProvider) =>
        {
            var contextFactory = servicesProvider.GetService<DbContextFactory>();
            DbContext context = contextFactory.Create();
            return new EmployeeRepository(context);
        });
    ...
}

Надеюсь, это поможет!

...