探索ABP基礎架構的橫切關注點

大家好,我是張飛洪,感謝您的閱讀,我會不定期和你分享學習心得,希望我的文章能成爲你成長路上的墊腳石,讓我們一起精進。

授權、驗證、異常處理和日誌記錄等橫切關注點是每個系統的基本組成部分,它們對於確保系統的安全和良好運行至關重要。

實現橫切關注點會導致應用中的很多地方出現重複代碼。此外,一次授權或驗證檢查缺失可能會導致整個系統崩潰。

ABP框架的主要目標之一是使你的應用“不要重複自己”(DRY),ASP.NET Core已經爲一些跨領域的問題提供了一個良好的基礎設施,但ABP進一步實現了自動化,讓使用更加容易。

本章探討了ABP的基礎設施:

  • 認證授權
  • 用戶驗證
  • 異常處理

認證和授權是安全中的兩個主要概念。身份驗證是識別當前用戶的過程,授權用於允許或禁止用戶執行應用的特定操作。

ASP.NET Core系統本身提供了一種高級而靈活的認證和授權,ABP框架的認證授權與ASP.NET Core100%兼容,並進行了一定的擴展,它允許將權限授予角色和用戶,它還允許在客戶端進行權限檢查。

簡單授權檢查

最簡單的場景,只允許登錄的用戶執行特定操作。
[Authorize]屬性不帶任何參數,只檢查當前用戶是否已通過身份驗證(登錄)。

請參見以下控制器(MVC):

public class ProductController : Controller {     
    public async Task GetListAsync(){}     
    [Authorize]         
    public async Task CreateAsync(ProductCreationDto input){}             
    [Authorize]     
    public async Task DeleteAsync(Guid id){} 
}

在本例中,CreateAsyncDeleteAsync操作僅允許通過身份驗證的用戶使用,假設匿名用戶(尚未登錄的用戶)嘗試執行這些操作,ASP.NET Core向客戶端返回授權錯誤響應。而GetListAsync方法對每個人都可用,甚至對匿名用戶也是如此。

Authorize可在Controller級別,用於授權內部的所有Actions操作。如果想允許匿名用戶執行特定操作,可以配置[AllowAnonymous]屬性。如以下代碼塊所示:

[Authorize] 
public class ProductController : Controller { 
    [AllowAnonymous]     
    public async Task> GetListAsync(){}   
    public async Task CreateAsync(ProductCreationDto input) {}      
    public async Task DeleteAsync(Guid id){}  
}

在這裏,我在類ProductController的頂部使用了[Authorize]屬性,在GetListAsync方法使用[AllowAnonymous]屬性,這使得尚未登錄的用戶也可以訪問GetListAsync方法。

雖然無參數的[Authorize]屬性有一些適用場景,但是如果我們想要定義特定的權限(或策略),使得所有經過身份驗證的用戶具有不同的權限。

權限系統

ABP框架對ASP.NET Core最重要的擴展是權限系統。權限是爲特定用戶或角色授予或禁止的策略,它與應用功能進行關聯,並在用戶嘗試使用該功能時進行檢查。如果當前用戶已被授予權限,則該用戶可以使用功能。否則,用戶無法使用該功能。

ABP提供了在應用中定義、授予和檢查權限的功能。

1 定義權限

在使用權限之前需要先定義權限,首先創建從PermissionDefinitionProvider類繼承的類。創建新的ABP解決方案時,會有一個空的權限定義提供程序類(在Application.Contracts項目中)。請參見以下示例:

public class ProductManagementPermissionDefinitionProvider : PermissionDefinitionProvider 
{     
    public override void Define(IPermissionDefinitionContext context)     
    {         
        var myGroup = context.AddGroup("ProductManagement");
        myGroup.AddPermission("ProductManagement.ProductCreation");
        myGroup.AddPermission"ProductManagement.ProductDeletion");     
    } 
}

ABP框架在應用啓動時調用Define方法。在本例中,我創建了一個名爲ProductManagement的權限組,並在其中定義了兩個權限,用於對用戶界面(UI)上的權限進行分組,通常每個模塊都要定義其權限組。組和權限名稱是任意string字符串值(建議定義const常量字段)。

這是一個最小的配置,您還可以將顯示名稱指定本地化字符串,並指定權限名稱,以便在UI上以用戶友好的方式顯示它們。以下代碼塊使用本地化系統指定顯示名稱,同時定義組和權限:

public class ProductManagementPermissionDefinitionProvider : PermissionDefinitionProvider 
{     
    public override void Define(IPermissionDefinitionContext context)     
    {         
        var myGroup = context.AddGroup("ProductManagement",L("ProductManagement"));
        myGroup.AddPermission("ProductManagement.ProductCreation",L("ProductCreation"));
        myGroup.AddPermission("ProductManagement.ProductDeletion",L("ProductDeletion"));
    }

    private static LocalizableString L(string name)
    {
        return LocalizableString.Create(name);
    } 
}

我定義了一個L方法來簡化本地化。(第8章“使用ABP的功能和服務”中將詳細介紹本地化系統)

多租戶中的權限定義

對於多租戶應用程序,可以爲AddPermission方法指定multiTenancySide參數,以定義僅限主機或僅限租戶的權限。(第16章“實現多租戶”中將詳細介紹多租戶)。

定義完權限後,下一次應用啓動後,該權限就可以使用了(在“權限管理”對話框中)。

2 管理權限界面

默認情況下,可以爲用戶或角色授予權限。假設您創建了一個經理角色(manager),並希望爲該角色授予產品權限。程序啓動後,我們導航到管理|身份管理|角色頁面。然後創建經理角色(如果之前沒有創建),請單擊權限操作按鈕,如圖所示

角色管理頁面

單擊權限按鈕後將打開一個對話框,如下所示:

在圖中,您可以在左側看到權限組,而該組中的權限在右側可用。權限組和我們定義的權限已經可以使用,無需進行任何額外操作。

具有經理角色的用戶都繼承該角色的權限。用戶可以有多個角色,並且繼承所有分配角色的所有權限的聯合。您還可以在“用戶管理”頁面上直接向用戶授予權限,以獲得更大的靈活性。

我們已經定義了權限並將其分配給了角色。下一步是檢查當前用戶是否具有請求的權限。

3 檢查權限

3.1[Authorize]屬性

您可以使用[Authorize]屬性以聲明的方式檢查權限,也可以使用IAuthorizationService以編程方式檢查權限。

我們可以重寫上面的ProductController類,以授予產品創建和刪除權限,如下所示:

public class ProductController : Controller 
{     
    public async Task<List<ProductDto>> GetListAsync(){}
    [Authorize("ProductManagement.ProductCreation")]     
    public async Task CreateAsync(ProductCreationDto input){}     
    [Authorize("ProductManagement.ProductDeletion")]     
    public async Task DeleteAsync(Guid id){} 
}

[Authorize]屬性將字符串參數作爲策略名稱。ABP將權限定義爲自動策略,您可以在需要指定策略名稱的任何位置使用權限名稱。

3.2 IAuthorizationService

聲明式授權易於使用,建議儘可能使用。但是,當您想要有條件地檢查權限或執行未授權案例的邏輯時,它是有限的。對於這種情況,可以注入並使用IAuthorizationService,如下例所示

public class ProductController : Controller 
{     
    private readonly IAuthorizationService  _authorizationService;
    public ProductController(IAuthorizationService authorizationService)
    {
        _authorizationService = authorizationService; 
    }          
    public async Task CreateAsync(ProductCreationDto input)
    {
        if (await _authorizationService.IsGrantedAsync("ProductManagement.ProductCreation")) 
        {  
            // TODO: Create the product  
        } 
        else
        {
            // TODO: Handle unauthorized case
        } 
    } 
}

IsGrantedAsync方法檢查給定的權限,如果當前用戶(或用戶的角色)已被授予權限,則返回true。如果您有自定義邏輯的權限要求,這將非常有用。但是,如果您只想檢查權限並對未經授權的情況拋出異常,CheckAsync方法更實用:

public async Task CreateAsync(ProductCreationDto input)
{
    await _authorizationService.CheckAsync("ProductManagement.ProductCreation");     
    //TODO: Create the product 
}

如果用戶沒有該操作的權限,CheckAsync方法會引發AbpAuthorizationException異常,該異常由ABP框架處理,並向客戶端返回HTTP響應。IsGrantedAsyncCheckAsync方法是ABP框架定義的有用的擴展方法。

[warning] 提示:從AbpController繼承

建議從AbpController類而不是標準Controller類派生。因爲它內部做了擴展,定義了一些有用的屬性。比如,它有AuthorizationService屬性(屬於IAuthorizationService類型),您可以直接使用它,無需手動注入IAuthorizationService接口。

服務器上的權限檢查是一種常見的方法。但是,您可能還需要檢查客戶端的權限。

4 客戶端權限

ABP公開了一個標準HTTP API,其URL爲/api/abp/application-configuration,返回包含本地化文本、設置、權限等的JSON數據。客戶端可以使用該API來檢查權限或在客戶端執行本地化。

不同的客戶端類型可能會提供不同的服務來檢查權限。例如,在MVC/Razor Pages中,可以使用abp.authJavaScript API檢查權限,如下所示:

abp.auth.isGranted('ProductManagement.ProductCreation');

這是一個全局函數,如果當前用戶具有給定的權限,則返回true。否則,返回false
在Blazor應用程序中,可以重用相同的[Authorize]屬性和IAuthorizationService
我們將在第4部分“用戶界面和API開發”中詳細介紹客戶端權限檢查。

5 子權限

在複雜的應用中,可能需要創建一些依賴於其父權限的子權限。當父權限被授予時,子權限才能正常工作。

角色管理權限具有一些子權限,如創建、編輯和刪除。角色管理權限用於授權用戶進入角色管理頁面。如果用戶無法進入該頁面,那麼授予角色創建權限就沒有意義,因爲不進入該頁面幾乎不可能創建新角色。

在權限定義類中,AddPermission方法返回創建的權限,並將其分配給變量,變量使用AddChild方法創建子權限,如下代碼塊所示

public override void Define(IpermissionDefinitionContext context) 
{
    var myGroup = context.AddGroup("ProductManagement",L("ProductManagement"));
    var parent = myGroup.AddPermission("MyParentPermission");
    parent.AddChild("MyChildPermission"); 
}

在本例,我們創建了一個名爲MyParentPermission的父權限,然後創建了另一個名爲MyChildPermission的子權限。
子權限也可以具有子權限,比如我們可以把parent.AddChild的返回值賦予一個變量,然後調用它AddChild方法繼續添加子權限。

通過開/關策略授權來定義和使用權限,顯得簡單而強大,然而,ASP.NET Core允許創建完整的自定義邏輯來定義策略。

基於策略的授權

ASP.NET Core基於策略的授權機制允許您授權應用中的某些操作,就像使用權限一樣。但這一次,使用代碼表示的自定義邏輯,實際上是ABP框架提供的一種簡單且自動化的策略。

定義權限需求

首先需要定義一個創建產品的權限需求(我們可以在應用層中定義這些類),稍後檢查,代碼段:

public class ProductCreationRequirement :  IAuthorizationRequirement { }

ProductCreationRequirement是一個空類,僅實現IAuthorizationRequirement接口。然後,爲該需求定義一個授權處理程序ProductCreationRequirementHandler,如下所示:

public class ProductCreationRequirementHandler : AuthorizationHandler<ProductCreationRequirement> 
{     
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,ProductCreationRequirement requirement)
    {
        if (context.User.HasClaim(c => c.Type == "productManager"))
        {
            context.Succeed(requirement);
        } 
        return Task.CompletedTask;
    } 
}

處理程序必須派生自AuthorizationHandler<T>,其中TProductCreationRequirement類型。在本例中,我只是檢查了當前用戶是否擁有productManager聲明,這是我的自定義聲明(聲明是存儲在身份驗證票據中的值)。您可以構建自定義邏輯。如果允許當前用戶擁有創建產品需求,你要做的就是調用context.Succeed上下文。

定義權限需求和處理程序後,需要在模塊類的ConfigureServices方法中註冊它們,如下所示:

public override void ConfigureServices(ServiceConfigurationContext context) 
{
    Configure<AuthorizationOptions>(options => 
    {
        options.AddPolicy("ProductManagement.ProductCreation",
        policy => policy.Requirements.Add(new ProductCreationRequirement()));
    });
    context.Services.AddSingleton<IAuthorizationHandler,ProductCreationRequirementHandler>(); 
}

我使用AuthorizationOptions定義了一個名爲ProductManagement.ProductCreation的策略。然後,我將ProductCreationRequirementHandler註冊爲單例服務。

現在,假設我對ControllerAction使用[Authorize("ProductManagement.ProductCreation")]屬性,或者使用IAuthorizationService檢查策略,我的自定義授權處理程序就可以進行授權邏輯處理了。

權限與自定義策略

一旦實現了自定義策略,就不能使用“權限管理”對話框向用戶和角色授予權限,因爲它不是一個簡單的啓用/禁用權限。然而,客戶端策略檢查仍然有效,因爲ABP很好地集成到ASP.NET Core的政策體系。

如果您只需要開/關方式的策略,ABP的權限系統很容易很強大,而自定義策略允許您使用自定義邏輯動態檢查策略。

基於資源的授權

ASP.NET Core的授權系統比本文介紹的功能更多。基於資源的授權是一種允許您基於對象(如實體)控制策略的功能。例如,您可以控制刪除特定產品的訪問權限,而不是對所有產品擁有共同的刪除權限。ABP與ASP.NET Core完全兼容。建議你查看ASP.NET Core的文檔,以瞭解有關授權的更多信息。
到目前爲止,我們已經在MVC控制器上看到了[Authorize]屬性的用法。但是,此屬性和IAuthorizationService不限於控制器。

控制器之外的授權

ASP.NET Core允許您對Razor頁面、Razor組件和Web層中的一些地方使用[Authorize]IAuthorizationService

ABP框架更進一步,允許對服務類和方法使用[Authorize]屬性,而不依賴於Web層,即使在非Web應用程序中也是如此。因此,這種用法完全有效,如下所示:

public class ProductAppService : ApplicationService, IProductAppService 
{
    [Authorize("ProductManagement.ProductCreation")]
    public Task CreateAsync(ProductCreationDto input)
    {
        // TODO     
    } 
}

只有當前用戶擁有ProductManagement.ProductCreation(產品創建)權限/策略時,才能執行CreateAsync方法。實際上,[Authorize]在任何註冊爲依賴注入(DI)的類中都是可用的。然而,由於授權被認爲是應用層的一個功能,因此建議在應用層而不是領域層使用授權。

動態代理/攔截器

ABP使用使用攔截器的動態代理來完成方法調用的授權檢查。如果通過類引用(而不是接口引用)注入服務,動態代理系統將使用動態繼承技術。在這種情況下,必須使用virtual關鍵字定義方法,以允許動態代理系統覆蓋它並執行授權檢查。

驗證類別

驗證可確保數據的安全性和一致性,並幫助應用程序正常運行。驗證話題很廣,有一些常見的驗證類別:

  • 客戶端驗證:用於在將數據發送到服務器之前預先驗證用戶輸入。這對用戶體驗(UX)很重要,您應該儘可能地實現它。例如,檢查所需的文本框字段是否爲空是一種客戶端驗證。(我們將在第4部分“用戶界面和API開發”中介紹客戶端驗證)
  • 服務器端驗證:由服務器執行,以防止不完整、格式錯誤或惡意請求。它爲應用程序提供一定程度的安全性。例如,檢查服務器端的必填輸入字段是否爲空就是此類驗證的一個例子。
  • 業務驗證:也在服務器中執行,用於驗證業務規則,並保證業務數據的一致性。它在業務代碼的每一個級別都可以執行,例如,在轉賬之前檢查用戶的餘額是一種業務驗證。

關於ASP.NET Core的驗證系統:
ASP.NET Core爲驗證提供了許多選項。本書重點介紹ABP框架添加的功能。

本節重點介紹服務端驗證,以及驗證過程和驗證異常處理的方法。
讓我們從最簡單的數據註釋特性驗證開始:

註釋驗證(Data annotation attributes)

public class ProductAppService : ApplicationService, IProductAppService 
{     
    public Task CreateAsync(ProductCreationDto input)
    {
         // TODO     
    } 
}

public class ProductCreationDto {
    [Required]     
    [StringLength(100)]     
    public string Name { get; set; }
    [Range(0, 999.99)]     
    public decimal Price { get; set; }          
    [Url]     
    public string PictureUrl { get; set; }     
    public bool IsDraft { get; set; }
 }

ProductAppService是應用服務,它的入參ProductCreationDto在ABP框架中自動驗證,就像ASP.NET Core MVC框架一樣。

ProductCreationDto有三個驗證屬性,採用的是ASP.NET Core有內置的驗證屬性,此外ASP.NET Core還有其他內置驗證屬性:

  • [Required]: 非空驗證
  • [StringLength]: 字符串長度大小驗證
  • [Range]: 範圍驗證
  • [Url]: Url格式驗證
  • [RegularExpression]: 正則表達式(regex)驗證
  • [EmailAddress]: 電子郵件驗證

ASP.NET Core還允許您通過繼承ValidationAttribute類並重寫IsValid方法來自定義驗證。

註釋驗證簡單易用,推薦在DTO和模型上使用。但不適用自定義邏輯驗證(會受到限制)

使用接口 IValidatableObject自定義驗證

模型或DTO對象可以實現 IValidatableObject接口,實現自定義代碼塊驗證。請參見以下示例:

public class ProductCreationDto : IValidatableObject 
{     
    ...     
    [Url]     
    public string PictureUrl { get; set; }
    public bool IsDraft { get; set; }
    public IEnumerable Validate(ValidationContext context)
    {
        if (IsDraft == false && string.IsNullOrEmpty(PictureUrl)) 
        {
            yield return new ValidationResult("Picture must be provided to publish a product",new []{ nameof(PictureUrl) }); 
        } 
    } 
}

在本例中,ProductCreationDto有一個自定義規則:如果IsDraftfalse,並且圖片路徑爲控,則提示需要上傳圖片。
如果需要從DI系統解析服務,可以使用context.GetRequiredService方法。例如,如果我們想本地化錯誤消息,我們可以重寫Validate方法,如下代碼塊所示:

public IEnumerable Validate(ValidationContext context) 
{
    if (IsDraft == false && string.IsNullOrEmpty(PictureUrl)) 
    {
        var localizer = context.GetRequiredService<IStringLocalizer<ProductManagementResource>();
        yield return new ValidationResult(localizer["PictureIsMissingErrorMessage"],new []{ nameof(PictureUrl) }); 
    } 
}

這裏,我們從DI解析IStringLocalizer<ProductManagementResource>實例,並用它向客戶端返回本地化錯誤消息。(我們將在第8章詳細介紹本地化系統)

正式驗證與業務驗證

作爲最佳實踐,只在DTO/Model類中實現正式驗證。然而,在應用或領域層服務中的業務邏輯驗證,例如,檢查數據庫中是否已經存在給定的產品名稱,則不要在Validate方法中驗證。

驗證異常

1 自動異常

如果用戶輸入無效,ABP框架會自動拋出AbpValidationException類型的異常。以下情況會引發異常:

  • 輸入對象爲null,因此不需要檢查它是否爲null。
  • 輸入對象總是無效的,所以您不必在API控制器中檢查 Model.IsValid

在這些情況下,ABP不會調用您的服務方法(或Controller Action)。要想正確執行,必須確保輸入不爲null而且有效。

2 手動異常

如果在服務內部執行其他驗證,並希望引發與驗證相關的異常,還可以引發AbpValidationException,如以下代碼段所示:

public async Task CreateAsync(ProductCreationDto input) {
    if (await HasExistingProductAsync(input.Name)){
        throw new AbpValidationException(new List<ValidationResult>{new ValidationResult("Product name is already in use!", new[] {nameof(input.Name)})});
    } 
}

這裏,我們假設HasExistingProductAsync在存在產品時返回true。我們通過指定驗證錯誤來拋出AbpValidationExceptionValidationResult表示驗證錯誤;它的第一個構造函數參數是驗證錯誤消息,第二個參數(可選)是DTO屬性的名稱。

一旦您或ABP驗證系統拋出AbpValidationException異常,ABP異常處理系統將捕獲並處理它。

禁用驗證

可以使用[DisableValidation]在方法或類級別繞過ABP驗證系統,如下例所示:

[DisableValidation] 
public async Task CreateAsync(ProductCreationDto input) { }

在本例中,CreateAsync方法用[DisableValidation]修飾,因此ABP不會對輸入對象執行任何自動驗證。
如果對類使用[DisableValidation],則該類的所有方法的驗證都將被禁用。在這種情況下,可以對某個方法使用[EnableValidation],以便僅對該特定方法啓用驗證。

當禁用方法的自動驗證時,仍然可以執行自定義驗證邏輯並拋出AbpValidationException,如前一節所述。

其他類型的驗證

除了對Controller ActionsRazor Page handlers執行驗證,ABP還允許爲應用中的任何類啓用自動驗證功能。您只需實現IValidationEnabled接口,如下例所示:

public class SomeServiceWithValidation : IValidationEnabled, ITransientDependency { ... }

然後,ABP使用本章介紹的驗證系統自動驗證所有輸入。

動態代理/攔截器

ABP使用使用攔截器的動態代理來完成方法調用的驗證。如果通過類引用(而不是接口引用)注入服務,動態代理系統將使用動態繼承技術。在這種情況下,必須使用virtual關鍵字定義方法,以允許動態代理系統覆蓋它並執行驗證。

到目前爲止,我們已經介紹了與ASP.NET Core兼容的ABP驗證系統。最後我們將介紹FluentValidation庫集成,它允許您將驗證邏輯與驗證對象分離。

整合FluentValidation庫

大多數情況,內置的驗證系統就足夠了,而且它很容易定義驗證規則,我個人認爲它沒有任何問題,在DTO/model類中嵌入數據驗證邏輯是完全可行的。然而,一些開發人員認爲DTO/model類內部嵌入驗證邏輯是一種糟糕的做法。在這種情況下,ABP提供了一個與流行的FluentValidation庫的集成包,它將驗證邏輯與DTO/model類分離,並提供了比標準註釋驗證方法更強大的功能。

要使用FluentValidation庫,首先需要將其安裝到項目中。可以使用ABP命令行界面(ABP CLI)的add-package命令爲項目安裝它,如下所示:

abp add-package Volo.Abp.FluentValidation

安裝完軟件包後,可以創建驗證類並設置驗證規則,如下代碼塊所示:

public class ProductCreationDtoValidator : AbstractValidator 
{
    public ProductCreationDtoValidator()
    {
        RuleFor(x => x.Name).NotEmpty().MaximumLength(100);
        RuleFor(x => x.Price).ExclusiveBetween(0, 1000);
        //...     
    } 
}

具體請參閱FluentValidation文檔,瞭解如何定義更高級的驗證規則:.

ABP自動發現驗證類,並將它們集成到驗證過程中。這意味着您甚至可以將標準驗證邏輯與FluentValidation驗證類混合使用。

一個系統最重要的質量指標之一是:它如何響應錯誤和異常情況。它應該積極處理錯誤,並向客戶端返回正確的響應,並優雅地將問題告知用戶。

在Web開發中,如果每個客戶端請求異常都要處理一遍,對開發人員來說就顯得重複而繁瑣。

ABP框架完全自動化了程序中各方面的錯誤處理。大多數情況下,您無需在代碼中編寫任何try-catch語句,因爲它會執行以下操作:

  • 處理、記錄所有異常,並向客戶端返回標準格式的錯誤信息,或爲服務渲染提供標準錯誤頁面。
  • 隱藏內部結構性錯誤,同時支持返回用戶友好的本地化錯誤消息。
  • 支持標準異常,例如驗證和授權異常,並向客戶端發送正確的HTTP狀態碼。
  • 處理客戶端上的錯誤,並向用戶顯示有意義的消息。

當ABP異常系統支持向客戶端返回用戶友好的消息或特定錯誤代碼(業務)。

用戶友好異常 UserFriendlyException

ABP提供了一些預定義的異常類來定製錯誤處理行爲。其中之一是UserFriendlyException類。

首先,要了解UserFriendlyException使用場景,先要了解服務端API是什麼異常。以下是自定義異常範例:

Public async Task ExampleAsync() { throw new Exception("my error message..."); }

假設瀏覽器客戶端通過AJAX請求ExampleAsync方法。它將向用戶顯示以下錯誤消息:

如圖所示,ABP顯示了內部異常的標準消息,實際的錯誤消息會寫入日誌系統。對於此類一般性錯誤,服務器會向客戶端返回HTTP 500狀態代碼,因爲向用戶顯示原始異常消息是沒有用的,甚至可能是危險的,因爲它可能包含內部系統的一些敏感信息,例如數據庫表名和字段。

但是,對於某些特定情況,您可能希望向用戶返回一條用戶友好、信息豐富的自定義錯誤消息。對於這種情況,可以使用UserFriendlyException異常,如下代碼塊所示:

public async Task ExampleAsync() { throw new UserFriendlyException("This message is available to the user!"); }

此時,ABP不會隱藏錯誤消息:

UserFriendlyException不是唯一的,任何繼承自UserFriendlyException或實現IUserFriendlyException接口的異常類都可返回用戶友好的異常消息。

當您拋出用戶友好的異常時,ABP會向客戶端返回HTTP 403(禁止)狀態碼。(有關HTTP狀態碼映射,請參閱末尾的“控制HTTP狀態碼”部分)

[success] UserFriendlyException是一種特殊類型的業務異常,您可以直接向用戶返回消息。

業務異常 BusinessException

當請求的操作不滿足系統業務些規則時,需要拋出異常。ABP中的業務異常是ABP框架識別和處理的特殊異常類型。
在最簡單的情況下,可以直接使用BusinessException類拋出業務異常。請參見EventHub項目示例

public class EventRegistrationManager : DomainService 
{
    public async Task RegisterAsync(Event @event, AppUser user) 
    { 
        if (Clock.Now > @event.EndTime) 
        { 
           throw new BusinessException(EventHubErrorCodes.CantRegisterOrUnregisterForAPastEvent);
        }         
        ...     
    } 
}

EventRegistrationManager是一個領域服務,用於執行事件註冊的業務規則。RegisterAsync是檢查事件時間,如果是註冊到過去的事件則引發業務異常。

BusinessException的構造函數接受幾個參數,所有參數都是可選的:

  • code: 自定義錯誤碼。客戶端可以在處理異常時進行檢查、跟蹤錯誤類型。不同的異常,通常使用不同的錯誤碼。錯誤碼還支持本地化。
  • message: 異常消息
  • details: 詳細消息
  • innerException: 內部異常。如果緩存了一個業務異常,則可以傳遞到這裏。
  • logLevel: 異常日誌級別,它是LogLevel類型的枚舉,默認值是 LogLevel.Warning

1 本地化業務異常

如果使用UserFriendlyException,則必須自己對消息進行本地化,因爲異常消息將要顯示給用戶。
如果拋出BusinessException,ABP不會向用戶顯示異常消息,除非顯式地將其本地化。爲此,它使用了錯誤代碼名稱空間

假設您使用了EventHub:CantRegisterOrUnregisterForAPastEvent作爲錯誤代碼。這裏,EventHub通過使用冒號成爲錯誤代碼命名空間。我們必須將錯誤代碼名稱空間映射到本地化資源,這樣ABP就可以知道這些錯誤消息使用哪個本地化資源:

Configure(options => { options.MapCodeNamespace("EventHub",typeof(EventHubResource)); });

在這個代碼片段中,我們將EventHub錯誤代碼命名空間映射到EventHubResource本地化資源。現在,您可以在本地化文件(包括名稱空間)中將錯誤代碼定義爲key,如下所示:

{"culture": "en", "texts": { "EventHub:CantRegisterOrUnregisterForAPastEvent": "You can not register to or unregister from an event in the past, sorry!" } }

配置完成後,每當您拋出帶有該錯誤代碼的BusinessException異常時,ABP都會向用戶顯示本地化消息。

在某些情況下,您可能希望在錯誤消息中包含一些附加數據。請參閱以下代碼片段:

throw new BusinessException(EventHubErrorCodes.OrganizationNameAlreadyExists).WithData("Name", name);

在這裏,我們使用WithData擴展方法將組織名稱包含在錯誤消息中。然後,我們可以定義本地化字符串,如以下代碼段所示:

"EventHub:OrganizationNameAlreadyExists": "The organization {Name} already exists. Please use another name."

在本例中,{Name}是組織名稱的佔位符。ABP會自動將其替換爲給定的名稱。

我們已經看到了如何拋出BusinessException異常。如果要創建自定義異常類呢?

2 自定義業務異常類

還可以創建自定義異常類,而不是直接引發BusinessException異常。在這種情況下,您可以創建一個繼承自BusinessException的新類,如下代碼塊所示

public class OrganizationNameAlreadyExistsException : BusinessException 
{
    public string Name { get; private set; }
    public OrganizationNameAlreadyExistsException(string name) : base(EventHubErrorCodes.OrganizationNameAlreadyExists) 
    {
       Name = name; WithData("Name", name);    
    } 
}

在本例中,OrganizationNameAlreadyExistsException是一個自定義業務異常類。它在構造函數中使用組織的名稱。拋出這個異常非常簡單:

throw new OrganizationNameAlreadyExistsException(name);

這種用法比使用自定義數據引發BusinessException異常更簡單,因爲開發人員可能會忘記設置自定義數據。當您在多個位置拋出相同的異常時,它還可以減少代碼重複

異常日誌記錄

如異常處理開頭所述,ABP會自動記錄所有異常:業務異常、授權和驗證異常以警告級別(Warning級別),其他錯誤的警告級別默認是Error級別。
我們可以實現IHasLogLevel接口,爲異常類設置不同的日誌級別:

public class MyException : Exception, IHasLogLevel { 
    public LogLevel LogLevel { get; set; } = LogLevel.Warning;     
    //... 
}

MyException類實現了具有Warning級別的IHasLogLevel接口。如果拋出MyException異常,ABP支持寫入警告日誌。

還可以爲異常寫入其他日誌,您可以實現IExceptionWithSelfLogging接口來編寫其他日誌,如下所示:

public class MyException : Exception, IExceptionWithSelfLogging {
    public void Log(ILogger logger) {
        //...log additional info
    }
 }

HTTP狀態代碼

ABP盡最大努力爲已知的異常類型返回正確的HTTP狀態碼,如下所示:

  • 401 (unauthorized-未經授權) :用戶尚未登錄, 對應 AbpAuthorizationException
  • 403 (forbidden-禁止) :用戶已登錄, 對應AbpAuthorizationException
  • 400 (bad request-錯誤請求) 對應AbpValidationException
  • 404 (not found-未找到) 對應EntityNotFoundException
  • 403 (forbidden-禁止) 對應 UserFriendlyException/BusinessException
  • 501 (not implemented-未實現) 對應NotImplementedException
  • 500 (internal server error-服務器內部錯誤) 對應其他異常

如果要爲異常返回自定義一個HTTP狀態碼,可以將錯誤代碼映射到HTTP狀態代碼,如以下配置所示:

services.Configure(options => {options.Map(EventHubErrorCodes.OrganizationNameAlreadyExists,HttpStatusCode.Conflict); });

建議在解決方案的Web或HTTP API層中進行配置。

總結

在本章中,我們探討了業務應用中實現的橫切關注點,包括授權,驗證和異常處理。下一章將介紹一些ABP的基本功能,如自動審計日誌和數據過濾。

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