gpt4 book ai didi

探索ABP基础架构的横切关注点

转载 作者:我是一只小鸟 更新时间:2023-02-10 14:31:49 27 4
gpt4 key购买 nike

大家好,我是张飞洪,感谢您的阅读,我会不定期和你分享学习心得,希望我的文章能成为你成长路上的垫脚石,让我们一起精进.

授权、验证、异常处理和日志记录等横切关注点是每个系统的基本组成部分,它们对于确保系统的安全和良好运行至关重要.

实现横切关注点会导致应用中的很多地方出现重复代码。此外,一次授权或验证检查缺失可能会导致整个系统崩溃.

ABP框架的 主要目标之一 是使你的应用“不要重复自己”(DRY),ASP.NET Core已经为一些跨领域的问题提供了一个良好的基础设施,但ABP进一步实现了自动化,让使用更加容易.

本章探讨了ABP的基础设施:

  • 认证授权
  • 用户验证
  • 异常处理

认证和授权是安全中的两个主要概念。身份验证是识别当前用户的过程,授权用于允许或禁止用户执行应用的特定操作.

ASP.NET Core 系统本身提供了一种高级而灵活的认证和授权,ABP框架的认证授权与 ASP.NET Core 100%兼容,并进行了一定的扩展,它允许将权限授予角色和用户,它还允许在客户端进行权限检查.

简单授权检查

最简单的场景,只允许登录的用户执行特定操作。 [Authorize] 属性不带任何参数,只检查当前用户是否已通过身份验证(登录).

请参见以下控制器(MVC):

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

                        
                      

在本例中, CreateAsync 和 DeleteAsync 操作仅允许通过身份验证的用户使用,假设匿名用户(尚未登录的用户)尝试执行这些操作, 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响应。 IsGrantedAsync 和 CheckAsync 方法是ABP框架定义的有用的扩展方法.

[warning] 提示:从 AbpController 继承 。

建议从 AbpController 类而不是标准 Controller 类派生。因为它内部做了扩展,定义了一些有用的属性。比如,它有 AuthorizationService 属性(属于 IAuthorizationService 类型),您可以直接使用它,无需手动注入 IAuthorizationService 接口.

服务器上的权限检查是一种常见的方法。但是,您可能还需要检查客户端的权限.

4 客户端权限

ABP公开了一个标准HTTP API,其URL为 /api/abp/application-configuration ,返回包含本地化文本、设置、权限等的JSON数据。客户端可以使用该API来检查权限或在客户端执行本地化.

不同的客户端类型可能会提供不同的服务来检查权限。例如,在 MVC/Razor Pages 中,可以使用 abp.auth JavaScript 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> ,其中 T 是 ProductCreationRequirement 类型。在本例中,我只是检查了当前用户是否拥有 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 注册为单例服务.

现在,假设我对 Controller 或 Action 使用 [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 有一个自定义规则:如果 IsDraft 为 false ,并且图片路径为控,则提示需要上传图片。 如果需要从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 。我们通过指定验证错误来抛出 AbpValidationException 。 ValidationResult 表示验证错误;它的第一个构造函数参数是验证错误消息,第二个参数(可选)是DTO属性的名称.

一旦您或ABP验证系统抛出 AbpValidationException 异常,ABP异常处理系统将捕获并处理它.

禁用验证

可以使用 [DisableValidation] 在方法或类级别绕过ABP验证系统,如下例所示:

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

                        
                      

在本例中, CreateAsync 方法用 [DisableValidation] 修饰,因此ABP不会对输入对象执行任何自动验证。 如果对类使用 [DisableValidation] ,则该类的所有方法的验证都将被禁用。在这种情况下,可以对某个方法使用 [EnableValidation] ,以便仅对该特定方法启用验证.

当禁用方法的自动验证时,仍然可以执行自定义验证逻辑并抛出 AbpValidationException ,如前一节所述.

其他类型的验证

除了对 Controller Actions 和 Razor 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的基本功能,如自动审计日志和数据过滤.

最后此篇关于探索ABP基础架构的横切关注点的文章就讲到这里了,如果你想了解更多关于探索ABP基础架构的横切关注点的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。

27 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com