Преобразование приложения на основе EF в многопользовательское с помощью переопределения контекста

#c# #sql-server #asp.net-mvc #entity-framework

#c# #sql-сервер #asp.net-mvc #entity-framework

Вопрос:

У меня есть Entity Framework, приложение на основе кода, которое я должен сделать многопользовательским, то есть существует около полудюжины объектов «верхнего уровня», которые теперь должны ссылаться на конкретный идентификатор клиента. (Поскольку мы получаем до 100 пользователей, нет, мы не собираемся поддерживать индивидуальную схему, поэтому, пожалуйста, не предлагайте этого. :))

С помощью объектно-ориентированной абстракции для доступа к данным, такой как EF, я пытаюсь представить, как я могу добраться до места, где мне не нужно изменять какой-либо базовый код за пределами dbcontext, чтобы заставить это работать. По сути, я хочу использовать их в качестве критериев успеха:

  • Существующий код доступа к данным изменять не нужно. Этого много, многие из них процедурные и дублируют друг друга. К сожалению, нет классов репозитория, и, как бы я ни хотел туда попасть, я должен отложить технический долг.
  • Запросы фильтруют эти объекты верхнего уровня по идентификатору клиента. Так, например, существующий код получает context.Members.Где (x => x.IsAwesome), но волшебным образом также фильтрует, где идентификатор арендатора равен идентификатору арендатора (контекст арендатора доступен для каждого запроса и доступен для внедрения).
  • Добавление объектов верхнего уровня также присваивает идентификатор клиента. Другими словами, код выполняет что-то вроде context.Members.Add(newEntity) и newEntity волшебным образом присваивают своему свойству TenantId значение этого идентификатора, доступного через этот внедренный компонент.

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

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

1. Как мне достичь критериев успеха? См. Также последнее предложение.

2. Итак, вы хотите оставить существующий код полностью нетронутым (кроме контекстного кода)?

3. Да, это идеальный мир. Не прочь изменить сами типы сущностей.

Ответ №1:

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

 public interface IMultiTenantEntity {
    int TenantID { get; set; }
}
  

Затем реализуйте его для всех ваших объектов. Они генерируются автоматически, но частично, поэтому просто сделайте:

 public partial class YourEntity : IMultiTenantEntity {}
  

Теперь, чтобы заполнить это свойство при сохранении, переопределите SaveChanges его в своем контексте (опять же, оно автоматически создается, но частично, поэтому вам не нужно трогать автоматически созданный код):

 public partial class YourContext : DbContext
{
    private int _tenantId;
    public override int SaveChanges() {
        var addedEntities = this.ChangeTracker.Entries().Where(c => c.State == EntityState.Added)
            .Select(c => c.Entity).OfType<IMultiTenantEntity>();

        foreach (var entity in addedEntities) {
            entity.TenantID = _tenantId;
        }
        return base.SaveChanges();
    }

    public IQueryable<Code> TenantCodes => this.Codes.Where(c => c.TenantID == _tenantId);
}
  

Выше я предполагаю, что вы уже каким-то образом ввели текущий идентификатор клиента в _tenantId поле.

Затем для каждого набора объектов добавьте отдельное свойство, которое вернет этот набор, отфильтрованный TenantID (опять же в частичном классе для вашего контекста):

 public IQueryable<YourEntity> TenantYourEntities => this.YourEntities.Where(c => c.TenantID == _tenantId);
  

Теперь все, что вам нужно сделать, это найти все ссылки на наборы YourEntities (щелкните правой кнопкой мыши> найти все ссылки) и заменить их ссылками на TenantYourEntities . Тогда все ваши запросы будут отфильтрованы TenantID без особых усилий. Конечно, не заменяйте ссылки там, где вы используете DbSet для изменения entities ( Db.YourEntities.Add(...) ) .

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

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

2. В качестве альтернативы вы можете изменить .tt файл вашего контекста, чтобы он генерировал ваши наборы баз данных с именами типа «allMembers», в то время как для свойств Members он генерирует запросы, отфильтрованные пользователем. Тогда все ваши существующие запросы (context.Members ….) будут отфильтрованы арендатором. Но все ваши операторы context.Members.Add(…) теперь будут выдавать ошибку времени компиляции, которую может быть проще исправить.

3. Просто продолжение … интересная проблема заключается в том, что если вы объявляете индекс в построителе контекстной модели, который включает идентификатор родительского арендатора, метод save, по-видимому, обрабатывает свойство как «новое» и, следовательно, не сохраняет его. Это довольно странно.

Ответ №2:

Ну, технически, если идентификатор клиента известен во время создания экземпляра контекста, вы можете просто задать поле в своем контексте с этим значением и ссылаться на это поле в своих перегрузках. Например, вы можете сделать что-то вроде чтения из настроек приложения. Щелкните правой кнопкой мыши на вашем проекте и выберите «Свойства». Затем перейдите на вкладку «Настройки» и включите ее. Добавьте туда все, что вы будете использовать при разработке. Затем добавьте конфигурации в свой проект для каждого клиента и отредактируйте преобразования конфигурации, чтобы заменить их соответствующим значением. Затем при инициализации DI вы можете прочитать это значение параметра и ввести его как константу.

Если клиент устанавливается во время выполнения, например, через часть URL, то использование DI становится немного сложнее. Контекст, как правило, будет ограничен запросом, так что на самом деле это не проблема. Однако инициализация DI обычно не выполняется в конвейере запросов. На этом этапе вам, вероятно, просто нужно будет установить значение вручную или иным образом создать свой контекст в коде, который является частью конвейера запросов, например контроллера.

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

1. Но вопрос не в том, как ввести идентификатор (я думаю), а в том, как автоматически установить его в предложении Where и автоматически установить для связанных объектов, прежде чем сохранять их в базе данных.

2. Да, то, что сказал @Evk. Я не очень беспокоюсь о введении контекста. Если мне нужно использовать конкретный экземпляр контекстной прокладки клиента, это не имеет большого значения. Приложение уже тесно связано, так что у меня есть более серьезные проблемы. 🙂 Но да, смотрите критерии успеха и последнее предложение в тексте вопроса.