預定義的過濾
ISoftDelete
軟刪除過濾用來在查詢數據庫時,自動過濾(從結果中抽取)已刪除的實體。如果一個實體可以被軟刪除,它必須實現ISoftDelete接口,該接口只定義了一個IsDeleted屬性,例如:
public class Person : Entity, ISoftDelete
{
public virtual string Name { get; set; }
public virtual bool IsDeleted { get; set; }
}
不會真實刪除數據
不會從數據庫里真實刪除一個Person實體,當需要刪除它時,只是把它的IsDeleted屬性設置為true(DbContext.SaveChanges時自動執行)。
namespace Mt.EntityFramework
{
/// <summary>
/// Base class for all DbContext classes in the application.
/// </summary>
public abstract class AbpDbContext : DbContext, ITransientDependency, IShouldInitialize
{
protected virtual void CancelDeletionForSoftDelete(DbEntityEntry entry)
{
if (!(entry.Entity is ISoftDelete))
{
return;
}
var softDeleteEntry = entry.Cast<ISoftDelete>();
softDeleteEntry.Reload();
softDeleteEntry.State = EntityState.Modified;
softDeleteEntry.Entity.IsDeleted = true;
}
}
}
查找數據時軟刪除數據不會被獲取
實現ISoftDelete之后,當你從數據庫獲取Person列表,軟刪除的人員不會被獲取,此處有一個示例類,使用一個person倉儲獲取所有人員:
public class MyService
{
private readonly IRepository<Person> _personRepository;
public MyService(IRepository<Person> personRepository)
{
_personRepository = personRepository;
}
public List<Person> GetPeople()
{
return _personRepository.GetAllList();
}
}
GetPeople方法僅獲取全部IsDeleted=false(不是delete)的Person。所有的倉儲方法和導航屬性都工作正常。我們可以添加一些其實where條件、連接等,它會自動添加IsDeleted=false條件到生成的Sql查詢。
namespace Mt.EntityFramework
{
/// <summary>
/// Base class for all DbContext classes in the application.
/// </summary>
public abstract class AbpDbContext : DbContext, ITransientDependency, IShouldInitialize
{
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Filter(AbpDataFilters.SoftDelete, (ISoftDelete d) => d.IsDeleted, false);
//While "(int?)t.TenantId == null" seems wrong, it's needed. See https://github.com/jcachat/EntityFramework.DynamicFilters/issues/62#issuecomment-208198058
modelBuilder.Filter(AbpDataFilters.MustHaveTenant, (IMustHaveTenant t, int tenantId) => t.TenantId == tenantId || (int?)t.TenantId == null, 0);
modelBuilder.Filter(AbpDataFilters.MayHaveTenant, (IMayHaveTenant t, int? tenantId) => t.TenantId == tenantId, 0);
}
}
}
ISoftDelete過濾一直可用,除非你顯式禁用它。
Autdit
public interface IHasDeletionTime : ISoftDelete
{
/// <summary>
/// Deletion time of this entity.
/// </summary>
DateTime? DeletionTime { get; set; }
}
public interface IDeletionAudited : IHasDeletionTime
{
/// <summary>
/// Which user deleted this entity?
/// </summary>
long? DeleterUserId { get; set; }
}
所以FullAuditedEntity是軟刪除, 參閱 ABP框架 - 實體
IMustHaveTenant
如果你正在創建多租戶應用並存儲所有租戶數據在一個數據庫里,你明確地不想一個租戶的數據意外地被另一個租戶看到,這種情況下你可用IMustHaveTenant。例如:
public class Product : Entity, IMustHaveTenant
{
public int TenantId { get; set; }
public string Name { get; set; }
}
IMustHaveTenant定義了TenantId,區別不同的租戶實體。ABP默認情況下使用IAbpSeesion獲取當前TenantId,並自動為當前租戶過濾查詢。
IMustHaveTenant默認可用。
如果當前用戶尚未登錄系統或當前是個宿主用戶(宿主用戶是一個更高級別的用戶,它管理租戶和租戶數據),ABP自動禁用IMustHaveTenant過濾,因此,可以獲取所有租戶的所有數據。注意:這與安全性無關,你應當一直授權敏感數據。
IMayHaveTenant
如果一個實體類被租戶和宿主共享(也就是說一個實體對象可被租戶或宿主擁有),你可以使用IMayHaveTenant過濾。IMayHaveTenant接口定義了TenantId,但它是可空的。
public class Role : Entity, IMayHaveTenant
{
public int? TenantId { get; set; }
public string RoleName { get; set; }
}
一個null值表示這是個宿主實體,一個非null值表示這個實體被Id為TenantId的租戶擁有。默認情況下,ABP使用IAbpSeesion獲取當前TenantId。IMayHaveTenant過濾不像IMustHaveTenant那么通用,但在實體類型通用宿主和租戶時,需要它。
IMayHaveTenant一直可用,除非你顯式禁用它。
禁用過濾
調用DisableFilter方法可以禁用每個工作單元的一個過濾,如下:
var people1 = _personRepository.GetAllList();
using (_unitOfWorkManager.Current.DisableFilter(AbpDataFilters.SoftDelete))
{
var people2 = _personRepository.GetAllList();
}
var people3 = _personRepository.GetAllList();
DisableFilter接受一個或多個過濾名稱組成的字符串,AbpDataFilters.SoftDelete是個字符串常量,它代表ABP的軟刪除過濾。
people2將包含被軟刪除的people,people1和people3只包含未被軟刪除的people。使用using聲明,你可以在using域內禁用一個過濾。如果不使用using聲明,過濾會被禁用,直到當前工作單元結束或是你顯示啟用這個過濾。
你可以注入IUnitOfWorkManager,然后像上例那樣使用,如果你的類繼承自特殊的基類(如應用服務,AbpController,AbpApiController...),你也可以直接使用CurrentUnitOfWork屬性。
關於using聲明
如果一個過濾是啟用的,當你使用using聲明,調用DisableFilter方法,這個過濾會被禁用,然后在using聲明之后,自動地被啟用。但是如果這個過濾是在使用using聲明前,就是禁用的,那么DisableFilter什么也不做,在using聲明之后,它仍然是禁用的。
關於多租戶
你可以禁用多租戶過濾來查詢所有租戶的數據,但這只對一個數據庫有效。如果你為每個租戶使用分離的數據庫,禁用過濾就無法幫助你獲取所有租戶的數據,因為數據在不同的數據庫甚至是不同的服務器,更多信息查看多租戶文檔。
啟用過濾
在一個工作單元里,你可以使用EnableFilter方法啟用一個過濾。相似於(也相反於)DisableFilter。EnableFilter也在使用using聲明時,返回可釋放對象,用來在有需要的情況下重新禁用過濾。
設置過濾參數
一個過濾可以參數化,IMusthaveTenant過濾就是一個例子,因為當前租戶的Id在運行時才能檢測到。對於此類過濾,如果有需要,我們可以修改過濾值,如:
CurrentUnitOfWork.SetFilterParameter(AbpDataFilters.MusthaveTenant, AbpDataFilters.Parameters.TenantId, 42);
另一個例子:為IMayHaveTenant過濾,設置租戶Id:
CurrentUnitOfWork.SetFilterParameter(AbpDataFilters.MayHaveTenant, AbpDataFilters.Parameters.TenantId, 42);
SetFilterParameter方法也返回一個IDisposeble,所以使用一個using聲明,讓它自動在聲明之后恢復原值。
public class EfDynamicFiltersUnitOfWorkFilterExecuter : IEfUnitOfWorkFilterExecuter
{
public void ApplyDisableFilter(IUnitOfWork unitOfWork, string filterName)
{
foreach (var activeDbContext in unitOfWork.As<EfUnitOfWork>().GetAllActiveDbContexts())
{
activeDbContext.DisableFilter(filterName);
}
}
public void ApplyEnableFilter(IUnitOfWork unitOfWork, string filterName)
{
foreach (var activeDbContext in unitOfWork.As<EfUnitOfWork>().GetAllActiveDbContexts())
{
activeDbContext.EnableFilter(filterName);
}
}
public void ApplyFilterParameterValue(IUnitOfWork unitOfWork, string filterName, string parameterName, object value)
{
foreach (var activeDbContext in unitOfWork.As<EfUnitOfWork>().GetAllActiveDbContexts())
{
if (TypeHelper.IsFunc<object>(value))
{
activeDbContext.SetFilterScopedParameterValue(filterName, parameterName, (Func<object>)value);
}
else
{
activeDbContext.SetFilterScopedParameterValue(filterName, parameterName, value);
}
}
}
public void ApplyCurrentFilters(IUnitOfWork unitOfWork, DbContext dbContext)
{
foreach (var filter in unitOfWork.Filters)
{
if (filter.IsEnabled)
{
dbContext.EnableFilter(filter.FilterName);
}
else
{
dbContext.DisableFilter(filter.FilterName);
}
foreach (var filterParameter in filter.FilterParameters)
{
if (TypeHelper.IsFunc<object>(filterParameter.Value))
{
dbContext.SetFilterScopedParameterValue(filter.FilterName, filterParameter.Key, (Func<object>)filterParameter.Value);
}
else
{
dbContext.SetFilterScopedParameterValue(filter.FilterName, filterParameter.Key, filterParameter.Value);
}
}
}
}
}
SetTenantId 方法
雖然你可以使用SetFilterParameter方法,為MayHaveTenant和MusthaveTenant修改過濾值,但修改租戶過濾有一個更好的方式:SetTenantId()。SetTenantId為這兩個過濾修改參數值,並且單數據庫或每個租戶一個數據庫都有效。所以,總是推薦用SetTenantId修改租戶過濾的參數值。查看多租戶文檔獲取更多信息。
自定義過濾
定義一個接口
為自定義過濾並整合到ABP,首先,定義一個接口,它將被使用這個過濾的實體實現。假設我們要通過PersonId自動過濾實體,接口示例:
public interface IHasPerson
{
int PersonId { get; set; }
}
實現這個接口
public class Phone : Entity, IHasPerson
{
[ForeignKey("PersonId")]
public virtual Person Person { get; set; }
public virtual int PersonId { get; set; }
public virtual string Number { get; set; }
}
定義過濾
因為ABP使用EntityFramework.DynamicFilters,我們使用它的規則來定義這個過濾,在我們的DbContext類里,我們重寫OnModelCreating,如下所示:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Filter("PersonFilter", (IHasPerson entity, int personId) => entity.PersonId == personId, 0);
}
“PersonFilter”是這個過濾的唯一的名稱,第二個參數表明過濾的接口和過濾參數PersonId(如果過濾不可參數化,可不用),最后一個參數是personId的默認值。
注冊這個過濾
最后在我們模塊的PreInitialize方法里,注冊這個過濾到ABP工作單元系統:
Configuration.UnitOfWork.RegisterFilter("PersonFilter", false);
第一個參數就是我們之前定義的名稱,第二個參數指明默認情況下是否啟用。
使用
using (CurrentUnitOfWork.EnableFilter("PersonFilter"))
{
using(CurrentUnitOfWork.SetFilterParameter("PersonFilter", "personId", 42))
{
var phones = _phoneRepository.GetAllList();
//...
}
}
我們應當從其它地方獲取personId代替硬編碼。上面是個可參數化過濾的例子,一個過濾可能有0或多個參數,如果沒有參數,就沒有必要設置過濾的參數值,同樣,如果默認過濾是啟用的,就不必再手動啟用它(當然,我們可以禁用它)。
EntityFramework.DynamicFilters 文檔
獲取更多有關動態數據過濾信息,請參閱github頁上的文檔: https://github.com/jcachat/EntityFramework.DynamicFilters
我們可以為security, active/passive等實體自定義過濾。
其它 ORM
ABP數據過濾是為EntityFramework和NHibernate實現的,其它ORM上不可以用(包含EntityFramework Core)。但實質上,你可以在大多數情況上模仿它,只要你也是用倉儲來獲取數據的,你可以自定義一個倉儲,然后重寫GetAll和其它所需的數據獲取方法。