Multi-Tenancy / Global Filters in Entity Framework

Multi-Tenancy / Global Filters in Entity Framework

In this article I am going to describe a method to add global filters in Entity framework. The version I used for this example is 6.1

What does it do?

Imagine that you have a multi-tenant database i.e. in a single database you have data for multiple tenants. The way to do this in general is to add a column for “tenantId” (or equivalent) in every table that requires to be tenant specific.

What can work (but not really)?

Every query that a developer writes must take into account tenancy, in Lambda expressions, complex linq queries with joins and all and to make sure that you do not run into a disaster situation where tenant Y’s data starts to show up for tenant X you will have to setup a strict review process where every new query written needs to be scrutinized (cough).

The Code

The first thing you need to make sure is that you DBContext class exposes IDbSet<> instead of DbSet<> e.g.

    public class MyDataContext : DbContext
    {
         public IDbSet<MyEntity> MyEntities { get; set; }
    }

Then you need your own implementation of a DbSet which wraps over the functionality of the default one:

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 => _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 => _set.Local;

        IEnumerator<TEntity> IEnumerable<TEntity>.GetEnumerator()
        {
            return _set.Where(_filter).GetEnumerator();
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return _set.Where(_filter).GetEnumerator();
        }

        Type IQueryable.ElementType => typeof(TEntity);

        Expression IQueryable.Expression => _set.Where(_filter).Expression;

        IQueryProvider IQueryable.Provider => _set.AsQueryable().Provider;

        bool IListSource.ContainsListCollection => false;

        IList IListSource.GetList()
        {
            throw new InvalidOperationException();
        }

        private void DoInitializeEntity(TEntity entity)
        {
            _initializeEntity?.Invoke(entity);
        }

        public DbSqlQuery<TEntity> SqlQuery(string sql, params object[] parameters)
        {
            return _set.SqlQuery(sql, parameters);
        }
    }

The two important points to note above is that we can now pass an expression which will act as a where clause and another expression which gets run while initializing (adding) an entity. Both the latter expressions combined ensures that:

  1. A person will not get what does not belong to the person.
  2. A person cannot add an Entity into the realm of another tenant.

Now if we replace the DbSet with something like this:

this.MyEntities = new FilteredDbSet<MyEntity>(this, x => x.TenantId == _tenantId, x => x.TenantId = _tenantId);

we should be good to go.

There is one catch though, replacing the DbSet although seems (and ideally should be) simple, it really isn’t. If we try to do it while the DbContext initializes, e.g. its constructor, we get an error as the EF hasn’t yet prepared its own models, and no putting this in onModelCreating doesn’t help either as the actual model creation happens after that.

One solution is to do this via a database initializer, like so:

public sealed class TenancyInitializer<TContext> : IDatabaseInitializer<TContext>
        where TContext : DbContext, IMultiTenantContext
    {
        private readonly IDatabaseInitializer<TContext> _chainedInitializer;

        public bool AllowForcedCallsToInternalInitializer { get; set; }

        private static int _initializeAlreadyCalledFor = 0; //in generic classes, static fields are per generic type

        public TenancyInitializer(IDatabaseInitializer<TContext> chainedInitializer, bool allowForcedCallsToInternalInitializer)
        {
            _chainedInitializer = chainedInitializer;
            AllowForcedCallsToInternalInitializer = allowForcedCallsToInternalInitializer;
        }

        public TenancyInitializer(bool allowForcedCallsToInternalInitializer)
        {
            AllowForcedCallsToInternalInitializer = allowForcedCallsToInternalInitializer;
        }

        public TenancyInitializer(IDatabaseInitializer<TContext> chainedInitializer)
        {
            _chainedInitializer = chainedInitializer;
        }

        public TenancyInitializer()
        { }

        public void InitializeDatabase(TContext context)
        {
            context.ApplyTenancy();

            var initializeAlreadyCalledFor = Interlocked.Exchange(ref _initializeAlreadyCalledFor, 1) == 0;

            if (_chainedInitializer != null && (!initializeAlreadyCalledFor || AllowForcedCallsToInternalInitializer))
                _chainedInitializer.InitializeDatabase(context);
        }
    }

Note that in the example above we left room to add other database intializers for seeding and dropping/creating code-first related database initializers in case we need to use them as well.

Let’s define the Interface which we will apply to our DataContext class which will mark it to have a method for applying tenancy used by the initializer described above.

public interface IMultiTenantContext
    {
        void ApplyTenancy(string tenantId = null);
    }

Below implementation is an example of how the “Schedule” Entity can have that filter applied, note the second expression which is used to intialize a Schedule entity whenever one is added.

public TenantDataContext()
        {
            this.Database.Initialize(true);
        }

public void ApplyTenancy(string tenantId = null)
        {
            if (tenantId != null)
                _tenantId = tenantId; //nope, not thread safe, but then again EF is not thread safe.

            if (_tenantId == null)
            {
                var currentContext = Context.AppContext.Current;

                if (currentContext == null)
                    throw new InvalidOperationException(
                        $"The '{nameof(Context.AppContext)}' was null, without it one tenant might see another tenants data.");

                _tenantId = currentContext.TenantId;
            }

            this.Schedules = new FilteredDbSet<Schedule>(this, x => x.TenantId == _tenantId, x => x.TenantId = _tenantId);
        }

Lastly, for completion sake, here is the context that I was using in the examples above. Using CallContext to keep the context ensures that it will be available throughout the application and will remain unique for a request even if there is multi-threading (or async/await) happening in the code. But this is for convenience, you can use whatever other method of passing the tenant id to the “ApplyTenancy” method of the context above.

[Serializable]
    public sealed class AppContext : MarshalByRefObject, ILogicalThreadAffinative
    {
        public string TenantId { get; }

        private const string Key = "bcc.adv.con.appCon";
        public static AppContext Current
        {
            get { return (AppContext) CallContext.LogicalGetData(Key); }
            private set
            {
                CallContext.LogicalSetData(Key, value);
            }
        }

        public AppContext(string tenantId)
        {
            TenantId = tenantId;
        }

        public void SetIntoContext()
        {
            if (Current != null)
                throw new InvalidOperationException("The context is meant to be immutable, an attempt was made to set the context with the context already having data.");

            Current = this; //ideally this should be made thread-safe, but would slow stuff down. It will be called in an http module or app start (clients) anyway.
        }
    }
Advertisement

3 thoughts on “Multi-Tenancy / Global Filters in Entity Framework

  1. hi nice article, can you add a couple of sample usages identity 2.2, MVC 5 – where the context is setup when user logs in, by getting his tenantId from User.tenantId.

    Also if the Iqueryable is over ridden, then wont the same old calls auto filter the entites based on user tenant a C# that might make this more valubale http://bit.ly/2mveRlx

    Like

    • Would like to see a code sample on CRUD usage as well, do you have github link code the code and sample, or there a nuget on this? guess I could just copy this sweet class, great job BTW!!

      I would greatly like to see your approach in a blog post on the multi-tenant configuration and usage in EF with ASP identity 2.2 MVC 5. Maybe based around a simple use-case, like add manage employee & purchase requests, approvals at a manager, at a per tenant level. For e.g. how to set up tenant Id context @user login, dispose @ logout. While using/sharing the same DB across organizations, and filter (PO requests or employee entities) table records in users session scoped to only show the records that match his Organization Id (show records in PO table where user.tenantId => POtable.TentantIdCol == user.tenantId ).get to list… it could be anything.. but that gives you an idea on what would be helpful. If you search you will notice many questions around this, hoping to see an answer with post from your site on this.

      If you can also address, how are claims different from adding a property to the ASP Users table? I am guessing more of smaller shops like me, that are invested in ASP MVC 5 would be able to connect with you.

      Liked by 1 person

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s