Search code examples
c#entity-frameworkasp.net-coremodel-view-controllerdbcontext

Access DbContext in Model to set properties and apply global filter for multitenant webapp


I have a MultiTenant WebApp that incorporates TenantService. The DbContext config is loaded at runtime where the connectionstrings come from another server. Ignored here for simplification as this is working fine.

Each BaseItem has property Sharedwith, either Public,Private, Tenant or Archived from an enum; Each BaseItem has property TenantId and CreatedByUserId, depending who created the item. Each BaseItem has unmapped property canView, which is calculated at runtime to true or false, true for Public, true if loggedinuser = createdbyuserid for Private, true if TenantId = loggedinuser's TenantId.

Questions:

  1. In my CheckifCanView, how can I access ApplicationDbContext _context to get TenantId and LoggedInUserId?
  2. In DbCOntext I filter canView foreach type of BaseItem. I cannot do modelBuilder.Entity<BaseItem>().HasQueryFilter(x => x.canView == true); in OnModelCreating because obviously I first need to calculate canView. Are there any other way I can globally filter all BaseItems?. I have hundreds of them.
        public IEnumerable<TestItem1> GetTestItem1s() {
            return TestItem1s.AsEnumerable().Where(x => x.canView == true); ;
        }

What I tried: I have previously calculated canView in a foreach in the controller and returned model.Where(x=>x.canView==true) to the View, but this not very efficient. I have tried Injecting another Userservice, but since the data comes from the same DbContext and the DbContext is configured at runtime, it's a no go. I tried to get access to httpcontext since I can then store and retrieve them from headers or cookies, but can't get httpcontext injected.

namespace WebApplication1
{
    public interface ITenantService
    {
        public int GetTenantId();
        public int GetLoggedInUserId();
    }
    public class TenantService : ITenantService
    {
        public int GetTenantId() { return 1; }
        public int GetLoggedInUserId() { return 1; }
    }
    public class ApplicationDbContext : IdentityDbContext
    {
        public int TenantId;
        public int LoggedInUserId;
        private readonly ITenantService _tenantService;
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options, ITenantService tenantService)
        : base(options)
        {
            _tenantService = tenantService;
            TenantId = _tenantService.GetTenantId();
            LoggedInUserId = _tenantService.GetLoggedInUserId();
        }
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            //CANT FILTER BEFORE CANVIEW VALUE SET
            //modelBuilder.Entity<BaseItem>().HasQueryFilter(x => x.canView == true);
 
        }
        public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = new CancellationToken()) {
            foreach (var entry in ChangeTracker.Entries<hasTenant>().ToList())
            {
                switch (entry.State)
                {
                    case EntityState.Added:
                        //entry.Entity.TenantId = TenantId;
                        break;
                }
            }
            foreach (var entry in ChangeTracker.Entries<hasUser>().ToList())
            {
                switch (entry.State)
                {
                    case EntityState.Added:
                        //entry.Entity.CreatedbyUserId = TenantId;
                        break;
                }
            }
            var result = await base.SaveChangesAsync(cancellationToken);
            return result;
        }
        public DbSet<TestItem1> TestItem1s { get; set; } = default;
        public DbSet<TestItem2> TestItem2s { get; set; } = default;


        //REFER TO QUESTION 2. Apply global filter for canView==true but only after it has been calculated. 
        public IEnumerable<TestItem1> GetTestItem1s() {
            return TestItem1s.AsEnumerable().Where(x => x.canView == true); ;
        }
        public IEnumerable<TestItem2> GetTestItem2s()
        {
            return TestItem2s.AsEnumerable().Where(x => x.canView == true); ;
        } 
    }
    public enum Sharedwith
    {
        Public,
        Private,
        Tenant,
        Archive
    }
    public interface hasTenant
    {
        public int TenantId { get; set; }
    }
    public interface hasUser
    {
        public int CreatedbyUserId { get; set; }
    }
    public class BaseItem : hasTenant, hasUser
    {
        public int Id { get; set; }
        public int TenantId { get; set; }
        public string Description { get; set; }
        public int CreatedbyUserId { get; set; }
        public Sharedwith Sharedwith { get; set; }
        [NotMapped]
        public bool canView
        {
            get
            { 
                return Extentions.CheckifCanView(this.TenantId, this.CreatedbyUserId, this.Sharedwith);
            }
            set { }
        } 
    }
    public class TestItem1 : BaseItem, hasTenant, hasUser
    {
        public string CustomProp { get; set; }
    }
    public class TestItem2 : BaseItem, hasTenant, hasUser
    {
        public string CustomProp { get; set; }
    }
    public static class Extentions
    {
        //CANT USE THIS IN STATIC CLASS
        //readonly ApplicationDbContext _context;
        //public Extentions(ApplicationDbContext _context) {
        //    _context = _context;
        //}
        public static bool CheckifCanView(int TenantId, int CreatedbyUserId, Sharedwith Sharedwith)
        {
            //CANT USE THIS IN STATIC CLASS
            //var _context = new ApplicationDbContext();
            //var c = _context.TenantId;
            
             

            //REFER TO QUESTION 1. This must come from _context. 
            var contextTenantid = 1; //var contextTenantid = _context.TenantId;
            var contextUsertid = 1; //var contextUsertid = _context.LoggedInUserId;

            switch (Sharedwith)
            {
                case Sharedwith.Public: return true; break;
                case Sharedwith.Private: return CreatedbyUserId==contextUsertid; break;
                case Sharedwith.Tenant: return TenantId==contextTenantid; break;
                case Sharedwith.Archive: return false; break;
                default: return false;
            }
        }
    }
}

Here is Git Source code


Solution

  • I ended up not including the context into the model, but rather create an extention to do the calculation. Don't know if its the most sufficient or if it can be improved somewhat, but for now, it's working. Hopefully it can also help someone else.

    If anyone has more ideas please let me know.

    First in the model, set a normal canView property:

    [NotMapped]
    public bool canView { get; set; }
    

    Change the class Extentions to accept ApplicationDbContext _context

    public static class Extentions
    {
        public static bool CheckifCanView(int TenantId, int CreatedbyUserId, Sharedwith Sharedwith, ApplicationDbContext _context)
        {
            var contextTenantid = _context.TenantId;
            var contextUsertid = _context.LoggedInUserId;
            switch (Sharedwith)
            {
                case Sharedwith.Public:
                    return true;
                    break;
                case Sharedwith.Private:
                    return CreatedbyUserId == contextUsertid;
                    break;
                case Sharedwith.Tenant:
                    return TenantId == contextTenantid;
                    break;
                case Sharedwith.Archive:
                    return false;
                    break;
                default:
                    return false;
            }
        }
    }
    

    Then I added a new DbSetExtensions class:

    public static class DbSetExtensions
    {
        public static IQueryable < T > canView < T > (this IQueryable < T > t, ApplicationDbContext _context) where T: BaseItem
        {
            var newt = new List < T > ();
            foreach(var item in t)
            {
                item.canView = Extentions.CheckifCanView(item.TenantId, item.CreatedbyUserId, item.Sharedwith, _context);
                if (item.canView == true) newt.Add(item);
            }
            return newt.AsQueryable();
        }
    }
    

    So now from my controller, I can do the following:

    public IActionResult Index()
    {
        var model = _context.TestItem1s.canView(_context);
        return View(model);
    }
    

    This way I can have multiple BaseItem's where I can calculate the canView property based on the context's logged in user and it returns only the ones that the user has permission for, simply by adding .canView(_context)