6

Sono nuovo di ASP.Net MVC e applicazione web multi-tenancy. Ho letto molto, ma essendo un principiante seguo solo ciò che ho capito. Così sono riuscito a creare un'applicazione web di esempio di esempio e ho bisogno di risolverne la parte finale. Spero che questo scenario sia utile anche per altri principianti, ma gradirebbe qualsiasi altro approccio. Grazie in anticipoApplicazione Web multi-tenant con filtro dbContext

1) del database in SQL Server 2008.

enter image description here

2) livello di Data: C# progetto di libreria di classi denominato 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). Business Layer: Libreria di classi MyApplication.Business seguito FilteredDbSet Class cortesia: Zoran Maksimovic

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 Strato: ASP.NET MVC Application 4 Internet con 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) Controller. Qui è il problema

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

posso pensare di alternative più semplici, un database per ogni inquilino, o uno schema per inquilino. Questo è quello che vuoi, però? – flup

+2

Se fosse per me, renderei l'API (azioni del controller) che richiede l'ID tenant, quindi convalidare l'azione del controller sulla sessione. In questo modo stai solo autorizzando a livello di richiesta. – blins

+2

un database per inquilino non è particolarmente adatto per essere onesto a causa di una serie di frammentazione del pool di connessioni, più schemi ecc. un unico database per tutti gli inquilini che è possibile suddividere internamente, a mio parere, scala molto meglio . – ryancrawcour

risposta

6

NInject DI può fare la magia !!A condizione che si abbia una routine di login che crea la variabile di sessione "thisTenantID".

nel Livello 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

Questo può essere fatto anche in Unity? –

1

Il modo in cui è stato progettato il repository segue un disegno molto chiaro, ma il parametro che si sta passando nel costruttore rende le cose un po 'più complicate quando si utilizza l'iniezione di dipendenza.

Quello che propongo qui di seguito, non è forse il miglior design, ma ti permetterà di progredire senza apportare troppe modifiche al codice esistente.

Il trucco in questa soluzione è che devi chiamare il metodo "Inizializza" quando crei il controller, che potenzialmente potresti non gradire, ma è piuttosto efficace.

Ecco i passaggi:

  • creare un nuovo metodo nella IEmployeeRepository
public interface IEmployeeRepository 
{ 
    //leave everything else as it is 
    void Initialise(int tenantId); 
} 
  • attuare tale metodo nella EFEmployeeRepository
public class EFEmployeeRepository 
{ 
    //leave everything else as it is 

    public void Initialise(int tenantID = 0) 
    { 
     context = new EFDbContext(tenantID); 
    } 
} 
  • Nel HomeController, si avrebbe bisogno di chiamare "inizializzare" nel costruttore
public HomeController(IEmployeeRepository empRepository) 
{ 
    repoEmployee = empRepository; 
    repoEmployee.Initialise(/* use your method to pass the Tenant ID here*/); 
} 

Un'alternativa a questo approccio potrebbe essere quello di creare un RepositoryFactory che sarebbe tornato il Repository riempita con tutti i filtri necessari. In tal caso inietterai la Fabbrica anziché il Repository sul Controller.