6

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

1)数据库中的SQLServer 2008

enter image description here

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

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 Class礼貌:佐兰·梅克赛莫维奇

public class FilteredDbSet<TEntity> : IDbSet<TEntity>, IOrderedQueryable<TEntity>, IOrderedQueryable, IQueryable<TEntity>, IQueryable, IEnumerable<TEntity>, IEnumerable, IListSource 
    where TEntity : class 
    { 
     private readonly DbSet<TEntity> _set; 
     private readonly Action<TEntity> _initializeEntity; 
     private readonly Expression<Func<TEntity, bool>> _filter; 

     public FilteredDbSet(DbContext context) 
      : this(context.Set<TEntity>(), i => true, null) 
     { 
     } 

     public FilteredDbSet(DbContext context, Expression<Func<TEntity, bool>> filter) 
      : this(context.Set<TEntity>(), filter, null) 
     { 
     } 

     public FilteredDbSet(DbContext context, Expression<Func<TEntity, bool>> filter, Action<TEntity> initializeEntity) 
      : this(context.Set<TEntity>(), filter, initializeEntity) 
     { 
     } 

     public Expression<Func<TEntity, bool>> Filter 
     { 
      get { return _filter; } 
     } 

     public IQueryable<TEntity> Include(string path) 
     { 
      return _set.Include(path).Where(_filter).AsQueryable(); 
     } 

     private FilteredDbSet(DbSet<TEntity> set, Expression<Func<TEntity, bool>> filter, Action<TEntity> initializeEntity) 
     { 
      _set = set; 
      _filter = filter; 
      MatchesFilter = filter.Compile(); 
      _initializeEntity = initializeEntity; 
     } 

     public Func<TEntity, bool> MatchesFilter 
     { 
      get; 
      private set; 
     } 

     public IQueryable<TEntity> 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<TDerivedEntity>() where TDerivedEntity : class, TEntity 
     { 
      var entity = _set.Create<TDerivedEntity>(); 
      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); 
     } 

     /// <summary> 
     /// Returns the items in the local cache 
     /// </summary> 
     /// <remarks> 
     /// It is possible to add/remove entities via this property that do NOT match the filter. 
     /// Use the <see cref="ThrowIfEntityDoesNotMatchFilter"/> method before adding/removing an item from this collection. 
     /// </remarks> 
     public ObservableCollection<TEntity> Local 
     { 
      get { return _set.Local; } 
     } 

     IEnumerator<TEntity> IEnumerable<TEntity>.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<TEntity> SqlQuery(string sql, params object[] parameters) 
     { 
      return _set.SqlQuery(sql, parameters); 
     } 
    } 

public class EFDbContext : DbContext 
{ 
    public IDbSet<AppUser> AppUser { get; set; } 
    public IDbSet<Tenant_SYS> Tenant { get; set; } 
    public IDbSet<Employee> 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<PluralizingTableNameConvention>(); 
    } 

    public EFDbContext(int tenantID = 0) //Constructor of the class always expect a tenantID 
    { 
     //Here, the Dbset can expose the unfiltered data    
     AppUser = new FilteredDbSet<AppUser>(this); 
     Tenant = new FilteredDbSet<Tenant_SYS>(this); 

     //From here, add all the multitenant dbsets with filtered data 
     Employee = new FilteredDbSet<Employee>(this, d => d.TenantID == tenantID); 
    } 
} 

public interface IEmployeeRepository 
{ 
    IQueryable<Employee> Employees { get; } 
    void SaveEmployee(Employee Employee); 
    void DeleteEmployee(Employee Employee); 
    List<Employee> GetEmployeesSorted(); 
} 

public class EFEmployeeRepository : IEmployeeRepository 
{ 
    private EFDbContext context; 

    public EFEmployeeRepository(int tenantID = 0) 
    { 
     context = new EFDbContext(tenantID); 
    } 

    IQueryable<Employee> 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<Employee> 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层:ASP.NET MVC与Ninject DI

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<IAppUserRepository>().To<EFAppUserRepository>(); 
     ninjectKernel.Bind<IEmployeeRepository>().To<EFEmployeeRepository>(); 

    } 
} 

5)控制器4互联网应用。这里的问题是

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<Employee> Employees = repoEmployee.Employees.ToList(); 
     return View(); 
    } 
} 
+1

我能想象的更简单的替代品,一个数据库,或每个租户一个模式。这是你想要的,但? – flup 2013-03-11 22:35:14

+2

如果是我,我会做你的API(控制器的动作)要求,则租户ID在对会话控制器的动作验证。这样,您只需在请求级授权。每个租户 – blins 2013-03-11 22:56:40

+2

一个数据库不因一些事情像连接池碎片,多种模式等 对于您可以在内部进行分片,在我看来,所有租户的单一数据库,扩展更好的扩展特别是诚实的。 – ryancrawcour 2013-03-12 00:45:44

回答

6

NInject DI可以做魔术!假设您将拥有一个创建会话变量“thisTenantID”的登录例程。

在Web层:每个租户

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

    ninjectKernel.Bind<IAppUserRepository>().To<EFAppUserRepository>(); 
    ninjectKernel.Bind<IEmployeeRepository>().To<EFEmployeeRepository>().WithConstructorArgument("tenantID", c => (int)HttpContext.Current.Session["thisTenantID"]); 
} 
+0

这可以在Unity中完成吗? – 2015-04-07 05:32:58

1

你设计你的资料库遵循一个非常明确的设计方式,但使用依赖注入时要传递在构造函数中的参数使事情变得有点复杂。

我在下面提出的建议可能不是最好的设计,但它可以让您在不对现有代码做太多修改的情况下取得进展。

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

步骤如下:

  • 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中,则需要在构造函数中调用“初始化”
public HomeController(IEmployeeRepository empRepository) 
{ 
    repoEmployee = empRepository; 
    repoEmployee.Initialise(/* use your method to pass the Tenant ID here*/); 
} 

这种方法的另一种可能是创建一个RepositoryFactory,将返回填充存储库用你需要的所有过滤器。在这种情况下,您将向Factory注入Factory而不是Repository。