Data Filtering
Volo.Abp.Data package defines services to automatically filter data on querying from a database.
Pre-Defined Filters
ABP defines some filters out of the box.
ISoftDelete
Used to mark an entity as deleted instead of actually deleting it. Implement the ISoftDelete
interface to make your entity "soft delete".
Example:
using System;
using Volo.Abp;
using Volo.Abp.Domain.Entities;
namespace Acme.BookStore
{
public class Book : AggregateRoot<Guid>, ISoftDelete
{
public string Name { get; set; }
public bool IsDeleted { get; set; } //Defined by ISoftDelete
}
}
ISoftDelete
defines the IsDeleted
property. When you delete a book using repositories, ABP automatically sets IsDeleted
to true and protects it from actual deletion (you can also manually set the IsDeleted
property to true if you need). In addition, it automatically filters deleted entities when you query the database.
ISoftDelete
filter is enabled by default and you can not get deleted entities from database unless you explicitly disable it. See theIDataFilter
service below.
Soft-delete entities can be hard-deleted when you use
HardDeleteAsync
method on the repositories.
IMultiTenant
Multi-tenancy is an efficient way of creating SaaS applications. Once you create a multi-tenant application, you typically want to isolate data between tenants. Implement IMultiTenant
interface to make your entity "multi-tenant aware".
Example:
using System;
using Volo.Abp;
using Volo.Abp.Domain.Entities;
using Volo.Abp.MultiTenancy;
namespace Acme.BookStore
{
public class Book : AggregateRoot<Guid>, ISoftDelete, IMultiTenant
{
public string Name { get; set; }
public bool IsDeleted { get; set; } //Defined by ISoftDelete
public Guid? TenantId { get; set; } //Defined by IMultiTenant
}
}
IMultiTenant
interface defines the TenantId
property which is then used to automatically filter the entities for the current tenant. See the Multi-tenancy document for more.
IDataFilter Service: Enable/Disable Data Filters
You can control the filters using IDataFilter
service.
Example:
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Volo.Abp;
using Volo.Abp.Data;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Repositories;
namespace Acme.BookStore
{
public class MyBookService : ITransientDependency
{
private readonly IDataFilter _dataFilter;
private readonly IRepository<Book, Guid> _bookRepository;
public MyBookService(
IDataFilter dataFilter,
IRepository<Book, Guid> bookRepository)
{
_dataFilter = dataFilter;
_bookRepository = bookRepository;
}
public async Task<List<Book>> GetAllBooksIncludingDeletedAsync()
{
//Temporary disable the ISoftDelete filter
using (_dataFilter.Disable<ISoftDelete>())
{
return await _bookRepository.GetListAsync();
}
}
}
}
- Inject the
IDataFilter
service to your class. - Use the
Disable
method within ausing
statement to create a code block where theISoftDelete
filter is disabled inside it.
In addition to the Disable<T>()
method;
IDataFilter.Enable<T>()
method can be used to enable a filter.Enable
andDisable
methods can be used in a nested way to define inner scopes.IDataFilter.IsEnabled<T>()
can be used to check whether a filter is currently enabled or not.
Always use the
Disable
andEnable
methods it inside ausing
block to guarantee that the filter is reset to its previous state.
The Generic IDataFilter Service
IDataFilter
service has a generic version, IDataFilter<TFilter>
that injects a more restricted and explicit data filter based on the filter type.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Volo.Abp;
using Volo.Abp.Data;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Repositories;
namespace Acme.BookStore
{
public class MyBookService : ITransientDependency
{
private readonly IDataFilter<ISoftDelete> _softDeleteFilter;
private readonly IRepository<Book, Guid> _bookRepository;
public MyBookService(
IDataFilter<ISoftDelete> softDeleteFilter,
IRepository<Book, Guid> bookRepository)
{
_softDeleteFilter = softDeleteFilter;
_bookRepository = bookRepository;
}
public async Task<List<Book>> GetAllBooksIncludingDeletedAsync()
{
//Temporary disable the ISoftDelete filter
using (_softDeleteFilter.Disable())
{
return await _bookRepository.GetListAsync();
}
}
}
}
- This usage determines the filter type while injecting the
IDataFilter<T>
service. - In this case you can use the
Disable()
andEnable()
methods without specifying the filter type.
AbpDataFilterOptions
AbpDataFilterOptions
can be used to set options for the data filter system.
The example code below disables the ISoftDelete
filter by default which will cause to include deleted entities when you query the database unless you explicitly enable the filter:
Configure<AbpDataFilterOptions>(options =>
{
options.DefaultStates[typeof(ISoftDelete)] = new DataFilterState(isEnabled: false);
});
Carefully change defaults for global filters, especially if you are using a pre-built module which might be developed assuming the soft delete filter is turned on by default. But you can do it for your own defined filters safely.
Defining Custom Filters
Defining and implementing a new filter highly depends on the database provider. ABP implements all pre-defined filters for all database providers.
When you need it, start by defining an interface (like ISoftDelete
and IMultiTenant
) for your filter and implement it for your entities.
Example:
public interface IIsActive
{
bool IsActive { get; }
}
Such an IIsActive
interface can be used to filter active/passive data and can be easily implemented by any entity:
public class Book : AggregateRoot<Guid>, IIsActive
{
public string Name { get; set; }
public bool IsActive { get; set; } //Defined by IIsActive
}
EntityFramework Core
ABP uses EF Core's Global Query Filters system for the EF Core Integration. So, it is well integrated to EF Core and works as expected even if you directly work with DbContext
.
Best way to implement a custom filter is to override ShouldFilterEntity
and CreateFilterExpression
method for your DbContext
. Example:
protected bool IsActiveFilterEnabled => DataFilter?.IsEnabled<IIsActive>() ?? false;
protected override bool ShouldFilterEntity<TEntity>(IMutableEntityType entityType)
{
if (typeof(IIsActive).IsAssignableFrom(typeof(TEntity)))
{
return true;
}
return base.ShouldFilterEntity<TEntity>(entityType);
}
protected override Expression<Func<TEntity, bool>> CreateFilterExpression<TEntity>(ModelBuilder modelBuilder)
{
var expression = base.CreateFilterExpression<TEntity>(modelBuilder);
if (typeof(IIsActive).IsAssignableFrom(typeof(TEntity)))
{
Expression<Func<TEntity, bool>> isActiveFilter = e => !IsActiveFilterEnabled || EF.Property<bool>(e, "IsActive");
expression = expression == null ? isActiveFilter : QueryFilterExpressionHelper.CombineExpressions(expression, isActiveFilter);
}
return expression;
}
- Added a
IsActiveFilterEnabled
property to check ifIIsActive
is enabled or not. It internally uses theIDataFilter
service introduced before. - Overrided the
ShouldFilterEntity
andCreateFilterExpression
methods, checked if given entity implements theIIsActive
interface and combines the expressions if necessary.
In addition you can also use HasAbpQueryFilter
to set a filter for an entity. It will combine your filter with ABP EF Core builtin global query filters.
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<MyEntity>(b =>
{
b.HasAbpQueryFilter(e => e.Name.StartsWith("abp"));
});
}
Using User-defined function mapping for global filters
Using User-defined function mapping for global filters will gain performance improvements.
To use this feature, you need to change your DbContext like below:
protected bool IsActiveFilterEnabled => DataFilter?.IsEnabled<IIsActive>() ?? false;
protected override bool ShouldFilterEntity<TEntity>(IMutableEntityType entityType)
{
if (typeof(IIsActive).IsAssignableFrom(typeof(TEntity)))
{
return true;
}
return base.ShouldFilterEntity<TEntity>(entityType);
}
protected override Expression<Func<TEntity, bool>> CreateFilterExpression<TEntity>(ModelBuilder modelBuilder)
{
var expression = base.CreateFilterExpression<TEntity>(modelBuilder);
if (typeof(IIsActive).IsAssignableFrom(typeof(TEntity)))
{
Expression<Func<TEntity, bool>> isActiveFilter = e => !IsActiveFilterEnabled || EF.Property<bool>(e, "IsActive");
if (UseDbFunction())
{
isActiveFilter = e => IsActiveFilter(((IIsActive)e).IsActive, true);
var abpEfCoreCurrentDbContext = this.GetService<AbpEfCoreCurrentDbContext>();
modelBuilder.HasDbFunction(typeof(MyProjectNameDbContext).GetMethod(nameof(IsActiveFilter))!)
.HasTranslation(args =>
{
// (bool isActive, bool boolParam)
var isActive = args[0];
var boolParam = args[1];
if (abpEfCoreCurrentDbContext.Context?.DataFilter.IsEnabled<IIsActive>() == true)
{
// isActive == true
return new SqlBinaryExpression(
ExpressionType.Equal,
isActive,
new SqlConstantExpression(Expression.Constant(true), boolParam.TypeMapping),
boolParam.Type,
boolParam.TypeMapping);
}
// empty where sql
return new SqlConstantExpression(Expression.Constant(true), boolParam.TypeMapping);
});
}
expression = expression == null ? isActiveFilter : QueryFilterExpressionHelper.CombineExpressions(expression, isActiveFilter);
}
return expression;
}
public static bool IsActiveFilter(bool isActive, bool boolParam)
{
throw new NotSupportedException(AbpEfCoreDataFilterDbFunctionMethods.NotSupportedExceptionMessage);
}
public override string GetCompiledQueryCacheKey()
{
return $"{base.GetCompiledQueryCacheKey()}:{IsActiveFilterEnabled}";
}
MongoDB
ABP abstracts the IMongoDbRepositoryFilterer
interface to implement data filtering for the MongoDB Integration, it works only if you use the repositories properly. Otherwise, you should manually filter the data.
Currently, the best way to implement a data filter for the MongoDB integration is to create a derived class of MongoDbRepositoryFilterer
and override FilterQueryable
. Example:
[ExposeServices(typeof(IMongoDbRepositoryFilterer<Book, Guid>))]
public class BookMongoDbRepositoryFilterer : MongoDbRepositoryFilterer<Book, Guid> , ITransientDependency
{
public BookMongoDbRepositoryFilterer(
IDataFilter dataFilter,
ICurrentTenant currentTenant) :
base(dataFilter, currentTenant)
{
}
public override TQueryable FilterQueryable<TQueryable>(TQueryable query)
{
if (DataFilter.IsEnabled<IIsActive>())
{
return (TQueryable)query.Where(x => x.IsActive);
}
return base.FilterQueryable(query);
}
}
This example implements it only for the Book
entity. If you want to implement for all entities (those implement the IIsActive
interface), create your own custom MongoDB repository filterer base class and override the AddGlobalFilters
as shown below:
public abstract class MyMongoRepository<TMongoDbContext, TEntity, TKey> : MongoDbRepository<TMongoDbContext, TEntity, TKey>
where TMongoDbContext : IAbpMongoDbContext
where TEntity : class, IEntity<TKey>
{
protected MyMongoRepository(IMongoDbContextProvider<TMongoDbContext> dbContextProvider)
: base(dbContextProvider)
{
}
protected override void AddGlobalFilters(List<FilterDefinition<TEntity>> filters)
{
base.AddGlobalFilters(filters);
if (typeof(IIsActive).IsAssignableFrom(typeof(TEntity))
&& DataFilter.IsEnabled<IIsActive>())
{
filters.Add(Builders<TEntity>.Filter.Eq(e => ((IIsActive)e).IsActive, true));
}
}
}
public class MyMongoDbModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
//.......
context.Services
.Replace(ServiceDescriptor.Transient(typeof(IMongoDbRepositoryFilterer<,>),typeof(MyMongoDbRepositoryFilterer<,>)));
}
}