6

Jestem nowym użytkownikiem aplikacji sieci ASP.Net MVC i multi-tenancy. Dużo czytałem, ale będąc początkującym, podążam za tym, co rozumiem. Tak więc udało mi się zbudować przykładową aplikację internetową i trzeba rozwiązać końcową jej część. Mam nadzieję, że ten scenariusz przyda się także niektórym innym początkującym, ale z zadowoleniem przyjmie inne podejście. Dzięki z góryAplikacja internetowa dla wielu dzierżawców z filtrowanym dbContext

1) Baza danych w SQLServer 2008

enter image description here

2) Warstwa danych: Projekt # klasa biblioteka C nazywa 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). Warstwa działalności: klasa MyApplication.Business biblioteka Po FilteredDbSet Class uprzejmości: 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 warstwy: Aplikacja ASP.NET MVC 4 Internet z 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) kontrolera. Tutaj jest problem

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

mogę myśleć prostsze alternatywy, jedna baza danych za lokatora lub najemcy za jeden schemat. Tego właśnie chcesz? – flup

+2

Jeśli to ja, sprawiłbym, że twój interfejs API (akcje kontrolera) wymaga identyfikatora dzierżawcy, a następnie sprawdzi się w akcji kontrolera względem sesji. W ten sposób autoryzujesz tylko na poziomie żądania. – blins

+2

jedna baza danych na dzierżawcę nie jest szczególnie skalowalna, aby być uczciwym z powodu wielu rzeczy, takich jak fragmentacja puli połączeń, wiele schematów itp. pojedyncza baza danych dla wszystkich lokatorów, którą można odfiltrować wewnętrznie, moim zdaniem znacznie lepiej . – ryancrawcour

Odpowiedz

6

NInject DI może zrobić magię !!Pod warunkiem, że będziesz mieć procedurę logowania, która tworzy zmienną sesji "thisTenantID".

w warstwie internetowej:

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

Czy można to zrobić również w Jedności? –

1

Sposób, w jaki zostały zaprojektowane repozytorium następuje bardzo jasną konstrukcję, ale parametr, które przechodzą w konstruktorze sprawia, że ​​rzeczy nieco bardziej skomplikowane przy użyciu iniekcji zależność.

To, co proponuję poniżej, może nie jest najlepszym wzornictwem, ale pozwoli na postęp bez wprowadzania zbyt dużych zmian w istniejącym kodzie.

Haczyk w tym rozwiązaniu jest to, że trzeba wywołać metodę „Uruchomienie” podczas tworzenia kontrolera, który potencjalnie może nie podoba, ale jest to dość skuteczne.

Oto kroki:

  • Załóż nową metodę w swojej IEmployeeRepository
public interface IEmployeeRepository 
{ 
    //leave everything else as it is 
    void Initialise(int tenantId); 
} 
  • wdrożenia tej metody w EFEmployeeRepository
public class EFEmployeeRepository 
{ 
    //leave everything else as it is 

    public void Initialise(int tenantID = 0) 
    { 
     context = new EFDbContext(tenantID); 
    } 
} 
  • W HomeController, trzeba by nazwać „Uruchomienie” w konstruktorze
public HomeController(IEmployeeRepository empRepository) 
{ 
    repoEmployee = empRepository; 
    repoEmployee.Initialise(/* use your method to pass the Tenant ID here*/); 
} 

Alternatywą dla takiego podejścia mogłoby być stworzenie RepositoryFactory że wróci do repozytorium wypełniony z wszystkimi filtrami, których potrzebujesz. W takim przypadku wprowadzisz do sterownika fabrykę, a nie repozytorium.

Powiązane problemy