Многопользовательское веб-приложение с отфильтрованным dbContext

Я новичок в ASP.Net MVC и мультитенантном веб-приложении. Я много читал, но, будучи новичком, просто следую тому, что понимаю. Итак, мне удалось создать пример веб-приложения со сценарием, и мне нужно решить его конечную часть. Надеюсь, этот сценарий будет полезен и для некоторых других новичков, но мы приветствуем любой другой подход. заранее спасибо

1) База данных в SQLServer 2008.

введите описание изображения здесь

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 Класс: Зоран Максимович

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) ВЕБ-уровень: Интернет-приложение ASP.NET MVC 4 с 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) Контроллер. Вот в чем проблема.

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();
    }
}

person Sindhoor    schedule 11.03.2013    source источник
comment
Я могу придумать более простые альтернативы, одну базу данных на каждого арендатора или одну схему на каждого арендатора. Но это то, чего вы хотите?   -  person flup    schedule 12.03.2013
comment
Если бы это был я, я бы сделал так, чтобы ваш API (действия контроллера) требовал идентификатора клиента, а затем проверял действие контроллера для сеанса. Таким образом, вы просто авторизуетесь на уровне запроса.   -  person blins    schedule 12.03.2013
comment
одна база данных на каждого арендатора, честно говоря, не особенно хорошо масштабируется из-за ряда вещей, таких как фрагментация пула соединений, несколько схем и т. д. единая база данных для всех клиентов, которую вы можете сегментировать внутри, на мой взгляд, масштабируется намного лучше.   -  person ryancrawcour    schedule 12.03.2013
comment
@ryancrawcour По моему опыту, One-DB-per-tenant отлично масштабируется до сотен клиентов, и к тому времени вы все равно добавите дополнительное оборудование. У меня не было проблем с фрагментацией в моих приложениях.   -  person Dai    schedule 12.03.2013


Ответы (2)


NInject D Я могу творить чудеса !! При условии, что у вас будет процедура входа в систему, которая создает переменную сеанса thisTenantID.

На веб-уровне:

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"]);
}
person jpWebLearner    schedule 12.03.2013

То, как вы спроектировали свой репозиторий, следует очень четкому дизайну, но параметр, который вы передаете в конструктор, немного усложняет работу при использовании внедрения зависимостей.

То, что я предлагаю здесь ниже, возможно, не лучший дизайн, но он позволит вам развиваться, не внося слишком больших изменений в существующий код.

Загвоздка в этом решении заключается в том, что при создании контроллера вам необходимо вызвать метод «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 вам нужно будет вызвать "Initialise" в конструкторе.
public HomeController(IEmployeeRepository empRepository)
{
    repoEmployee = empRepository;
    repoEmployee.Initialise(/* use your method to pass the Tenant ID here*/);
}

Альтернативой этому подходу может быть создание RepositoryFactory, который вернет репозиторий, заполненный всеми необходимыми фильтрами. В этом случае вы вставите в контроллер Factory, а не репозиторий.

person zoranmax    schedule 12.03.2013