How to implement the default presented in C # with EntityFramework?

6

I'm thinking of a way to apply the pattern ( multi-tenant ), raised on this issue ( Standard that has contributed to the reliability of software that needs to meet complex models such as multi-business models ) EntityFramework .

As the title of the issue presents, the standard is based on a way to protect the application so that data from a Reseller, its client companies and its customers do not have their data presented to others.

  

I believe the multi-tenant (" multi-tenant ") would be appropriate for this case - even if it is a single operator of a single company accessing the various tenants. This article briefly describes the philosophy behind the multitenancy .

It's a multi-profile access application.

So, I thought about doing this in a base class for the repositories. So I started by creating something like this:

An interface to set a pattern:

public interface IRepository<TContext, T, TKey> : IDisposable
    where TContext : DbContext, new()
    where T : class
{
    T Get(TKey id);
    IQueryable<T> GetAll(Expression<Func<T, bool>> filter);

    IQueryable<T> Query(params Expression<Func<T, object>>[] includes);

    T Add(T entity);
    List<T> AddRange(List<T> items);
    bool Edit(T entity);
    bool Delete(TKey id);
    bool Delete(T entity);
    int Delete(Expression<Func<T, bool>> where);

    int SaveChanges();
}

And an abstract class, which implements this interface, to be inherited by the repository classes:

public abstract class CustomRepository<TContext, T, TKey> : IRepository<TContext, T, TKey>
    where TContext : DbContext, new()
    where T : class
{
    private TContext _context = null;
    private bool _responsibleContext = false;

    /// <summary>
    /// constructor
    /// </summary>
    public CustomRepository()
    {
        _context = new TContext();
        _responsibleContext = true;
    }

    /// <summary>
    /// constructor with a DbContext param
    /// </summary>
    /// <param name="context">A DbContext param</param>
    public CustomRepository(TContext context)
    {
        _context = context;
        _responsibleContext = false;
    }

    /// <summary>
    /// disposer
    /// </summary>
    public void Dispose()
    {
        if (_responsibleContext && _context != null)
            _context.Dispose();
        GC.SuppressFinalize(this);
    }

    #region other interface implementations ...

    public T Get(TKey id)
    {
        return _context.Set<T>().Find(id);
    }

    public IQueryable<T> GetAll(Expression<Func<T, bool>> filter)
    {
        return Query().Where(filter);
    }

    public IQueryable<T> Query(params Expression<Func<T, object>>[] includes)
    {
        IQueryable<T> set = _context.Set<T>();
        foreach (var include in includes)
            set = set.Include(include);
        return set;
    }

    public T Add(T entity)
    {
        _context.Set<T>().Add(entity);
        SaveChanges();
        return entity;
    }

    public List<T> AddRange(List<T> items)
    {
        _context.Set<T>().AddRange(items);
        SaveChanges();
        return items;
    }

    public bool Edit(T entity)
    {
        var result = false;
        _context.Entry<T>(entity).State = EntityState.Modified;
        if (SaveChanges() > 0)
            result = true;
        return result;
    }

    public bool Delete(TKey id)
    {
        var entity = Get(id);
        return Delete(entity);
    }

    public bool Delete(T entity)
    {
        _context.Entry(entity).State = EntityState.Deleted;
        return SaveChanges() > 0;
    }

    public int Delete(Expression<Func<T, bool>> where)
    {
        var entries = Query().Where(where);
        _context.Set<T>().RemoveRange(entries);
        return SaveChanges();
    }

    public int SaveChanges()
    {
        return _context.SaveChanges();
    }

    #endregion other interface implementations ...
}

And if I understood the standard proposal well, I would have to pass an instance reference from my User's session, so I could change the methods to something like:

public T Get(TKey id)
{
    var entry = _context.Set<T>().Find(id);
    if (entry.RevendaId == userSession.RevendaId && 
      entry.EmpresaId == userSession.EmpresaId && 
      entry.ClienteId == userSession.ClienteId)
        return entry;
    else
        return null;
}

And also:

public IQueryable<T> Query(params Expression<Func<T, object>>[] includes)
{
    IQueryable<T> set = _context.Set<T>();
    foreach (var include in includes)
        set = set.Include(include);

    set = set.Where(x => x.RevendaId == userSession.RevendaId &&
      x.EmpresaId == userSession.EmpresaId &&
      x.ClienteId == userSession.ClienteId);

    return set;
}

And apply this approach in all methods.

Well, I have two questions :

  • Would this approach attempt to implement the pattern be a correct one?

    1.1. If not, what would a correct implementation of this pattern be in the scenario presented?

  • If yes, how would I pass a Session User instance to the repository instance?

  • asked by anonymous 17.09.2014 / 15:22

    3 answers

    3

    What you're calling the repository is not quite the default repository , since you're letting IQueryable leak ... but I do not want to go into too many details about naming as this would be very purist . That said, I will continue to refer to the pattern described by you as a repository.

    What seems to me to be your goal is:

    • create an encapsulated component that can be reused

    • apply filters on all queries, so that users of a tenant A can not operate on the data of another tenant B

    • Pass the logged in user as a parameter of this component, and use that information in the filter (ie the logged in user is associated with a specific tenant)

    My suggestions for achieving your goal:

    • use dependency injection to inject the IRepository<T> interface wherever you want (your dependents). Example:

      class EuDependoDeUmRepositorio
      {
          IRepository<MinhaEntidade> repositorio;
          public EuDependoDeUmRepositorio(IRepository<MinhaEntidade> repositorio)
          {
              this.repositorio = repositorio;
          }
      
          public void FazerAlgumaCoisa()
          {
              // operações sobre 'this.repositorio'
          }
      }
      
    • Make a generic implementation of IRepository<T> dependent on an interface that represents the logged in user, which will be injected into it ... example ISessaoUsuario

      class Repository<T> : IRepository<T>
      {
          ISessaoUsuario sessaoUsuario;
          public Repository(ISessaoUsuario sessaoUsuario)
          {
              this.sessaoUsuario = sessaoUsuario;
          }
      
          // mais detalhes sobre os outros métodos abaixo
      }
      
    • In concrete implementation of IRepository<T> , in all methods of obtaining data, use a Expression<T> visitor to change IQueryable<T> and apply the filters automatically, in the properties that identify the tenant.

      That is, turn this:

      db.MinhaEntidade.Where(e => e.Nome == "xpto")
      

      Automatically this:

      db.MinhaEntidade.Where(e => e.Nome == "xpto"
          && e.InquilinoId == this.SessaoUsuario.InquilinoId)
      

      Manipulating objects Expression<T> is the hardest part, but think about it, it's almost generic for any ORM that supports LINQ ... it's very reusable, it's worth it.

    • In this implementation, in the save method, get all the entities that will be saved and verify that the entity is being saved in the correct tenant . To do this, I suggest that entities implement an interface of type IInquilinoEspecifico , which allows you to easily obtain the tenant of the object being saved.

       foreach (var e in entidadesModificadas.OfType<IInquilinoEspecifico>())
       {
           if (e.InquilinoId != this.SessaoUsuario.InquilinoId)
               throw new Exception("Você não pode salvar dados de um inquilino diferente do que o usuário logado");
       }
      
    17.09.2014 / 20:12
    2

    I'll try to give you an answer that is not opinionated.

    Would this approach attempt to implement the pattern be a correct one?

    Apparently your architecture uses a database only, and every query always takes into account a composite key schema. There are some issues:

    • It is unclear how the session will be passed into the repository;
    • It's not clear how persistence will be done;
    • Some things are still long, such as:

      public bool Edit(T entity)
      {
          var result = false;
          _context.Entry<T>(entity).State = EntityState.Modified;
          if (SaveChanges() > 0)
              result = true;
          return result;
      }
      

      And that could be simplified to:

      public bool Edit(T entity)
      {
          _context.Entry<T>(entity).State = EntityState.Modified;
          return SaveChanges() > 0;
      }
      

    From an Entity Framework perspective, you are underapproving the Framework in favor of an unobtrusive approach, and adding a composite keystroke, whose configuration is possible, but raises the complexity of your application unnecessarily.

    Incidentally, I already replied to @Rod here : For organization, isolation and simplicity, the ideal is to separate by database , adding an additional database to register companies and users.

    If yes, how would I pass a Session User instance to the repository instance?

    It's a doubt that I have, inclusively. Instead, I would do it in the repository builder:

    public CustomRepository(int RevendaId, int EmpresaId, int ClienteId)
    {
        _context = new TContext();
        _responsibleContext = true;
        this.RevendaId = RevendaId;
        this.EmpresaId = EmpresaId;
        this.ClienteId = ClienteId;
    }
    
        
    17.09.2014 / 19:23
    0

    You could study the possibility of separating banks by tenants. For a possible restore it would be much quieter.

    I adopted the following strategy for an application that I developed. One of the options when it came to knowing which tenancy had to load was by the client domain. In the client control panel it should provide the contract (id), login and password for me to upload its data.

    In the constructor of my DBContext I gave a replace in XXX.

    public class KFMAutoXXXContext : DbContext
    {
    public MyXXXContext(string database) : base(ConfigurationManager.ConnectionStrings["MyXXXContext"].ToString().Replace("XXX", database))
    {
    }
    }
    

    My connection string contained this XXX in the DB name.

    In my project I had an administrative DB where I could set up new contracts, disable tenants, issue invoices, etc.

        
    12.11.2016 / 21:45