Sto creando una sorta di applicazione multi-tenant con tabelle condivise utilizzando .NET Core 2.0 ed EF Core.
Sto anche usando un repository generico insieme all'Unità di lavoro, se è importante.
Voglio renderlo adeguatamente protetto ed anche evitare di ripetere la logica, quindi penso che se è possibile in qualche modo modificare il DbContext che sto usando per ogni operazione di ricerca aggiungere qualcosa come: entity => entity.tenantId == userContext.tenantId
.
Devo anche assicurarmi che durante la creazione del tenantId corretto venga applicato e non autorizzare l'aggiornamento di altre proprietà del tenant, ma finora questa logica è inclusa nel Service Layer: correggimi se sbaglio con questo approccio?
IUserContext è definito in Astrazioni di dominio e il livello applicazione lo implementa in modo diverso (API o App Web), ma non sono sicuro che non si tratti di odore / modello di codice quando il livello dati sta facendo questo tipo di logica? (Temo che lo sia).
Questa logica dovrebbe andare ai Servizi (dovrà quindi essere ripetuta più volte, cosa che non è una buona idea credo), DbContext o dovrei adattare il repository in qualche modo?
Quindi quello che vuoi è che se qualcuno scrivesse una dichiarazione Linq come questa
var result = myDbcontext.myDbSet.SomeLinq(...)
Sarebbe internamente come
var result = myDbContext.myDbSet
.Where(entity => entity.tenantId == userContext.tenantId)
.SomeLinq(...)
L'istruzione linq originale è in passaggi più piccoli:
IQueryable<MyType> mySet = myDbContext.myDbSet;
var result = mySet = .myDbSet.SomeLinq(...);
Quindi, ciò che dovresti fare è che quando gli utenti vogliono accedere a myDbContext.myDbSet
, ottengono effettivamente il sottoinsieme in cui tenantId == userContext.tenantId
Penso che la soluzione accurata sarebbe quella di creare una classe che esponga IQueryable per ogni DbSet nel tuo DbContext e nasconda il DbContext effettivo.
Qualcosa come questo:
class MyOriginalDbContext : DbContext
{
public DbSet<Student> Students {get; set;}
public DbSet<Teacher> Teachers {get; set;}
public DbSet<ClassRoom> ClassRooms {get; set;}
...
}
public MyLimitedContext : IDisposable
{
// to be filled in constructor
private readonly MyOriginalDbcontext dbContext = ...
private readonly int tenantId = ...
IQueryable<Student> Students
{
get
{
return this.dbContext.Students
.Where(student => student.tenantId == tenantId);
}
}
IQueryable<Student> Teachers
{
get
{
return this.dbContext.Teachers
.Where(teacher => teacher.tenantId == tenantId);
}
}
...
Gli utenti non noteranno la differenza:
using (var dbContext = new MyLimitedContext(...))
{
var TeachersWithTheirStudents = dbContext.Teachers
.Join(dbContext.Student)
.GroupBy(teacher => teacher.Id,
...
}
È possibile utilizzare i filtri di query globali. Leggi di più qui .
Tali filtri vengono applicati automaticamente a tutte le query LINQ che coinvolgono tali tipi di entità, inclusi i tipi di entità a cui si fa riferimento indirettamente, ad esempio mediante l'uso di Includi o riferimenti di proprietà di navigazione diretta
Un esempio:
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);
}
Puoi anche disabilitare il filtro per una singola query LINQ se hai bisogno di:
blogs = db.Blogs
.Include(b => b.Posts)
.IgnoreQueryFilters()
.ToList();