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

时间:2023-02-10 09:12:23

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

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

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

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),并希望为该角色授予产品权限。程序启动后,我们导航到管理|身份管理|角色页面。然后创建经理角色(如果之前没有创建),请单击权限操作按钮,如图所示

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

角色管理页面

单击权限按钮后将打开一个对话框,如下所示:
探索ABP基础架构的横切关注点

在图中,您可以在左侧看到权限组,而该组中的权限在右侧可用。权限组和我们定义的权限已经可以使用,无需进行任何额外操作。

具有经理角色的用户都继承该角色的权限。用户可以有多个角色,并且继承所有分配角色的所有权限的联合。您还可以在“用户管理”页面上直接向用户授予权限,以获得更大的灵活性。

我们已经定义了权限并将其分配给了角色。下一步是检查当前用户是否具有请求的权限。

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 子权限

在复杂的应用中,可能需要创建一些依赖于其父权限的子权限。当父权限被授予时,子权限才能正常工作。
探索ABP基础架构的横切关注点

角色管理权限具有一些子权限,如创建、编辑和删除。角色管理权限用于授权用户进入角色管理页面。如果用户无法进入该页面,那么授予角色创建权限就没有意义,因为不进入该页面几乎不可能创建新角色。

在权限定义类中,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基础架构的横切关注点

如图所示,ABP显示了内部异常的标准消息,实际的错误消息会写入日志系统。对于此类一般性错误,服务器会向客户端返回HTTP 500状态代码,因为向用户显示原始异常消息是没有用的,甚至可能是危险的,因为它可能包含内部系统的一些敏感信息,例如数据库表名和字段。

但是,对于某些特定情况,您可能希望向用户返回一条用户友好、信息丰富的自定义错误消息。对于这种情况,可以使用UserFriendlyException异常,如下代码块所示:

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

此时,ABP不会隐藏错误消息:
探索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的基本功能,如自动审计日志和数据过滤。