體驗 ABP 的功能和服務

大家好,我是張飛洪,感謝您的閱讀,我會不定期和你分享學習心得,希望我的文章能成爲你成長路上的墊腳石,讓我們一起精進。
ABP是一個全棧開發框架,它在企業解決方案的各個方面都有許多構建模塊。在前面三章中,我們探討了ABP框架提供的基本服務、數據訪問基礎設施和橫切關注點問題。

在第2部分的最後一章中,我們將繼續介紹經常使用的一些ABP功能:

  • 獲取當前用戶
  • 使用數據過濾
  • 控制審計日誌
  • 緩存數據
  • 本地化用戶界面(UI)

一、獲取當前用戶

如果應用需要對某些功能進行身份驗證,通常需要獲取有關當前用戶的信息。ABP提供ICurrentUser服務,以獲取當前登錄用戶的詳細信息。對於Web應用程序,ICurrentUser的實現與ASP.NET Core完全集成,因此您可以輕鬆獲取當前用戶的Claims(聲明)。

有關ICurrentUser服務的簡單用法,請參閱以下代碼塊:

using System; 
using Volo.Abp.DependencyInjection; 
using Volo.Abp.Users; 
namespace DemoApp 
{     
    public class MyService : ITransientDependency 
    {         
        private readonly ICurrentUser _currentUser;         
        public MyService(ICurrentUser currentUser)         
        { 
            _currentUser = currentUser;
        }         
        public void Demo() 
        { 
            Guid? userId = _currentUser.Id;             
            string userName = _currentUser.UserName;             
            string email = _currentUser.Email;         
        }     
    }
}

在本例中,MyService構造函數注入ICurrentUser服務,然後獲取當前用戶的唯一IdUsernameEmail

ICurrentUser接口屬性

以下是ICurrentUser接口的相關屬性:

  • IsAuthenticated (bool):如果當前用戶已登錄(已驗證),則返回 true
  • Id (Guid?):當前用戶唯一標識符(UID)。如果尚未登錄,則返回 null
  • UserName (string):當前用戶的用戶名。如果尚未登錄,則返回 null
  • TenantId (Guid?):當前用戶的租戶ID。它可用於多租戶應用程序。如果當前用戶與租戶無關,則返回null
  • Email (string):當前用戶的電子郵件地址。如果未登錄或未設置電子郵件地址,則返回null
  • EmailVerified (bool):如果當前用戶的郵件已被驗證,則返回true
  • PhoneNumber (string):當前用戶的電話號碼。如果當前用戶未登錄或未設置電話號碼,則返回null
  • PhoneNumberVerified (bool):如果當前用戶的電話號碼已被驗證,則返回true
    Roles (string[]):當前用戶的所有角色(字符串數組)。

注入ICurrentUser服務

ICurrentUser是一種廣泛使用的服務。因此,一些基本ABP類(如ApplicationService和AbpController)提供了預注入。在這些類中,您可以直接使用CurrentUser屬性,而不是手動注入此服務。

ICurrentUser服務注入

  • ICurrentUser是一種廣泛使用的服務。因此,一些ABP基類(如ApplicationServiceAbpController)提供了預注入。在這些類中,可以直接使用 CurrentUser屬性,而無需手動注入此服務。

ABP可以與任何身份驗證提供商合作,可以與ASP.NET Core提供的當前聲明合作(聲明[Claims]是用戶登錄時存儲在身份驗證票據中的鍵值對)。如果您使用的是基於cookie的身份驗證,它們將存儲在cookie中,並在每個請求中發送到服務器。如果您使用的是基於令牌的身份驗證,則客戶端會在每個請求中發送它們,通常在HTTP頭中。

ICurrentUser服務從當前聲明中獲取所有信息。如果想要直接查詢當前聲明,可以使用FindClaimFindClaimsGetAllClaims方法。如果您創建了自定義聲明,這些方法尤其有用:

定義聲明

ABP提供了一種將自定義聲明添加到身份驗證票據的簡單方法,以便在同一用戶的下一個請求中安全地獲取這些自定義值。您可以實現IAbpClaimsPrincipalContributor接口,將自定義聲明添加到身份驗證票據中。

在下面的示例中,我們將添加社會安全號碼信息,這是身份驗證票據的自定義聲明:

public class SocialSecurityNumberClaimsPrincipalContributor : IAbpClaimsPrincipalContributor, ITransientDependency 
{     
    public async Task ContributeAsync(AbpClaimsPrincipalContributorContext context) 
    {  
        ClaimsIdentity identity = context.ClaimsPrincipal.Identities.FirstOrDefault();        
        var userId = identity?.FindUserId();         
        if (userId.HasValue) 
        {             
            var userService = context.ServiceProvider.GetRequiredService();                         
            var socialSecurityNumber = await userService.GetSocialSecurityNumberAsync(userId.Value);             
            if (socialSecurityNumber != null)  
            {                 
                identity.AddClaim(new Claim("SocialSecurityNumber",socialSecurityNumber));             
            }         
        }     
    } 
}

在本例中,我們首先獲取ClaimsIdentity並查找當前用戶的ID。然後,我們從IUserService獲取社會保險號,這是自行開發的服務。您可以從ServiceProvider解析任何服務來查詢所需的數據。最後,我們爲identity添加了一個新的Claim(聲明)。 每當用戶登錄時,都會執行SocialSecurityNumberClaimsPrincipalContributor

您可以爲當前用戶使用自定義聲明,用於授權特定的業務需求、過濾數據或僅在UI上做顯示。

[success] 請注意,除非使身份驗證票據失效並重新登錄,否則無法更改身份驗證票據聲明,因此不要在聲明中存儲頻繁更改的數據。如果你想將用戶數據存儲在以後可以快速訪問的位置,則可以使用緩存系統。

ICurrentUser是系統經常使用的核心服務。下一節將介紹大部分時間都能無縫工作的數據過濾系統。

二、使用數據過濾

過濾查詢中的數據在數據庫操作中非常常見。如果您使用的是結構化查詢語言(SQL),那麼可以使用WHERE子句。如果您使用的是語言集成查詢(LINQ),則使用C#中的Where擴展方法。雖然大多數過濾條件在查詢中有所不同,但如果實現的是軟刪除和多租戶,則某些表達式在所有查詢中是一致的。

ABP自動化了數據過濾過程,幫助您避免在應用代碼中到處重複相同的過濾邏輯。

在本節中,我們將首先看到ABP框架的預構建數據過濾器,然後學習如何在需要時禁用過濾器。最後,我們將看到如何實現自定義數據過濾器

我們通常使用簡單的接口來實現對實體的過濾。ABP定義了兩個預定義的數據過濾器,以實現軟刪除和多租戶。

1 預構建數據過濾器

1.1 軟刪除過濾器

如果對一個實體使用軟刪除,則不會從物理上刪除數據庫中的實體。而是將其標記爲已刪除。

ABP定義了ISoftDelete接口,以將實體標記爲軟刪除。可以爲實體實現該接口,如下代碼塊所示

public class Order : AggregateRoot, ISoftDelete 
{     
    public bool IsDeleted { get; set; }     
    //...other properties 
}

在本例中,Order實體具有由ISoftDelete接口定義的IsDeleted屬性。一旦實現了該接口,ABP將爲您自動執行以下任務:

  • 刪除訂單時,ABP會標識Order實體實現軟刪除,並將IsDeleted設置爲 true,訂單不會在數據庫中被物理刪除。
  • 查詢訂單時,ABP會自動過濾掉被刪除的實體(通過向查詢中添加IsDeleted == false條件)。

數據過濾限制

數據過濾自動化僅在使用存儲庫或DbContextEF Core)時有效。如果您使用的是手寫的SQL DELETESELECT命令,您應該自己處理,因爲在這種情況下,ABP無法攔截您的操作。

1.2 多租戶過濾器

在SaaS解決方案中,多租戶是租戶之間共享資源的一種廣泛使用的模式。在多租戶應用程序中,隔離不同租戶之間的數據至關重要。一個租戶無法讀取或寫入另一個租戶的數據,即使它們位於同一個物理數據庫中。

ABP有一個完整的多租戶系統,將在第16章“實現多租戶”中詳細介紹。這裏僅僅提到的是它的過濾器,因爲它與數據過濾系統有關。
ABP定義了IMultiTenant接口,用於爲實體啓用多租戶數據過濾器。我們可以爲實體實現該接口,如以下代碼塊所示:

public class Order : AggregateRoot, IMultiTenant {
    public Guid? TenantId { get; set; }     
    //...other properties 
}

IMultiTenant接口定義了TenantId屬性,如本例所示,使用的是Guid類型。

一旦我們實現了IMultiTenant接口,ABP就會使用當前租戶的ID自動過濾訂單實體的所有查詢。當前租戶的ID是從ICurrentTenant服務獲得的。

使用多個數據過濾器

可以爲同一實體啓用多個數據篩選器。例如,本節中定義的Order實體可以實現ISoftDeleteIMultiTenant接口。
如您所見,爲實體實現數據過濾器非常簡單,只需實現與數據過濾器相關的接口即可。默認情況下,所有數據過濾器均已啓用,除非您明確禁用它們。

2 禁用數據過濾器

在某些情況下,可能需要禁用自動篩選器。例如,您可能希望從數據庫中讀取已刪除的實體,您可能希望查詢所有租戶的數據。爲此,ABP提供了一種簡單而安全的方法來禁用數據過濾器。

以下示例顯示瞭如何通過使用IDataFilter服務禁用ISoftDelete數據過濾器,從數據庫獲取所有訂單,包括已刪除的訂單:

public class OrderService : ITransientDependency {     
    private readonly IRepository _orderRepository;     
    private readonly IdataFilter _dataFilter;     
    public OrderService(Irepository orderRepository,IdataFilter dataFilter) 
    {
        _orderRepository = orderRepository;         
        _dataFilter = dataFilter;     
    }     
    public async Task> GetAllOrders()
    {         
        using (_dataFilter.Disable())
        {             
            return await _orderRepository.GetListAsync();         
        }     
    } 
}

在本例中,OrderService注入Order存儲庫和IdataFilter服務。然後使用_dataFilter.Disable<IsoftDelete>()表達式以禁用軟刪除篩選器。在using語句中,過濾器被禁用,我們可以查詢已刪除的訂單。

始終使用using語句

Disable方法返回一個一次性對象,以便我們可以在using語句中使用它。一旦using塊結束,過濾器會自動返回到啓用狀態。該方式允許我們安全地禁用過濾器,而不會影響調用GetAllOrders方法的任何邏輯。建議在using語句中禁用篩選器。

IdataFilter服務還提供了兩種方法:

  • Enable<Tfilter>:啓用數據過濾器。如果過濾器已啓用,則該選項無效。建議在using語句中啓用篩選器,和禁用方法一樣。
  • IsEnabled<Tfilter>:如果給定的篩選器當前已啓用,則返回 true。通常不需要此方法,因爲在這兩種情況下都會按預期 EnableDisable

我們已經學習瞭如何禁用和啓用數據過濾器。下面繼續展示如何創建自定義數據過濾器。

3 定義自定義數據過濾器

就像預構建的數據過濾器一樣,您可能需要定義自己的過濾器。數據過濾器由接口表示,因此第一步是爲過濾器定義接口。

假設您希望歸檔實體,並自動過濾歸檔數據。爲此,我們可以定義這樣的接口(您可以在領域層中定義),如下所示:

public interface Iarchivable { bool IsArchived { get; } }

IsArchived屬性將用於過濾實體。默認情況下,IsArchivedtrue的實體將被刪除。一旦我們定義了這樣一個接口,我們就可以實現它。請參見以下示例:

public class Order : AggregateRoot, Iarchivable 
{     
    public bool IsArchived { get; set; }     
    //...other properties 
}

在本例中,Order實體實現了Iarchivable接口,這使得在該實體上應用數據過濾器成爲可能。

請注意,Iarchivable接口沒有爲Iarchivable定義setter屬性,但Order實體定義了setter屬性。這是因爲我們不需要在接口上設置Iarchivable,但需要在實體上設置它。

由於數據過濾是在數據庫提供程序級別完成的,因此自定義過濾器的實現也取決於數據庫提供程序。本節將展示如何爲EF Core實現IsArchived過濾器。如果您使用的是MongoDB,請參閱ABP的文檔

ABP使用EF Core的全局查詢過濾器系統對EF Core中的數據進行過濾。可以在DbContext中爲數據過濾器實現過濾邏輯。

**第1步:**在DbContext中定義屬性(將在過濾器表達式中使用),如下所示:

protected bool IsArchiveFilterEnabled => DataFilter?.IsEnabled() ?? false;

該屬性使用IdataFilter服務獲取過濾器狀態。DataFilter屬性來自基類AbpDbContext,如果沒有從依賴項注入(DI)系統解析DbContext出實例,則該屬性可以爲null。這就是爲什麼我使用null檢查。

**第2步:**重寫ShouldFilterEntity方法,以決定是否應過濾給定的實體類型:

protected override bool ShouldFilterEntity(ImutableEntityType entityType) 
{     
    If (typeof(IArchivable).IsAssignableFrom(typeof(TEntity)))
    { 
        return true;     
    }          
    return base.ShouldFilterEntity(entityType); 
}

ABP框架爲DbContext中的每個實體調用此方法(僅在應用啓動後,首次使用DbContext類時調用一次)。如果此方法返回true,則爲該實體啓用EF Core全局過濾器。在這裏,我只是檢查給定的實體是否實現了IArchivable接口,並在這種情況下返回true。否則,調用base方法,以便檢查其他數據過濾器。

**第3步:**ShouldFilterEntity僅決定是否啓用過濾,實際的過濾邏輯應該通過重寫CreateFilterExpression方法來實現:

protected override Expression<Func<TEntity, bool>> CreateFilterExpression<TEntity>() {     
    var expression = base.CreateFilterExpression<TEntity>();     
    if (typeof(Iarchivable).IsAssignableFrom(typeof(TEntity)))   
    {         
        Expression<Func<TEntity, bool>> archiveFilter = e => !IsArchiveFilterEnabled || !EF.Property<bool>(e, "IsArchived"); 
        expression = expression == null ? archiveFilter : CombineExpressions(expression,archiveFilter);     
    }     
    return expression; 
}

實現似乎有點複雜,因爲它創建並組合了表達式。最重要的是如何定義archiveFilter表達式。

  • !IsArchiveFilterEnabled檢查過濾器是否已禁用。如果過濾器被禁用,則不計算其他條件,並且檢索所有實體時不進行過濾!
  • !EF.Property<bool>(e, "IsArchived")檢查該實體的IsArchived值是否爲false,因此它會刪除IsArchivedtrue的實體。
    我沒有在過濾器實現中使用Order實體,這意味着實現是通用的,可以與任何實體類型一起工作。您只需要爲要應用過濾器的實體實現IArchivable接口。

總之,ABP允許我們輕鬆地創建和控制全局查詢過濾器,它還使用過濾系統實現了兩種流行的模式:軟刪除和多租戶。下一節將介紹非常常見的日誌審計系統。

三、控制審計日誌

ABP的審計日誌系統會跟蹤所有請求和實體更改,並將它們寫入數據庫。最後,你會得到一份報告,描述了我們的系統做了什麼,什麼時候做的,是誰做的。

從啓動模板創建新解決方案時,審計日誌已經預先安裝並配置完畢。大多數情況下,無需進行任何配置。但是,ABP允許我們控制、自定義和擴展審計日誌系統。

首先,讓我們瞭解一下什麼是審計日誌對象。

審計日誌對象(Audit log object)

審計日誌對象是在特定作用域內對方法操作實體變更的日誌記錄,比如執行HTTP請求記錄。下面是ABP審計實體關係圖:

讓我們從根對象開始解釋:

  • AuditLogInfo:在每個作用域(通常是web請求)中,都會創建一個AuditLogInfo對象,其中包含當前用戶、當前租戶、HTTP請求、客戶端和瀏覽器詳細信息,以及操作的執行時間和持續時間等信息。
  • AuditLogActionInfo:每個審計日誌中,操作通常是controller action調用、page handler調用或application service方法調用。包括該調用中的類名、方法名和方法參數。
  • EntityChangeInfo:審計日誌對象可能包含數據庫實體更改。包含實體更改類型(創建、更新或刪除)、實體類型和更改實體的ID。
  • EntityPropertyChangeInfo:對於每個實體更改,它都會在屬性(數據庫字段)上記錄更改。此對象包含屬性的名稱、類型、舊值和新值。
  • Exception:在此審計日誌範圍內發生的異常列表。
  • Comment:與此審計日誌相關的註釋/日誌。

審計日誌對象保存在關係數據庫的多個表中:AbpAuditLogsAbpAuditLogActionsAbpEntityChanges, 和AbpEntityPropertyChanges。您可以打開數據庫表,詳細瞭解審計日誌對象的基本屬性,或查看AuditLogInfo對象的詳細信息。

MongoDB限制

ABP使用EF Core的更改跟蹤系統來獲取實體更改信息,但是MongoDB不會記錄實體更改,因爲MongoDB驅動程序沒有此類更改跟蹤系統。

審計日誌作用域(Audit log scope

如本節開頭所述,在每個審計日誌作用域都會創建一個審計日誌對象。

審計日誌作用域使用環境上下文模式(Ambient Context Pattern)。創建審計日誌作用域時,在此域中執行的所有操作和更改都將保存爲單個審計日誌對象。

有幾種方法可以建立審計日誌作用域:

1 審計日誌中間件

第一種是最常見的方法,在ASP.NET Core管道中配置審計日誌中間件

app.UseAuditing();

這通常放在app.UseEndpoints()app.UseConfiguredEndpoints()配置之前。使用該中間件時,每個HTTP請求都會寫入一個單獨的審計日誌記錄(默認已在啓動模板中配置完畢)。

2 審計日誌攔截器

如果您不使用審計日誌中間件,或者您的應用程序不是請求/回覆式的ASP.NET Core應用(例如,是桌面或Blazor Server應用),ABP會根據每個應用服務方法創建一個新的審計日誌作用域。

3 手動創建審計作用域

非必要不這麼做,但如果要手動創建審計作用域,可以使用IAuditingManager服務,如以下代碼所示:

public class MyServiceWithAuditing : ITransientDependency {     
    //...inject IAuditingManager _auditingManager;     
    public async Task DoItAsync() 
    {         
        using (var auditingScope = _auditingManager.BeginScope())
        {             
            try{                 
                //TODO: call other services...             
            } catch (Exception ex) {
                _auditingManager.Current.Log.Exceptions.Add(ex);
                throw;
            }           
            finally
            {
                await auditingScope.SaveAsync();
            }        
        }     
    }
}

一旦注入IAuditingManager服務後,可以使用BeginScope方法創建新的作用域,然後,創建一個try-catch塊來保存審計日誌,包括異常情況。在try內,可以執行邏輯,調用任何其他服務。所有這些操作及更改最終都會在finally中被保存爲單個審計日誌對象。

在審計日誌作用域內,_auditingManager.Current.Log可用於獲取當前審計日誌對象,用於審查或其他操作(例如,添加註釋或其他信息)。超出審計日誌作用域,_auditingManager.Current將返回null,因此如果不確定是否存在審計作用域,請務必檢查null值。

接下來,我們看看審計日誌系統的配置選項options

審計選項

AbpAuditingOptions用於配置審計默認選項,如下例所示:

Configure(options => { options.IsEnabled = false; });

您可以在模塊的ConfigureServices方法內配置options。有關審計系統的主要選項,請參見以下列表:

  • IsEnabled (bool; default: true):禁用審計。
  • IsEnabledForGetRequests (bool; default: false):默認情況下,ABP不會保存HTTPGET請求的審計日誌,因爲GET請求不應該更改數據庫。但是,將其設置爲true後也會啓用GET請求的審計日誌記錄。
  • IsEnabledForAnonymousUsers (bool; default: true):如果只想爲經過身份驗證的用戶做審計,請將其設置爲false。如果爲匿名用戶保存審計日誌,這些用戶的ID值將爲null
  • AlwaysLogOnException (bool; default: true):如果應用程序出現異常,ABP將默認保存審計日誌(IsEnabledForGetRequestsIsEnabledForAnonymousUsers選項將不做考慮)。將此設置爲false可禁用該行爲。
  • hideErrors (bool; default: true):將審計日誌對象保存到數據庫時忽略異常。將其設置爲false以拋出異常。
  • ApplicationName (string; default: null):如果多個應用使用同一數據庫保存審計日誌,則可以在每個應用中設置此選項,以便根據應用名稱篩選日誌。
  • IgnoredTypes (List<Type>):忽略審計日誌中的某些特定類型,包括實體類型。

除了這些全局選項外,還可以啓用/禁用實體的更改跟蹤。

啓用實體歷史記錄

審計日誌對象包含實體/屬性更改的詳細信息,默認情況下,它對所有實體都是禁用的,因爲全部開啓會將太多日誌寫入數據庫,從而迅速增加數據庫容量。

[warning] 所以,建議只對要跟蹤的實體進行啓用。

有兩種方法可以爲實體啓用歷史記錄,如下所述:

  • [Auditing]屬性,用於爲單個實體啓用它,後續將詳細介紹。
  • EntityHistorySelectors選項,用於爲多個實體啓用它。
    在以下示例中,我爲所有實體啓用EntityHistorySelectors選項:
Configure(options => { options.EntityHistorySelectors.AddAllEntities(); });

AddAllEntities方法是一種快捷方式。EntityHistorySelectors是命名選擇器的列表,您可以添加lambda表達式來選擇所需的實體。以下代碼相當於前面的配置代碼:

Configure(options => { options.EntityHistorySelectors.Add(new NamedTypeSelector("MySelectorName", type => true)); });
  • NamedTypeSelector的第一個參數是選擇器名稱MySelectorName。選擇器名稱是任意的,用於從選擇器列表中查找或刪除選擇器。
  • NamedTypeSelector的第二個參數採用一個表達式。它爲您提供一個實體type,並等待truefalse。如果要爲給定實體類型啓用實體歷史記錄,則返回true。因此,可以傳遞一個表達式(比如type => type.Namespace.StartsWith("MyRootNamespace"),選擇具有名稱空間的所有實體)。您可以根據需要添加任意多個選擇器。如果其中一個返回true,則該實體用於記錄屬性更改。

除了這些全局選項和選擇器之外,還可以根據類、方法和屬性級別來啓用/禁用審計日誌記錄。

禁用/啓用審計日誌詳細記錄

使用審計日誌時,通常需要記錄每次訪問。但是,在某些情況下,我們可能希望禁用某些特定操作或實體的審計日誌。比如:

  • 操作參數寫入日誌可能很危險(例如,它可能包含用戶的密碼);
  • 操作調用或實體更改可能超出用戶的控制,不值得記錄;
  • 操作可能是一個批量操作,寫入太多審計日誌會降低性能。

ABP定義了[DisableAuditing][Audited]屬性,以聲明方式控制記錄的對象。審計日誌記錄有兩個目標可以控制:服務調用和實體歷史記錄。

1 服務調用

默認情況下,審計日誌中包括應用服務方法、Razor頁面處理程序(Razor Page handlers)和模型視圖控制器(MVC)控制器操作(controller actions)。要禁用它們,可以在類或方法級別使用[DisableAuditing]屬性。

以下示例在應用服務類上使用[DisableAuditing]屬性:

[DisableAuditing] 
public class OrderAppService : ApplicationService, IOrderAppService 
{     
    public async Task CreateAsync(CreateOrderDto input){ }     
    public async Task DeleteAsync(Guid id){ } 
}

以上方法,ABP將排除服務OrderAppService所有方法的審計記錄。如果只想禁用其中一種方法,比如CreateAsync,可以在方法級別使用它:

public class OrderAppService : ApplicationService, IOrderAppService 
{     
    [DisableAuditing]     
    public async Task CreateAsync(CreateOrderDto input) { }     
    public async Task DeleteAsync(Guid id) { } 
}

在這種情況下,CreateAsync方法的調用將不做審計,而對DeleteAsync方法的調用被寫入審計日誌對象中。使用以下代碼可以實現相同的行爲:

[DisableAuditing] 
public class OrderAppService : ApplicationService, IOrderAppService {     
    public async Task CreateAsync(CreateOrderDto input)     {     }     
    [Audited]     
    public async Task DeleteAsync(Guid id)     {     } 
}

其中,除DeleteAsync方法之外,所有方法的審計日誌都被禁用,因爲DeleteAsync方法聲明瞭[Audited]屬性。

[success] [Audited][DisableAuditing]屬性可用於任何類,任何方法,以便在該類或方法上啓用/禁用審計日誌記錄。

審計日誌對象包含方法調用信息,它還包括已執行方法的所有參數。這對了解系統進行了哪些更改非常有用;但是,在某些情況下,可能需要排除輸入的某些屬性。比如,你不想將用戶的信用卡信息包含在審計日誌中。在這種情況下,可以對輸入對象的屬性使用[DisableAuditing]

public class CreateOrderDto 
{     
    public Guid CustomerId { get; set; }     
    public string DeliveryAddress { get; set; }     
    [DisableAuditing]     
    public string CreditCardNumber { get; set; }
}

本示例將DtoCreditCardNumber屬性從審計日誌中排除,ABP不會將它寫入審計日誌。

禁用方法審計日誌不會影響實體歷史記錄。如果某個實體發生了更改,仍會記錄更改。下面將介紹如何控制實體歷史記錄的審計日誌。

2 實體歷史記錄

在啓用實體歷史記錄部分,我們介紹瞭如何通過定義選擇器爲一個或多個實體啓用實體歷史記錄。但是,如果要爲單個實體啓用記錄,有一種更簡單的替代方法:只需在實體類上方添加[Audited]屬性:

[Audited] 
public class Order : AggregateRoot { }

在本例,我向訂單實體添加了[Audited]屬性,從而爲該實體啓用實體歷史記錄。

假設您已使用選擇器爲所有實體啓用記錄,但希望爲特定實體禁用它們。在這種情況下,可以對該實體類使用[DisableAuditing]屬性。
[DisableAuditing]屬性也可以用於實體的屬性,以將該屬性從審計日誌中排除,如下例所示:

[Audited] 
public class Order : AggregateRoot {     
    public Guid CustomerId { get; set; }     
    [DisableAuditing]     
    public string CreditCardNumber { get; set; } 
}

如上,ABP不會將CreditCardNumber值寫入審計日誌。

3 存儲審計日誌

ABP框架設計之時,對需要接觸數據源的任何地方引入抽象,從而不用擔心具體的存儲問題,審計日誌系統也不例外。它定義了IAuditingStore接口來抽象保存審計日誌對象的位置。該接口只有一個方法:

Task SaveAsync(AuditLogInfo auditInfo);

您可以實現此接口,以便在需要的地方保存審計日誌。如果您使用ABP的啓動模板創建解決方案,審計日誌被默認保存到主庫。

以上介紹了控制和定製審計日誌系統的不同方法。它是系統跟蹤和記錄更改的基礎設施。下一節將介紹緩存系統,這是Web應用的另一個基礎功能。

四、緩存數據

緩存是提高應用性能和可伸縮性的基礎設施之一。ABP擴展了ASP.NET Core的分佈式緩存系統,使其與ABP框架的其他功能兼容,例如多租戶。

如果您運行應用的多個實例或擁有分佈式系統(如微服務),則分佈式緩存是必不可少的。它提供了不同應用之間數據的一致性,並允許共享緩存的值。分佈式緩存通常是一個外部的獨立應用程序,如RedisMemcached

建議使用分佈式緩存系統,即使只有一個正在運行的實例。不要擔心性能,因爲分佈式緩存的默認在內存中工作。這意味着它不是分佈式的,除非您顯式地配置一個真正的分佈式緩存提供程序,比如Redis

ASP Core中的分佈式緩存

本節主要介紹ABP的緩存功能,並不涵蓋所有ASP.NET Core的分佈式緩存系統。如果你想了解更多,您可以參考Microsoft的文檔

在本節,我將展示如何使用IDistributedCache<T>接口、配置選項,以及錯誤處理和批處理操作。我們還將學習如何使用Redis作爲分佈式緩存提供程序。最後,我將討論緩存過期。

使用IDistributedCache接口

ASP.NET Core定義了IDistributedCache接口,但它不是類型安全的。它支持的存儲和讀取類型是字節數組,而不是對象。而ABP的IDistributedCache<T>接口被設計爲具有類型安全的泛型接口(T代表存儲在緩存中的類型)。它在內部使用標準的IDistributedCache接口,與ASP.NET Core 100%兼容。ABP的IDistributedCache<T>接口有兩個優點,如下所示:

  • 自動將對象序列化/反序列化爲JSON,然後是字節數組byte。所以,您無需處理序列化和反序列化。
  • 它會自動爲緩存的Key添加前綴,以允許對不同類型的緩存對象使用相同的Key

使用IDistributedCache<T>接口的第一步是:定義一個類來表示緩存中的項,如:

public class UserCacheItem {     
    public Guid Id { get; set; }     
    public string UserName { get; set; }     
    public string EmailAddress { get; set; } 
}

這是一個普通的C#類,唯一的限制是它應該是可序列化的,因爲它在保存到緩存時被序列化爲JSON,而從緩存讀出時被反序列化(保持簡單,不要添加無關的引用)。

定義了緩存類後,第二步:我們可以注入IDistributedCache<T>接口,如以下代碼塊所示:

public class MyUserService : ITransientDependency 
{     
    private readonly IDistributedCache<UserCacheItem > _userCache;     
    public MyUserService(IDistributedCache<UserCacheItem > userCache)     
    { 
        _userCache = userCache;     
    }
}

我注入IDistributedCache<UserCacheItem>服務來處理UserCacheItem對象的分佈式緩存。下面展示如何獲取緩存值,獲取不到則查詢數據庫:

public async Task GetUserInfoAsync(Guid userId)
{     
    return await _userCache.GetOrAddAsync(userId.ToString(), async () => await GetUserFromDatabaseAsync(userId), () => new DistributedCacheEntryOptions         
    { 
        AbsoluteExpiration = DateTimeOffset.Now.AddHours(1) }); 
    }

我向GetOrAddAsync方法傳遞了三個參數:

  • 第一個參數是緩存鍵,它是一個字符串值。
  • 第二個參數是一個工廠方法GetUserFromDatabaseAsync,如果在緩存中找不到,則執行該方法查詢數據庫。
  • 最後一個參數是返回DistributedCacheEntryOptions對象的工廠方法。它是可選的,用於配置緩存項的過期時間。僅當GetOrAddAsync方法Add操作時,纔會執行。

默認情況下,緩存鍵是string數據類型。另外,ABP還定義了另一個接口IDistributedCache<TCacheItem, TCacheKey>,允許您指定緩存Key,這樣就不需要手動將Key轉換爲字符串類型。我們可以注入IDistributedCache<UserCacheItem,Guid>服務,並在本例的第一個參數中刪除ToString()用法。

DistributedCacheEntryOptions選項用於控制緩存的生命週期:

  • AbsoluteExpiration:設置絕對過期時間,如上示例。
  • AbsoluteExpirationRelativeToNow:設置絕對過期時間的另一種方法。我們可以重寫一下上面的選項爲 AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1),結果是一樣的。
  • SlidingExpiration:設置緩存滑動過期時間,這意味着,如果繼續訪問緩存項,過期時間將自動延長。

如果未傳遞過期時間參數,則使用默認值。或者使用AbpDistributedCacheOptions全局選項(後續介紹)。在此之前,讓我們看看IDistributedCache<UserCacheItem>服務的其他方法,如下所示:

  • GetAsync 使用Key從緩存中讀取數據;
  • SetAsync 將項目保存到緩存中(覆寫);
  • RefreshAsync 重置滑動過期時間;
  • RemoveAsync 從緩存中刪除項目。

關於同步緩存方法

所有方法都有同步版本,比如GetAsync方法的GET方法。但是,建議儘可能使用異步版本。
以上方法是ASP.NET Core的標準方法。ABP爲每個方法添加了批處理的方法,例如GetManyAsync 之於GetAsync。如果有很多項要讀或寫,那麼使用批處理方法可以顯著提高性能。ABP 框架還定義了 GetOrAddAsync方法(見以上示例),用於安全地讀取緩存值,並在方法調用中設置緩存值。

配置緩存選項

AbpDistributedCacheOptions是配置緩存的主要選項類。您可以在模塊類的ConfigureServices方法中對其進行配置(在領域或應用層中配置),如下所示:

Configure(options => { 
    options.GlobalCacheEntryOptions.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(2); 
});

這裏配置了GlobalCacheEntryOptions屬性,將默認緩存過期時間配置爲2小時。
AbpDistributedCacheOptions還有一些其他屬性,如下所述:

  • KeyPrefix (string; default: null):添加到所有緩存鍵開頭的前綴。當使用多個應用共享的分佈式緩存時,此選項可用於隔離應用程序的緩存項。
  • hideErrors (bool; default: true):用於控制緩存服務方法上錯誤處理的默認值。

正如您在前面的示例中所看到的,可以通過將參數傳遞給IDistributedCache服務的方法來覆蓋這些選項。

錯誤處理

當我們使用外部進程(如Redis)進行分佈式緩存時,讀寫緩存時可能會出現問題,比如:緩存服務器可能掉線,或者出現網絡問題。這些臨時問題在大多數情況下都可以忽略,尤其是試圖從緩存中讀取數據時。如果緩存服務目前不可用,您可以嘗試從原始數據源讀取數據,它可能比較慢,但比總比拋出異常要好。

所有IDistributedCache<T>方法都會獲得一個可選的hideErrors參數來控制異常處理。如果傳遞false,則拋出所有異常。如果傳遞true,則ABP將隱藏與緩存相關的錯誤。如果未指定值,將使用默認值。

在多租戶中使用緩存

如果應用程序是多租戶,ABP會自動將當前租戶的ID添加到緩存Key中,以區分不同租戶的緩存值。通過這種方式,它在租戶之間提供了隔離。
如果要創建租戶之間共享緩存,可以在緩存類上使用[IgnoreMultiTenancy]屬性,如以下代碼塊所示:

[IgnoreMultiTenancy] 
public class MyCacheItem { /* ... */ }

在本例中,不同租戶都可以訪問MyCacheItem值。

使用Redis作爲分佈式緩存

Redis是一種流行的工具,用作分佈式緩存。ASP.NET Core爲Redis提供了一個緩存集成包。您可以參照Microsoft的文檔
ABP也提供了一個Redis集成包,它擴展了Microsoft的集成,以支持批處理操作(例如GetManyAsync,在使用IDistributedCache<T>接口一節中提到)。因此,建議使用ABP的Volo.Abp.Caching.StackExchangeRedisNuGet包,或者使用命令行在對應的項目中安裝:

abp add-package Volo.Abp.Caching.StackExchangeRedis

安裝完成後,只需在``appsettings.json配置Redis`服務器的連接字符串和端口,如下所示:

"Redis": { "Configuration": "127.0.0.1" }

有關配置的詳細信息,請參閱Microsoft文檔

緩存失效

有一種流行的說法是,緩存失效是計算機科學中的兩個難題之一(另一個是命名問題)。緩存值通常是數據的副本(需要頻繁讀取或者計算的值)。用於提高性能和可伸縮性,但當原始數據發生變更,緩存值過時時,問題就來了。我們應該仔細觀察這些變化,對緩存中的值進行及時地刪除或刷新,這就是所謂的緩存失效。

緩存失效在很大程度上取決於緩存的數據和應用程序邏輯。但在某些特定情況下,ABP可以幫助您使緩存的數據失效。

  • 一種特殊情況是,當實體發生更改(更新或刪除)時,我們可能希望緩存數據失效。對於這種情況,我們可以註冊ABP發佈的事件。當相關用戶實體發生更改時,以下代碼將使用戶緩存失效:
public class MyUserService : ILocalEventHandler<EntityChangedEventData<IdentityUser>>,  ITransientDependency 
{     
    private readonly IDistributedCache<UserCacheItem> _userCache;     
    private readonly IRepository<IdentityUser, Guid> _userRepository;     
    //...omitted other code parts     
    public async Task HandleEventAsync(EntityChangedEventData<IdentityUser> data)
    { 
        await _userCache.RemoveAsync(data.Entity.Id.ToString());     
    }
}

MyUserService註冊了EntityChangedEventData<IdentityUser>本地事件。當創建一個新的IdentityUser實體或更新/刪除現有IdentityUser實體時,會觸發此事件。HandleEventAsync方法用於將用戶從緩存中移除。

因爲本地事件只在當前的過程中起作用,這意味着處理類(此處爲MyUserService)應該與實體更改處於相同的進程中。

關於事件總線系統

本地和分佈式事件是ABP框架的特性,本書中沒有包括這些特性。如果您想了解更多信息,請參閱ABP文檔

五、本地化用戶界面

在做產品需求設計時,您可能希望本地化當前的UI。ASP.NET Core提供了一個本地化系統。ABP擴展的新功能和約定,使本地化更加容易和靈活。

本節介紹如何定義語言,爲語言創建並讀取文本。您將瞭解本地化資源的概念和嵌入式本地化資源文件。

我們從定義語言開始:

定義語言

本地化的第一個問題是:您希望在UI上支持哪些語言?ABP提供了一個定義語言的選項AbpLocalizationOptions,如以下代碼塊所示:

Configure(options => {
    options.Languages.Add(new LanguageInfo("en", "en", "English"));     
    options.Languages.Add(new LanguageInfo("tr", "tr", "Türkçe"));
    options.Languages.Add(new LanguageInfo("es", "es","Español")); 
});

以上代碼寫在模塊類的ConfigureServices方法中。如果您是使用ABP啓動模板創建的解決方案,該配置已經完成。您只需根據需要編輯即可。

LanguageInfo構造函數接受幾個參數:

  • cultureName: 語言的區域性名稱(代碼),運行時會設置爲CultureInfo.CurrentCulture
  • uiCultureName: 語言的UI區域性名稱(代碼),運行時會設置爲CultureInfo.CurrentUICulture
  • displayName: 顯示給用戶的語言的名稱。建議用原語寫下這個名字;
  • flagIcon: 顯示語言所屬的國旗的字符串值。

ABP根據當前HTTP請求確定選中的語言。

選中語言

ABP使用AbpRequestLocalizationMiddleware確定當前語言。這是一個添加到ASP.NET Core請求管道的中間件:

app.UseAbpRequestLocalization();

當請求通過此中間件時,將確定一種語言並將其設置爲CultureInfo.CurrentCultureCultureInfo.CurrentUICulture。這些是NET的標準做法。

當前語言是根據HTTP請求參數,按給定優先級順序確定的:
1.如果設置了culture查詢字符串參數,則由該參數確定當前語言。例如http://localhost:5000/?culture=en-US

2.如果設置了.AspNetCore.Culture的cookie值,將由該值確定當前語言。

3.如果設置了Accept-Language HTTP頭,將由該頭確定當前語言。
默認情況下,瀏覽器通常會發送最後一個。

關於ASP.NET Core的本地化系統

以上介紹的行爲是默認行爲,然而ASP.NET Core的語言確定更靈活,更可定製。有關更多信息,請參閱Microsoft文檔

在定義了要支持的語言之後,接下來就是定義本地化資源。

定義本地化資源

ABP與ASP.NET Core的本地化系統完全兼容。所以,你可以使用.resx文件作爲本地化資源(參看Microsoft文檔),然而,ABP提供了一種輕量級、靈活且可擴展的方法:使用簡單的JSON文件定義本地化文本。

當我們使用ABP啓動模板創建解決方案時,Domain.Shared項目已經包含了本地化資源和本地化JSON文件:

在本例,DemoAppResource表示本地化資源。一個應用程序可以有多個本地化資源,每個資源定義自己的JSON文件。您可以將本地化資源視爲一組本地化文本。它有助於構建模塊化系統,每個模塊都有自己的本地化資源。

本地化資源類是空類,如下代碼所示:

[LocalizationResourceName("DemoApp")] 
public class DemoAppResource { }

當你想使用本地化資源中的文本時,這個類指定相關的資源。LocalizationResourceName屬性爲資源設置字符串名稱。每個本地化資源都有一個唯一的名稱,在客戶端也可以引用該資源進行本地化。

默認本地化資源

在創建ABP解決方案時,通常有一個(默認)本地化資源,默認本地化資源類的名稱以項目名稱開頭,例如ProductManagementResource

一旦我們有了本地化資源,我們就可以爲我們支持的每種語言創建一個JSON文件。

使用本地化JSON文件

本地化文件是一個簡單的JSON格式的文件,如以下代碼塊所示

{
    "culture":"en",
    "texts": {
        "Home":"Home",
        "WelcomeMessage":"Welcome to the application."
    } 
}

該文件中有兩個根元素,如下所述:

  • culture:語言文化代碼。它與定義語言部分中引入的區域性代碼相匹配。
  • texts:包含本地化文本的鍵值對。鍵用於訪問本地化文本,值是當前區域性(語言)的本地化文本。

定義完語言的本地化文本之後,我們在運行時請求本地化文本。

讀取本地化文本

ASP.NET Core定義了一個IStringLocalizer<T>接口,以獲取當前文化中的本地化文本,其中T代表本地化資源類。您可以將該接口注入到類中,如以下代碼塊所示:

public class LocalizationDemoService : ITransientDependency {     
    private readonly IStringLocalizer<DemoAppResource> _localizer;     
    public LocalizationDemoService(IStringLocalizer<DemoAppResource> localizer)     
    {
        _localizer = localizer; 
    }     
    public string GetWelcomeMessage()    
    {         
        return _localizer["WelcomeMessage"];     
    } 
}

其中LocalizationDemoService注入IStringLocalizer<DemoAppResource>服務,用於訪問DemoAppResource的本地化文本。在GetWelcomeMessage方法中,我們獲取WelcomeMessage鍵的本地化文本。如果當前語言爲英語,則返回Welcome to the application,正如上一節的JSON文件中定義的那樣。

我們可以在本地化文本時傳遞參數。

參數化文本

本地化文本可以包含參數,如下例所示:

"WelcomeMessageWithName": "Welcome {0} to the application."

參數可以傳遞到定位器,如以下代碼塊所示:

public string GetWelcomeMessage(string name)
{
    return _localizer["WelcomeMessageWithName", name]; 
}

給定名稱將替換{0}佔位符

回退邏輯

當在當前區域性的JSON文件中找不到請求的文本時,本地化系統會回退到父區域性或默認區域性的文本。

例如,假設您請求獲取WelcomeMessage文本,而當前區域性(CultureInfo.CurrentUICulture)爲de-DE(德語),會出現以下的其中一種情況:

  • 如果在JSON文件中沒有定義"culture": "de-DE",或者JSON文件中不包含WelcomeMessage鍵,那麼它會返回到父區域性("de"),嘗試在該區域性中查找給定的鍵。
  • 如果在父區域性中找不到它,它將返回到本地化資源的默認區域性。
  • 如果在默認區域性中找不到,則返回給定的鍵(例如WelcomeMessage)作爲響應。

配置本地化資源

在使用本地化資源之前,應將其添加到AbpLocalizationOptions中。此配置已在啓動模板中完成,代碼如下:

Configure<AbpVirtualFileSystemOptions>(options => {
    options.FileSets.AddEmbedded<DemoAppDomainSharedModule>();      
}); 
Configure<AbpLocalizationOptions>(options => {
    options.Resources.Add<DemoAppResource>("en").AddBaseTypes(typeof(AbpValidationResource)).AddVirtualJson("Localization/DemoApp");
    options.DefaultResourceType = typeof(DemoAppResource); 
});

本地化JSON文件通常被定義爲嵌入式資源。我們使用AbpVirtualFileSystemOptions配置ABP的虛擬文件系統,以便將該程序集中的所有嵌入文件添加到虛擬文件系統中(當然也包括本地化文件)。

然後,我們將DemoAppResource添加到Resources字典中,以便ABP識別它。這裏,"en"參數設置本地化資源的默認區域。

ABP的本地化系統相當高級,它允許您通過繼承本地化資源來重用本地化資源的文本。在本例中,我們繼承了AbpValidationResource,它在ABP框架中定義(包含標準的驗證錯誤消息)。
AddVirtualJson方法設置與該資源相關的JSON文件(使用虛擬文件系統)。

最後,DefaultResourceType設置默認本地化資源。在某些不指定本地化資源的地方,可以使用默認資源。

在特殊服務中本地化

在重複性注入IStringLocalizer<T>都會很乏味。ABP將它預注入到一些特殊的基類中。從這些類繼承後,可以直接使用L快捷方式。

以下示例顯示瞭如何在應用服務方法中使用本地化文本:

public class MyAppService : ApplicationService {
    public async Task FooAsync()
    {         
        var str = L["WelcomeMessage"];     
    } 
}

在本例中,L屬性由ApplicationService基類定義,因此不需要手動注入IStringLocalizer<T>服務。你可能會想,因爲我們還沒有指定本地化資源,這裏使用的是哪一個?答案是DefaultResourceType選項,這在上面已經解釋過。

如果要爲特定應用服務指定另一個本地化資源,請在服務的構造函數中設置LocalizationResource屬性:

public class MyAppService : ApplicationService 
{     
    public MyAppService()
    {         
       LocalizationResource = typeof(AnotherResource);     
    } 
    //... 
}

除了ApplicationService之外,其他一些常見的基類,如AbpControllerAbpPageModel,提供了與注入IStringLocalizer<T>服務相同的L快捷屬性。

在客戶端使用本地化

ABP的所有本地化資源都可以直接在客戶端上使用。
例如,在ASP.NET的MVC/Razor Pages中,通過JavaScript的WelcomeMessage鍵本地化:

var str = abp.localization.localize('WelcomeMessage', 'DemoApp');

DemoApp是本地化資源名稱,WelcomeMessage是此處的本地化鍵(第4部分的“用戶界面和API開發”將詳細介紹客戶端本地化)。

總結

在本章中,我們瞭解了幾乎所有Web應用都需要的一些基本功能。

ICurrentUser服務用於讀取應用的當前用戶的信息。您可以使用標準claims(例如usernameID),並根據需要定義自定義聲明。

我們探索了數據過濾系統,在從數據庫查詢時自動過濾數據。通過這種方式,我們可以很容易地實現一些軟刪除和多租戶。我們還學習瞭如何定義自定義數據過濾器,並在必要時禁用過濾器。

我們還了解了審計日誌系統如何跟蹤和保存用戶的所有操作。我們可以通過屬性和選項控制審計日誌系統。

緩存數據是提高系統性能和可伸縮性的另一個基本概念。我們已經瞭解了ABP的IDistributedCache<T>服務,它提供了一種類型安全的方式,並自動執行序列化和異常處理。

最後,我們探討了ASP.NET Core 和ABP的本地化基礎設施。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章