Search code examples
c#asp.net-corefilterentity-framework-coreaspnetboilerplate

How to implement Entity Framework Core data filters?


In my ASP.NET Zero application, I want to setup data filters by CompanyId value. So I read the documents here and the information provided on this support forum thread.

In my EF Core project, I made the following change.

public override void PreInitialize()
{
    if (!SkipDbContextRegistration)
    {
        // ...

        Configuration.UnitOfWork.RegisterFilter("CompanyFilter", false);
    }
}

I then followed the pattern used for IMustHaveTenant data filter and applied the changes below into my DB context class:

protected int? CurrentCompanyId = null;
protected bool IsCompanyFilterEnabled => CurrentCompanyId != null && CurrentUnitOfWorkProvider?.Current?.IsFilterEnabled("CompanyFilter") == true;    

protected override bool ShouldFilterEntity<TEntity>(IMutableEntityType entityType)
{
    if (typeof(IHasCompany).IsAssignableFrom(typeof(TEntity)))
    {
        return true;
    }
    return false;
}

protected override Expression<Func<TEntity, bool>> CreateFilterExpression<TEntity>()
{
    Expression<Func<TEntity, bool>> expression = null;

    if (typeof(IHasCompany).IsAssignableFrom(typeof(TEntity)))
    {
        Expression<Func<TEntity, bool>> companyFilter = e => ((IHasCompany)e).CompanyId == CurrentCompanyId || (((IHasCompany)e).CompanyId == CurrentCompanyId) == IsCompanyFilterEnabled;
        expression = expression == null ? companyFilter : CombineExpressions(expression, companyFilter);
    }
    return base.CreateFilterExpression<TEntity>();
}

Then, in my app service method where I wish to apply the filter, I added the code below.

public async Task<PagedResultDto<EmployeeListDto>> GetEmployees(GetEmployeeInput input)
{
    using (CurrentUnitOfWork.EnableFilter("CompanyFilter"))
    {
        using (CurrentUnitOfWork.SetFilterParameter("CompanyFilter", "CompanyId", GetCurrentUserCompany()))
        {
            // ...
        }
    }
}

In the DB context class, if I hard code a CompanyId value on the line below, the filter works just fine.

protected int? CurrentCompanyId = 123;

I will be honest, I do not fully understand the code that is being used in the CreateFilterExpression method in the DB context class. I borrowed this straight from the ABP GitHub repo code for IMustHaveTenant filter.

I have modified the ABP user "My Settings" modal to include CompanyId. This permits each user to be assigned to a company and thus the application should restrict all data by company for this user.

In my app service base class, I have a method named GetCurrentUserCompany. This method gets the current user's default company. This is the value that should be used on the data filter for company.

Questions:

  • Do I need to set this company value in the DB context class?
  • If yes, then how do I call an app service or repository method to get the CompanyId in this DB context class?

Update: I added the code suggested by Aaron and its still not working. I'm testing with a user Id who has company 1 assigned. Yet when the data loads, it still showing them company 1 and 2.

App service showing filter (line 71) has been set.

Debug cmd window showing company filter is set.

When line 77 executes and does the GetAll call on the repository, its still showing all companies to the user.

Oct 6 Update: The new code works, but only when the user has a company Id assigned. When the user does not have a company assigned, the error shown in the image below is thrown. DB context error


Solution

    • Do I need to set this company value in the Db context class?
    • If yes, then how do I call an app service or repository method to get the company Id in this DB context class?

    No. Implement a getter in your DbContext class:

    // using Abp.Collections.Extensions;
    
    // protected int? CurrentCompanyId = null;
    protected int? CurrentCompanyId => GetCurrentCompanyIdOrNull();
    
    protected virtual int? GetCurrentCompanyIdOrNull()
    {
        if (CurrentUnitOfWorkProvider != null &&
            CurrentUnitOfWorkProvider.Current != null)
        {
            return CurrentUnitOfWorkProvider.Current
                .Filters.FirstOrDefault(f => f.FilterName == "CompanyFilter")?
                .FilterParameters.GetOrDefault("CompanyId") as int?;
        }
    
        return null;
    }
    

    Return expression in your CreateFilterExpression method:

    protected override Expression<Func<TEntity, bool>> CreateFilterExpression<TEntity>()
    {
        // Expression<Func<TEntity, bool>> expression = null;
        var expression = base.CreateFilterExpression<TEntity>();
    
        if (typeof(IHasCompany).IsAssignableFrom(typeof(TEntity)))
        {
            Expression<Func<TEntity, bool>> companyFilter = e => ((IHasCompany)e).CompanyId == CurrentCompanyId || (((IHasCompany)e).CompanyId == CurrentCompanyId) == IsCompanyFilterEnabled;
            expression = expression == null ? companyFilter : CombineExpressions(expression, companyFilter);
        }
    
        // return base.CreateFilterExpression<TEntity>();
        return expression;
    }
    

    A similar question: Implement OrganizationUnit filter in ASP.NET Core