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。