Моя организация должна иметь общую базу данных с общей базой данных схемы. Мы будем запрашивать на основе TenantId. У нас будет очень мало арендаторов (менее 10), и все они будут использовать одну и ту же схему базы данных без поддержки изменений или функциональных возможностей, связанных с арендатором. Метаданные арендатора будут храниться в памяти, а не в БД (статические члены).
Это означает, что всем сущностям теперь понадобится TenantId, и DbContext
должен знать, чтобы фильтр по умолчанию.
TenantId
скорее всего будет идентифицирован значением заголовка или исходным доменом, если не будет более целесообразного подхода.
Я видел различные примеры использования перехватчиков для этого, но не видел примера четкости реализации TenantId.
Проблемы, которые нам необходимо решить:
- Как нам изменить текущую схему для поддержки этого (просто я думаю, просто добавьте TenantId)
- Как мы обнаруживаем арендатора (просто, а также - основываем его на исходном домене запроса или значении заголовка - вытаскиваем из BaseController)
- Как мы распространяем это на методы обслуживания (немного сложнее... мы используем DI для гидратации через конструкторы... хотим избежать наложения всех сигнатур метода с помощью
TenantId
) - Как нам изменить DbContext для фильтрации на этом tenantId, как только мы его получим (не знаю)
- Как мы оптимизируем производительность. Какие индексы нам нужны, как мы можем гарантировать, что кеширование запросов не делает что-либо напуганное с изоляцией tenantId и т.д. (Не знаю).
- Аутентификация - используя SimpleMembership, как мы можем выделить
User
s, каким-то образом связав их с арендатором.
Я думаю, самый большой вопрос в том, что есть 4 - модификация DbContext.
Мне нравится, как эта статья использует RLS, но я не уверен, как справиться с этим в кодовом режиме, dbContext:
Я бы сказал, что то, что я ищу, - это способ - с учетом производительности - выборочно запрашивать изолированные ресурсы tenantId с использованием DbContext без переполнения моих вызовов с помощью "AND TenantId = 1"
и т.д.
Обновление. Я нашел некоторые варианты, но я не уверен, что за плюсы и минусы для каждого, или есть или нет какой-то "лучший" подход вообще. Моя оценка параметров сводится к:
- Простота реализации
- Производительность
ПОДХОД А
Это кажется "дорогостоящим", так как каждый раз, когда мы обновляем dbContext, мы должны повторно инициализировать фильтры:
Сначала я настраивал своих арендаторов и интерфейс:
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 с теми же проблемами.
Я полагаю, что к этому моменту времени должна быть зрелая, целесообразная конфигурация/архитектура для удовлетворения этой потребности. Как мы должны это делать?