具有过滤的dbContext的多租户Web应用程序

我是ASP.Net MVC和多租户Web应用程序的新手。 我做了很多阅读,但作为初学者,我只是按照我的理解。 所以我设法构建了一个示例场景Web应用程序,需要解决它的结尾部分。 希望这种情况对其他一些初学者也有用,但欢迎任何其他方法。 提前致谢

1)SQLServer 2008中的数据库。

在此处输入图像描述

2)数据层:名为MyApplication.Data的C#类库项目

public class AppUser { [Key] public virtual int AppUserID { get; set; } [Required] public virtual int TenantID { get; set; } [Required] public virtual int EmployeeID { get; set; } [Required] public virtual string Login { get; set; } [Required] public virtual string Password { get; set; } } public class Employee { [Key] public virtual int EmployeeID { get; set; } [Required] public virtual int TenantID { get; set; } [Required] public virtual string FullName { get; set; } } public class Tenant_SYS { //this is an autonumber starting from 1 [Key] public virtual int TenantID { get; set; } [Required] public virtual string TenantName { get; set; } } 

3)。 业务层:类库MyApplication.Business以下FilteredDbSet类礼貌:Zoran Maksimovic

 public class FilteredDbSet : IDbSet, IOrderedQueryable, IOrderedQueryable, IQueryable, IQueryable, IEnumerable, IEnumerable, IListSource where TEntity : class { private readonly DbSet _set; private readonly Action _initializeEntity; private readonly Expression<Func> _filter; public FilteredDbSet(DbContext context) : this(context.Set(), i => true, null) { } public FilteredDbSet(DbContext context, Expression<Func> filter) : this(context.Set(), filter, null) { } public FilteredDbSet(DbContext context, Expression<Func> filter, Action initializeEntity) : this(context.Set(), filter, initializeEntity) { } public Expression<Func> Filter { get { return _filter; } } public IQueryable Include(string path) { return _set.Include(path).Where(_filter).AsQueryable(); } private FilteredDbSet(DbSet set, Expression<Func> filter, Action initializeEntity) { _set = set; _filter = filter; MatchesFilter = filter.Compile(); _initializeEntity = initializeEntity; } public Func MatchesFilter { get; private set; } public IQueryable Unfiltered() { return _set; } public void ThrowIfEntityDoesNotMatchFilter(TEntity entity) { if (!MatchesFilter(entity)) throw new ArgumentOutOfRangeException(); } public TEntity Add(TEntity entity) { DoInitializeEntity(entity); ThrowIfEntityDoesNotMatchFilter(entity); return _set.Add(entity); } public TEntity Attach(TEntity entity) { ThrowIfEntityDoesNotMatchFilter(entity); return _set.Attach(entity); } public TDerivedEntity Create() where TDerivedEntity : class, TEntity { var entity = _set.Create(); DoInitializeEntity(entity); return (TDerivedEntity)entity; } public TEntity Create() { var entity = _set.Create(); DoInitializeEntity(entity); return entity; } public TEntity Find(params object[] keyValues) { var entity = _set.Find(keyValues); if (entity == null) return null; // If the user queried an item outside the filter, then we throw an error. // If IDbSet had a Detach method we would use it...sadly, we have to be ok with the item being in the Set. ThrowIfEntityDoesNotMatchFilter(entity); return entity; } public TEntity Remove(TEntity entity) { ThrowIfEntityDoesNotMatchFilter(entity); return _set.Remove(entity); } ///  /// Returns the items in the local cache ///  ///  /// It is possible to add/remove entities via this property that do NOT match the filter. /// Use the  method before adding/removing an item from this collection. ///  public ObservableCollection Local { get { return _set.Local; } } IEnumerator IEnumerable.GetEnumerator() { return _set.Where(_filter).GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return _set.Where(_filter).GetEnumerator(); } Type IQueryable.ElementType { get { return typeof(TEntity); } } Expression IQueryable.Expression { get { return _set.Where(_filter).Expression; } } IQueryProvider IQueryable.Provider { get { return _set.AsQueryable().Provider; } } bool IListSource.ContainsListCollection { get { return false; } } IList IListSource.GetList() { throw new InvalidOperationException(); } void DoInitializeEntity(TEntity entity) { if (_initializeEntity != null) _initializeEntity(entity); } public DbSqlQuery SqlQuery(string sql, params object[] parameters) { return _set.SqlQuery(sql, parameters); } } public class EFDbContext : DbContext { public IDbSet AppUser { get; set; } public IDbSet Tenant { get; set; } public IDbSet Employee { get; set; } ///this makes sure the naming convention does not have to be plural ///tables can be anything we name them to be protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Conventions.Remove(); } public EFDbContext(int tenantID = 0) //Constructor of the class always expect a tenantID { //Here, the Dbset can expose the unfiltered data AppUser = new FilteredDbSet(this); Tenant = new FilteredDbSet(this); //From here, add all the multitenant dbsets with filtered data Employee = new FilteredDbSet(this, d => d.TenantID == tenantID); } } public interface IEmployeeRepository { IQueryable Employees { get; } void SaveEmployee(Employee Employee); void DeleteEmployee(Employee Employee); List GetEmployeesSorted(); } public class EFEmployeeRepository : IEmployeeRepository { private EFDbContext context; public EFEmployeeRepository(int tenantID = 0) { context = new EFDbContext(tenantID); } IQueryable IEmployeeRepository.Employees { get { return context.Employee; } } public void SaveEmployee(Employee Employee) { if (Employee.EmployeeID == 0) { context.Employee.Add(Employee); } context.SaveChanges(); } public void DeleteEmployee(Employee Employee) { context.Employee.Remove(Employee); context.SaveChanges(); } public List GetEmployeesSorted() { //This is just a function to see the how the results are fetched. return context.Employee.OrderBy(m => m.FullName) .ToList(); //I haven't used where condition to filter the employees since it should be handled by the filtered context } } 

4)WEB层:使用Ninject DI的ASP.NET MVC 4 Internet应用程序

 public class NinjectControllerFactory : DefaultControllerFactory { private IKernel ninjectKernel; public NinjectControllerFactory() { ninjectKernel = new StandardKernel(); AddBindings(); } protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType) { return controllerType == null ? null : (IController)ninjectKernel.Get(controllerType); } private void AddBindings() { ninjectKernel.Bind().To(); ninjectKernel.Bind().To(); } } 

5) 控制器。 这是问题所在

 public class HomeController : Controller { IEmployeeRepository repoEmployee; public HomeController(IEmployeeRepository empRepository) { //How can I make sure that the employee is filtered globally by supplying a session variable of tenantID //Please assume that session variable has been initialized from Login modules after authentication. //There will be lots of Controllers like this in the application which need to use these globally filtered object repoEmployee = empRepository; } public ActionResult Index() { //The list of employees fetched must belong to the tenantID supplied by session variable //Why this is needed is to secure one tenant's data being exposed to another tenants accidently like, if programmer fails to put where condition List Employees = repoEmployee.Employees.ToList(); return View(); } } 

NInject DI可以做到神奇! 如果您有一个登录例程,它会创建会话变量“thisTenantID”。

在Web层:

 private void AddBindings() { //Modified to inject session variable ninjectKernel.Bind().ToMethod(c => new EFDbContext((int)HttpContext.Current.Session["thisTenantID"])); ninjectKernel.Bind().To(); ninjectKernel.Bind().To().WithConstructorArgument("tenantID", c => (int)HttpContext.Current.Session["thisTenantID"]); } 

您设计存储库的方式遵循非常清晰的设计,但是在构造函数中传递的参数在使用依赖项注入时会使事情变得复杂一些。

我在下面提出的建议可能不是最好的设计,但它可以让您在不对现有代码进行太多更改的情况下继续进行。

此解决方案中的问题是您在创建控制器时必须调用“初始化”方法,这可能是您可能不喜欢的,但它非常有效。

以下是步骤:

  • IEmployeeRepository中创建一个新方法
 public interface IEmployeeRepository { //leave everything else as it is void Initialise(int tenantId); } 
  • EFEmployeeRepository中实现该方法
 public class EFEmployeeRepository { //leave everything else as it is public void Initialise(int tenantID = 0) { context = new EFDbContext(tenantID); } } 
  • HomeController中 ,您需要在构造函数中调用“Initialise”
 public HomeController(IEmployeeRepository empRepository) { repoEmployee = empRepository; repoEmployee.Initialise(/* use your method to pass the Tenant ID here*/); } 

这种方法的另一种方法是创建一个RepositoryFactory,它将返回填充了所有需要的filter的Repository。 在这种情况下,您将把Factory而不是存储库注入Controller。