MultiTenancy с DbContext и TenantId - перехватчики, фильтры, EF Code-First

Моя организация должна иметь общую базу данных с общей базой данных схемы. Мы будем запрашивать на основе TenantId. У нас будет очень мало арендаторов (менее 10), и все они будут использовать одну и ту же схему базы данных без поддержки изменений или функциональных возможностей, связанных с арендатором. Метаданные арендатора будут храниться в памяти, а не в БД (статические члены).

Это означает, что всем сущностям теперь понадобится TenantId, и DbContext должен знать, чтобы фильтр по умолчанию.

TenantId скорее всего будет идентифицирован значением заголовка или исходным доменом, если не будет более целесообразного подхода.

Я видел различные примеры использования перехватчиков для этого, но не видел примера четкости реализации TenantId.


Проблемы, которые нам необходимо решить:

  • Как нам изменить текущую схему для поддержки этого (просто я думаю, просто добавьте TenantId)
  • Как мы обнаруживаем арендатора (просто, а также - основываем его на исходном домене запроса или значении заголовка - вытаскиваем из BaseController)
  • Как мы распространяем это на методы обслуживания (немного сложнее... мы используем DI для гидратации через конструкторы... хотим избежать наложения всех сигнатур метода с помощью TenantId)
  • Как нам изменить DbContext для фильтрации на этом tenantId, как только мы его получим (не знаю)
  • Как мы оптимизируем производительность. Какие индексы нам нужны, как мы можем гарантировать, что кеширование запросов не делает что-либо напуганное с изоляцией tenantId и т.д. (Не знаю).
  • Аутентификация - используя SimpleMembership, как мы можем выделить User s, каким-то образом связав их с арендатором.

Я думаю, самый большой вопрос в том, что есть 4 - модификация DbContext.


Мне нравится, как эта статья использует RLS, но я не уверен, как справиться с этим в кодовом режиме, dbContext:

https://azure.microsoft.com/en-us/documentation/articles/web-sites-dotnet-entity-framework-row-level-security/

Я бы сказал, что то, что я ищу, - это способ - с учетом производительности - выборочно запрашивать изолированные ресурсы tenantId с использованием DbContext без переполнения моих вызовов с помощью "AND TenantId = 1" и т.д.


Обновление. Я нашел некоторые варианты, но я не уверен, что за плюсы и минусы для каждого, или есть или нет какой-то "лучший" подход вообще. Моя оценка параметров сводится к:

  • Простота реализации
  • Производительность

ПОДХОД А

Это кажется "дорогостоящим", так как каждый раз, когда мы обновляем dbContext, мы должны повторно инициализировать фильтры:

https://blogs.msdn.microsoft.com/mvpawardprogram/2016/02/09/row-level-security-in-entityframework-6-ef6/

Сначала я настраивал своих арендаторов и интерфейс:

public static class Tenant {

    public static int TenantA {
        get { return 1; }
    }
    public static int TenantB
    {
        get { return 2; }
    }

}

public interface ITenantEntity {
    int TenantId { get; set; }
}

Я реализую этот интерфейс для любых объектов:

 public class Photo : ITenantEntity
 {

    public Photo()
    {
        DateProcessed = (DateTime) SqlDateTime.MinValue;
    }

    [Key]
    public int PhotoId { get; set; }

    [Required]
    public int TenantId { get; set; }
 }

И затем я обновляю свою реализацию DbContext:

  public AppContext(): base("name=ProductionConnection")
    {
        Init();
    }

  protected internal virtual void Init()
    {
        this.InitializeDynamicFilters();
    }

    int? _currentTenantId = null;

    public void SetTenantId(int? tenantId)
    {
        _currentTenantId = tenantId;
        this.SetFilterScopedParameterValue("TenantEntity", "tenantId", _currentTenantId);
        this.SetFilterGlobalParameterValue("TenantEntity", "tenantId", _currentTenantId);
        var test = this.GetFilterParameterValue("TenantEntity", "tenantId");
    }

    public override int SaveChanges()
    {
        var createdEntries = GetCreatedEntries().ToList();
        if (createdEntries.Any())
        {
            foreach (var createdEntry in createdEntries)
            {
                var isTenantEntity = createdEntry.Entity as ITenantEntity;
                if (isTenantEntity != null && _currentTenantId != null)
                {
                    isTenantEntity.TenantId = _currentTenantId.Value;
                }
                else
                {
                    throw new InvalidOperationException("Tenant Id Not Specified");
                }
            }

        }
    }

    private IEnumerable<DbEntityEntry> GetCreatedEntries()
    {
        var createdEntries = ChangeTracker.Entries().Where(V => EntityState.Added.HasFlag(V.State));
        return createdEntries;
    }

   protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Filter("TenantEntity", (ITenantEntity tenantEntity, int? tenantId) => tenantEntity.TenantId == tenantId.Value, () => null);

        base.OnModelCreating(modelBuilder);
    }

Наконец, в моих обращениях к DbContext я использую это:

     using (var db = new AppContext())
     {
          db.SetTenantId(someValueDeterminedElsewhere);
     }

У меня проблема с этим, потому что я обновляю свой AppContext примерно в миллионах мест (некоторые из них нуждаются в этом, некоторые - нет), так что это немного раздувает мой код. Также есть вопросы об определении арендатора - передаем ли я в HttpContext, заставляю ли мои контроллеры передавать TenantId во все вызовы метода службы, как я обрабатываю случаи, когда у меня нет исходного домена (вызовы webjob и т.д.).


ПОДХОД B

Найдено здесь: http://howtoprogram.eu/question/n-a,28158

Похоже, но просто:

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

 public partial class YourEntity : IMultiTenantEntity {}

 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);
}

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

Хотя это похоже на тупую версию A с теми же проблемами.

Я полагаю, что к этому моменту времени должна быть зрелая, целесообразная конфигурация/архитектура для удовлетворения этой потребности. Как мы должны это делать?

Ответ 1

Я хотел бы предложить следующий подход, 1. Создайте столбец с идентификатором идентификатора имени для каждой таблицы, содержащей основные бизнес-данные, это не требуется для любой таблицы сопоставлений.

  1. Используйте подход B, создав метод расширения, который возвращает IQueryable. Этот метод может быть расширением dbset, так что любой, кто пишет предложение фильтра, может просто вызвать этот метод расширения, за которым следует предикат. Это упростит задачу для разработчиков писать код, не беспокоясь о фильтрах идентификаторов арендаторов. Этот конкретный метод будет иметь код для применения условия фильтра для столбца идентификатора арендатора на основе контекста арендатора, в котором выполняется этот запрос.

Пример ctx.TenantFilter().Where(....)

  1. Вместо того, чтобы полагаться на контекст http, вы можете иметь идентификатор арендатора, переданный во всех ваших методах обслуживания, чтобы было легко обрабатывать контакты арендатора как в веб-приложениях, так и в веб-приложениях. Это делает звонок свободным от контактов и более легко проверяемым. Интерфейс интерфейса многопользовательского объекта выглядит неплохо, и у нас есть аналогичное ограничение в нашем приложении, которое работает до сих пор.

  2. Что касается добавления индекса, вам нужно будет добавить индекс для столбца идентификатора арендатора в таблицах с идентификатором арендатора, который должен заботиться о части индексации запроса стороне БД.

  3. Что касается части аутентификации, я бы рекомендовал использовать идентификатор asp.net 2.0 с конвейером owin. Система очень расширяема, настраиваемая и легко интегрируемая с любыми внешними поставщиками идентификаторов, если это необходимо в будущем.

  4. Пожалуйста, взгляните на шаблон репозитория для фреймворка сущности, который позволяет вам писать меньший код в общем виде. Это поможет нам избавиться от дублирования кода и избыточности и очень легко протестировать из unit test случаев

Ответ 2

Я думаю, самый большой вопрос в том, что есть 4 - модификация DbContext.

Не изменяйте контекст...

Вам не нужно смешивать код фильтрации с помощью бизнес-кода.

Я думаю, что все, что вам нужно, - это репозиторий, который отфильтровывает данные.
Этот репозиторий будет возвращать отфильтрованные данные на основе идентификатора, который вы получите от TenantIdProvider.
Тогда ваша Служба не должна ничего знать о арендаторах

using System;
using System.Data.Entity;
using System.Linq;

namespace SqlServerDatabaseBackup
{
    public class Table
    {
        public int TenantId { get; set; }
        public int TableId { get; set; }
    }

    public interface ITentantIdProvider
    {
        int TenantId();
    }

    public class TenantRepository : ITenantRepositoty
    {
        private int tenantId;
        private ITentantIdProvider _tentantIdProvider;
        private TenantContext context = new TenantContext(); //You can abstract this if you want
        private DbSet<Table> filteredTables;

        public IQueryable<Table> Tables
        {
            get
            {
                return filteredTables.Where(t => t.TenantId == tenantId);
            }
        }

        public TenantRepository(ITentantIdProvider tentantIdProvider)
        {
            _tentantIdProvider = tentantIdProvider;
            tenantId = _tentantIdProvider.TenantId();
            filteredTables = context.Tables;
        }

        public Table Find(int id)
        {
            return filteredTables.Find(id);
        }
    }

    public interface ITenantRepositoty
    {
        IQueryable<Table> Tables { get; }
        Table Find(int id);
    }

    public class TenantContext : DbContext
    {
        public DbSet<Table> Tables { get; set; }
    }

    public interface IService
    {
        void DoWork();
    }

    public class Service : IService
    {
        private ITenantRepositoty _tenantRepositoty;

        public Service(ITenantRepositoty tenantRepositoty)
        {
            _tenantRepositoty = tenantRepositoty;
        }

        public void DoWork()
        {
            _tenantRepositoty.Tables.ToList();//These are filtered records
        }
    }  
}

Ответ 3

Вопрос об EF, но я думаю, что здесь стоит упомянуть EF Core. В EF Core вы можете использовать Global Query Filters

Такие фильтры автоматически применяются к любым запросам LINQ, относящимся к этим типам сущностей, включая типы сущностей, на которые ссылаются косвенно, например, путем использования ссылок на свойства Include или прямой навигации.

Пример:

public class Blog
{
    private string _tenantId;

    public int BlogId { get; set; }
    public string Name { get; set; }
    public string Url { get; set; }

    public List<Post> Posts { get; set; }
}

public class Post
{
    public int PostId { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }
    public bool IsDeleted { get; set; }

    public int BlogId { get; set; }
    public Blog Blog { get; set; }
}

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>().Property<string>("TenantId").HasField("_tenantId");

    // Configure entity filters
    modelBuilder.Entity<Blog>().HasQueryFilter(b => EF.Property<string>(b, "TenantId") == _tenantId);
    modelBuilder.Entity<Post>().HasQueryFilter(p => !p.IsDeleted);
}