Abp vNext 基本使用

Abp vNext 基本使用

本文将介绍Abp vNext的使用方法
使用.Net 6.0Visual Studio 2022
Abp vNext 版本:5.3

官方文档:https://docs.abp.io/zh-Hans/abp/latest/

目录

Abp vNext 基本使用
Abp vNext 增删改查
Abp vNext Token

安装 abp cli

dotnet tool install -g Volo.Abp.Cli

安装之后,就可以使用abp命令创建项目了

创建项目

有两种方法,一种是去abp官网,生成项目,然后下载

第二种是通过abp命令创建项目,我个人更倾向于这种

  • 首先,cmdpowershell进入空文件夹
  • 然后,输入命令abp new 项目名称
  • 如果要创建Web Api项目,可以在创建项目时使用一些参数,abp new 项目名称 -t app --ui none,即,该项目没有使用UI框架,abp new 项目名称 -t app --ui none --separate-auth-server这个表示分离验证服务

运行项目

  • 首先,打开生成的项目,将项目中appsettings.json中有关数据库连接字符串的部分修改
  • 然后,运行解决方案下的Migration项目,生成数据库
  • 之后,就可以正常运行解决方案下的Web项目了

项目结构

  1. 领域层
  • Domain.Shared
  • Domain
  1. 应用层
  • Application.Contracts
  • Application
  1. 持久化
  • EntityFrameworkCore
  • DbMigrator
  1. 远程服务层
  • HttpApi
  • HttpApi.Client
  1. 展示(UI)层
  • Api.Host
  • Web
  • Blazor

DDD

ABP的目标是以简洁代码为指导原则,构建一个易维护的解决方案模型。它专注于DDD技术的实现,并提供一个分层启动模板。

下图展示了模块的层和项目的依赖关系:

Domain.Shared

DDD中最核心的部分
Domain.Shared项目包含常量,枚举和其他对象,这些对象实际上是领域层的一部分,但是解决方案中所有的层/项目中都会使用到。

  • Domain.Shared不依赖解决方案中的其他项目,其他项目直接或间接依赖该项目。
  • NuGet安装:Volo.Abp.AuditLogging.Domain.Shared

Domain

Domain主要包含实体集合根领域服务值类型存储接口和解决方案的其他领域对象。

  • Domain项目依赖于Domain.Shared项目,因为项目中会用到它的一些常量,枚举和定义其他对象。
  • NuGet安装:Volo.Abp.Ddd.Domain

Application.Contracts

Application.Contracts项目主要包含IService应用服务接口和应用层的数据传输对象 (DTO)。它用于分离应用层的接口和实现。这种方式可以将接口项目做为约定包共享给客户端。

  • Application.Contracts项目依赖于Domain.Shared项目,因为它可能会在IService应用接口和DTO中使用常量、枚举和其他的共享对象。
  • NuGet安装:Volo.Abp.Ddd.Application.Contracts

Application

Application项目包含Application.Contracts项目的IService应用服务接口实现.

  • Application项目依赖于Application.Contracts项目,因为它需要实现IService接口与使用DTO
  • Application项目依赖于Domain项目,因为它需要使用领域对象(Entity实体,Repository存储接口等)执行应用程序逻辑。
  • NuGet安装:Volo.Abp.Ddd.Application

EntityFrameworkCore

EntityFrameworkCore项目集成EF Core项目,它定义了DbContext并实现Domain项目中定义的Repository存储层。

  • EntityFrameworkCore项目依赖于Domain,因为它需要引用Entity实体和Repository存储接口。
  • NuGet安装:Volo.Abp.EntityFrameworkCore.XXX

DbMigrator

配置连接字符串,给EntityFrameworkCore项目引用
如果需要通过程序包管理器生成数据库或代码,需要引用EntityFrameworkCoreApplication.Contracts项目

  • DbMigrator项目依赖于Application.Contracts项目
  • DbMigrator项目依赖于EntityFrameworkCore项目

HttpApi

远程服务层,即WebApi

  • HttpApi项目依赖于Application.Contracts项目

HttpApi.Client

远程服务代理层,客户端应用程序Blazor引用该项目,将直接通过依赖注入使用远程应用服务。

  • HttpApi.Client项目依赖于Application.Contracts项目

HttpApi.Host

WebApi项目的启动项,为swagger

  • HttpApi.Host项目依赖于Application项目
  • HttpApi.Host项目依赖于EntityFrameworkCore项目
  • HttpApi.Host项目依赖于HttpApi项目

前端

MVCBlazorVueAngular等等,可能会包含ViewModel

命名空间

除了Domain.Shared是使用自己的命名空间,其它项目的命名空间都是一样的

模块化

解决方案下的每一个项目都有一个Module类,继承AbpModule,注意设置项目的默认命名空间和目标框架,项目框架可能需要设置成.NET Standard 2.0,因为Domain.SharedApplication.ContractsHttpApi.Client的目标框架是.NET Standard 2.0
对应NuGetVolo.Abp.Core

ConfigureServices是将你的服务添加到依赖注入系统并配置其他模块的主要方法。例:

public class XXXModule : AbpModule
{
    public override void ConfigureServices(ServiceConfigurationContext context)
    {
        //...
    }
}

DependsOn

根据项目依赖,在各个项目的Module类中使用[DependsOn(typeof(XXXModule))],这玩意儿还是看Abp生成的项目吧

定义实体

定义在Domain项目中,以实体为名称的目录下

聚合根、实体、值对象的关系:https://blog.csdn.net/kiwangruikyo/article/details/115252155

聚合根(AggregateRoot)

继承聚合根<Guid>
聚合一般包含多个实体或者值对象,聚合根可以理解为根实体或者叫主实体。


按继承链

  • BasicAggregateRoot_distributedEvents分布式事件、_localEvents本地事件
  • AggregateRootExtraProperties扩展属性、ConcurrencyStamp并发同步标志
  • CreationAuditedAggregateRootCreationTime创建时间、CreatorId创建者Id
  • AuditedAggregateRootLastModificationTime最后修改时间、LastModifierId最后修改者Id
  • FullAuditedAggregateRootIsDeleted是否删除、DeleterId删除者Id、DeletionTime删除时间

实体(Entity)

继承Entity<TKey>

带复合主键实体

继承Entity,overrideGetKeys函数

public override object[] GetKeys()
{
    return new object[]{XId,XXId};
}

GUID主键

GUID vs 自增

  1. GUID优点:
  • GUID全局唯一,适合分布式系统,方便拆分或合并表
  • 无需数据库往返即可在客户端生成GUID
  • GUID是无法猜测的,某些情况下它们可能更安全(例如,如果最终用户看到一个实体的Id,他们就找不到另一个实体的Id)
  1. GUID缺点:
  • GUID占16个字节,int占4个字节,long占8个字节
  • GUID本质上不是连续的,这会导致聚集索引出现性能问题

ABP提供IGuidGenerator默认生成顺序Guid值,解决了聚集索引的性能问题,建议用IGuidGenerator设置Id,如果你不设置Id,存储库默认会使用IGuidGenerator

使用存储库

存储库提供了一种标准方法来为实体执行常见的数据库操作

通用存储库

XXX指项目名称,TEntity指实体名称,TKey指主键类型,一般主键类型都是Guid

  1. EntityFrameworkCore项目中创建类XXXDbContext,继承AbpDbContext<XXXDbContext>
    在这个类中编写构造函数XXXDbContext(DbContextOptions<XXXDbContext> options)和overrideOnModelCreating函数,并在该函数中建立实体映射关系
    类上方添加[ConnectionStringName("Default")],这个Default是在DbMigrator项目的appsettings.json中配置
  2. EntityFrameworkCore项目中创建类XXXDbContextFactory实现IDesignTimeDbContextFactory<XXXDbContext>接口,这是一个工厂类,用于创建XXXDbContext,编写构造函数和BuildConfiguration,具体看创建项目时生成的代码
  3. EntityFrameworkCore项目中创建类EntityFrameworkCoreabpdemoDbSchemaMigrator,实现IXXXDbSchemaMigratorITransientDependency接口,用于数据库迁移
    IXXXDbSchemaMigratorDomain.Data中创建,里面只有Task MigrateAsync()一个函数
  4. Domain.Data中创建XXXDbMigrationService类,这是数据库迁移服务,具体看abp生成的代码
  5. 接下来就可以定义存储类,在Domain项目的TEntitys目录下创建ITEntityRepository存储库接口,继承IRepository<TEntity, TKey>接口,在EntityFrameworkCore项目的TEntitys目录下创建EfCoreTEntityRepository类,实现EfCoreRepository<XXXDbContext, TEntity, TKey>ITEntityRepository接口
  6. Application.Contracts项目的TEntitys目录下创建ITEntityAppService接口,继承IApplicationService,在Application项目的TEntitys目录下创建TEntityAppService服务类,实现ITEntityAppServiceXXXAppService接口,XXXAppServiceApplication项目下定义,在TEntityAppService中依赖注入ITEntityRepository就可以使用了
    也可以在TEntityService中使用IRepository<聚合根,TKey>来注入

增删改查

ABP自带增删改查,这些函数在IRepository自带,在IService中需要手动编写

  • InsertAsync
  • InsertManyAsync
  • UpdateAsync
  • UpdateManyAsync
  • DeleteAsync
  • DeleteManyASync

所有存储库方法都是异步的,尽可能使用异步模式,因为在.NET中,将异步与同步混合潜在死锁、超时和可伸缩性问题,不容易检测

autoSave

await this._TEntityRepository.InsertAsync(new TEntity(),autoSave:true);

在EF Core中。使用更改跟踪系统,对数据库的操作需要SaveChanges才会保存更改,如果需要立即执行更改,则把autoSave设置为true

CancellationToken

所有存储库默认带有一个CancellationToken参数,在需要的时候用来取消数据库操作,比如关闭浏览器后,无需继续执行冗长的数据库查询操作。大部分情况下,我们无需手动传入cancellationToken,因为ABP框架会自动从HTTP请求中捕捉并使用取消令牌

查询单个实体

  • GetAsync:根据Id表达式返回单个实体。如果未找到请求的实体,则抛出EntityNotFoundException
  • FindAsync:根据Id表达式返回单个实体。如果未找到请求的实体,则返回null

FindAsync适用于有自定义逻辑,否则使用GetAsync

查询实体列表

  • GetListAsync:返回满足给定条件的所有实体或实体列表

  • GetPagedListAsync:分页查询

  • GetAsyncFindAsync方法带有默认值为trueincludeDetails.

  • GetListAsyncGetPagedListAsync方法带有默认值为falseincludeDetails.

这意味着,默认情况下返回包含子对象的单个实体,而列表返回方法则默认不包括子对象信息.你可以明确通过 includeDetails 来更改此行为.

LINQ高级查询

private readonly ITEntityRepository _TEntityRepository;
private readonly IAsyncQueryableExecuter _asyncExecuter;

public async Task<List<TEntity>> GetOrderedTEntityAsync(string name)
{
    //var queryable = await this._TEntityRepository.WithDetailsAsync(x=>x.Category);
    var queryable = await this._TEntityRepository.GetQueryableAsync();
    var query = from item in queryable
        where item.Name.Contains(name)
        orderby item.Name
        select item;

    return await this._asyncExecuter.ToListAsync(query);
}
  • 为什么不用return await query.ToListAsync()

ToListAsync 是由EF Core定义的扩展方法,位于Microsoft.EntityFrameworkCore包内,如果想保持应用层独立于ORM,ABP的IAsyncQueryableExecuter服务提供了必要的抽象

异步扩展方法

ABP框架为IRepository接口提供了所有标准异步LINQ扩展方法

  • AllAsync
  • AnyASync
  • AverageAsync
  • ContainsAsync
  • CountAsync
  • FirstAsync
  • FirstOrDefaultAsync
  • LastAsync
  • LastOrDefaultAsync
  • LongCountAsync
  • MaxAsync
  • MinAsync
  • SingleAsync
  • SingleOrDefaultAsync
  • SumAsync
  • ToArrayAsync
  • ToListAsync

以上方法只对IRepository有效

复合主键查询

复合主键不能使用IRepository<TEntity,TKey>接口,因为它是获取单个PK(Id)类型,可以使用IRepository<TEntity>接口

其它存储库类型

  • IBasicRepository<TEntity,TPrimaryKey>IBasicRepository<TEntity>提供基本的存储库方法,但它们不支持LINQIQueryable功能
  • IReadOnlyRepository<TEntity,TKey>IReadOnlyRepository<TEntity>IReadOnlyBasicRepository<TEntity,TKey>IReadOnlyBasicRepository<TEntity>提供获取数据的方法,但不包括任何操作方法

自定义存储库

public interface ITEntityRepository:IRepository<TEntity,TKey>
{
    Task<List<TEntity>> GetListAsync(string name,bool includeDrafts = false);
}
  • 定义在Domain项目中
  • 从通用存储库派生
  • 如果不想包含通用存储库的方法,也可以派生自IRepository无泛型参数接口,这是一个空接口

EF Core 集成

EntityFramework Core项目的Module中配置要使用的DbContext

public override void PreConfigureServices(ServiceConfigurationContext context)
{
    abpdemoEfCoreEntityExtensionMappings.Configure();
}

public override void ConfigureServices(ServiceConfigurationContext context)
{
    context.Services.AddAbpDbContext<XXXDbContext>(options =>
    {
        //添加默认存储库,包括所有实体都可以使用默认存储库
        //不使用includeAllEntities则只对聚合根使用默认存储库
        options.AddDefaultRepositories(includeAllEntities: true);
    });

    Configure<AbpDbContextOptions>(options =>
    {
        //配置数据库
        options.UseSqlServer();
    });
}

实体映射

有两种实体映射方法

  • 在实体类上使用数据注释属性
  • EntityFrameworkCore项目的XXXDbContext中overrideOnModelCreating函数配置映射,别忘了DbSet<TEntity>
protected override void OnModelCreating(ModelBuilder builder)
{
    //内置审计日志和数据过滤
    base.OnModelCreating(builder);

    builder.ConfigurePermissionManagement();
    builder.ConfigureSettingManagement();
    builder.ConfigureBackgroundJobs();
    builder.ConfigureAuditLogging();
    builder.ConfigureIdentity();
    builder.ConfigureIdentityServer();
    builder.ConfigureFeatureManagement();
    builder.ConfigureTenantManagement();

    /* Configure your own tables/entities inside here */

    //builder.Entity<YourEntity>(b =>
    //{
    //    b.ToTable(XXXConsts.DbTablePrefix + "YourEntities", XXXConsts.DbSchema);
    //    b.ConfigureByConvention(); //默认配置预定义的Entity或AggregateRoot,即约定,无需再额外配置继承的Entity或AggregateRoot,让代码整洁规范
    //    //...
    //});
}

DTO

DTO定义在Application.Contracts项目下的TEntitys目录下
TEntityDto继承EntityDto<TKey>CreateTEntityDtoGetTEntityListDtoUpdateTEntityDto不需要继承EntityDto<TKey>
映射在Application项目下的XXXApplicationAutoMapperProfile类下

    public class XXXApplicationAutoMapperProfile : Profile
    {
        public XXXApplicationAutoMapperProfile()
        {
            CreateMap<TEntity, TEntityDto>();  
            CreateMap<CreateTEntityDto, TEntity>();
            CreateMap<UpdateTEntityDto, TEntity>();
        }
    }

数据迁移

配置好后,利用Code First的自动迁移功能进行迁移,对比传统迁移好处:

  1. 高效快速
  2. 增量更新
  3. 版本管理

两种方法

  • EntityFramework Core框架自带的生成
  • DbMigrator自带的生成

如果通过程序包管理器生成数据库或代码,记得解决DbMigrator项目依赖问题,看一看abp生成的DbMigrator代码

  • 程序包源:全部
  • 默认项目:EntityFrameworkCore
    生成的迁移文件在EntityFrameworkCore项目中

如果使用DbMigrator项目生成数据库或代码,只需要在EntityFrameworkCore项目中,使用程序包管理器,执行Add-migration "XXX"生成迁移文件,然后执行DbMigrator项目就可以了

数据加载

使用场景:实体带有导航属性或带有其它实体的集合

  1. 显式加载
  • EnsurePropertyLoadedAsync
  • EnsureCollectionLoadedAsync

上面俩个函数使用存储库调用,参数为实体和实体的导航属性

  1. 延迟加载
    首次访问才做加载,非默认启用,以下是启用流程
  • EntityFrameworkCore项目中安装Microsoft.EntityFrameworkCore.Proxies
  • 配置时使用UseLazyLoadingProxies方法
Configure<AbpDbContextOptions>(options=>
{
    options.PreConfigure<XXXAppDbContext>(opts=>
    {
        opts.DbContextOptions.UseLazyLoadingProxies();
    });
    options.UseSqlServere();
});
  • 确保导航属性和集合属性在实体中是virtual

延迟加载缺陷:

  • 无法使用异步,async/await
  • 1+N性能问题
  1. 立即加载
    顾名思义,首次查询立即加载相关数据
  • EntityFrameworkCore项目中,在自定义存储库中使用EF Core API
public async Task<TEntity> GetWith(Guid id)
{
    var dbContext = await GetDbContextAsync();
    return await dbContext.TEntitys.
        Include(x => x.OtherEntityId)
        .SingleAsync(x => x.Id == id);
}
  • 不使用EF Core API,IRepository.WithDetailsAsync
public async Task<TEntity> GetWith(Guid id)
{
    var queryable = await this._TEntityRepository.WithDetailsAsync(x => x.OtherEntitys);
    var query = queryable.Where(x => x.Id == id);
    var entity = await this._asyncExecuter.FirstOrDefaultAsync(query);
    foreach(var item in entity.OtherEntitys)
    {
        //...
    }
}

项目结构总结

XXX为项目名称,TEntity为实体名称,TKey为实体主键类型

Entity

定义在Domain项目中,新建目录,以实体名称的复数TEntitys为目录名称,在该目录下定义实体类TEntity

  • TEntity继承Entity<TKey>AggregateRoot<TKey>

DTO

定义在Application.Contracts项目中,新建目录,以实体名称的复数TEntitys为目录名称,在该目录下定义实体DTO类TEntityDto

  • TEntityDto继承EntityDto<TKey>
  • CreateTEntityDtoGetTEntityListDtoUpdateTEntityDto不需要继承EntityDto<TKey>
  • GetTEntityListDto继承PagedAndSortedResultRequestDto
  • 映射在Application项目下的XXXApplicationAutoMapperProfile类下

IRepository

定义在Domain项目中,新建目录,以实体名称的复数TEntitys为目录名称,在该目录下定义实体存储库接口IEntityRepository

  • IEntityRepository继承IRepository<TEntity,TKey>
  • 不继承IRepository<TEntity,TKey>也可以在Service中通过IRepository<TEntity,TKey>依赖注入来使用

Repository

定义在EntityFrameworkCore项目中,新建目录,以实体名称的复数TEntitys为目录名称,在该目录下定义实体存储库实现类EfCoreTEntityRepository

  • EfCoreTEntityRepository继承EfCoreRepository<XXXDbContext, TEntity, TKey>
  • EfCoreTEntityRepository实现IEntityRepository

IService

定义在Application.Contracts项目中,新建目录,以实体名称的复数TEntitys为目录名称,在该目录下定义实体应用服务接口ITEntityAppService

  • ITEntityAppService继承IApplicationService接口,并定义GetAsync(TKey id)GetListAsync(GetTEntityListDto input)CreateAsync(CreateTEntityDto input)UpdateAsync(TKey id, UpdateTEntityDto input)DeleteAsync(TKey id)
public interface ITEntityAppService : IApplicationService
{
    Task<TEntityDto> GetAsync(Guid id);

    Task<PagedResultDto<TEntityDto>> GetListAsync(GetTEntityListDto input);

    Task<TEntityDto> CreateAsync(CreateTEntityDto input);

    Task<TEntityDto> UpdateAsync(Guid id, UpdateTEntityDto input);

    Task DeleteAsync(Guid id);

}
  • 如果不继承IApplicationService接口,可以继承ICrudAppService<TEntityDto,TKey,PagedAndSortedResultRequestDto,CreateUpdateTEntityDto>接口

Service

定义在Application项目中,新建目录,以实体名称的复数TEntitys为目录名称,在该目录下定义实体应用服务接口TEntityAppService

  • TEntityAppService继承XXXAppService
  • TEntityAppService实现ITEntityAppService
  • 通过依赖注入使用ITEntityRepository

认证授权

ABP的权限系统是为特定的用户或角色授予或禁止的策略,它与应用功能进行关联,并在用户尝试使用该功能时进行检查,通过当前用户已被授予权限,则可以使用该功能,否则,用户则无法使用该功能

定义权限

Application.Contracts项目下有一个Permissions目录,该目录下有一个XXXPermissionDefinitionProvider类,继承PermissionDefinitionProvider

public override void Define(IPermissionDefinitionContext context)
{
    //权限组
    var myGroup = context.AddGroup("权限组名称");
    //定义权限,例如:
    myGroup.AddPermission("权限组名称.Create");
    myGroup.AddPermission("权限组名称.Delete");
    //AddPermission("一级权限名称").AddChild("二级权限名称")可以添加子权限

}

权限名称尽量使用常量,定义在Domain.Shared项目中

  • 一级权限名称可以使用实体名称
  • 二级权限名称可以使用增删改查操作名称

Permissions目录下还有一个XXXPermissions类,这是一个静态类,可以在这里定义一些常量,例如:

public static class XXXPermissions
{
    public const string GroupName = "XXX";

    public static class TEntitys1
    {
        public const string Default = GroupName + ".TEntitys1";
        public const string Create = Default + ".Create";
        public const string Update = Default + ".Update";
        public const string Delete = Default + ".Delete";
    }

    public static class TEntitys2
    {
        public const string Default = GroupName + ".TEntitys1";
        public const string Create = Default + ".Create";
        public const string Update = Default + ".Update";
        public const string Delete = Default + ".Delete";
    }
}

之后就可以使用XXXPermissions引用来代替权限名称字符串

public class XXXPermissionDefinitionProvider : PermissionDefinitionProvider
{
    public override void Define(IPermissionDefinitionContext context)
    {
        var myGroup = context.AddGroup(XXXPermissions.GroupName);

        myGroup.AddPermission(XXXPermissions.TEntitys1.Default, L("实体1:查询"));
        myGroup.AddPermission(XXXPermissions.TEntitys1.Create, L("实体1:创建"));
        myGroup.AddPermission(XXXPermissions.TEntitys1.Update, L("实体1:修改"));
        myGroup.AddPermission(XXXPermissions.TEntitys1.Delete, L("实体1:删除"));
    }

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

权限本地化

官方文档:https://docs.abp.io/zh-Hans/abp/latest/Localization
本地化资源XXXResource,在Domain.Shared项目的Localization目录下,[LocalizationResourceName("本地化资源名称")]

public override void Define(IPermissionDefinitionContext context)
{
    var myGroup = context.AddGroup("权限组名称");
    myGroup.AddPermission("权限组名称.Create", L("本地化权限名称"));
    myGroup.AddPermission("权限组名称.Delete", L("本地化权限名称"));
}

private static LocalizableString L(string name)
{
    //name就是本地化key
    return LocalizableString.Create<XXXResource>(name);
}

在类中可以通过依赖注入来使用

public class MyService
{
    private readonly IStringLocalizer<XXXResource> _localizer;

    public MyService(IStringLocalizer<XXXResource> localizer)
    {
        _localizer = localizer;
    }

    public void Foo()
    {
        var str = _localizer["本地化Key"];
    }
}

ABP提供了JavaScript服务,可以在客户端使用相同的本地化文本

//获取本地化资源
var testResource = abp.localization.getResource('本地化资源名称');
//获取本地化字符串
var str = testResource('本地化Key');

也可以这样获取本地化字符串

var str = abp.localization.localize('本地化Key', '本地化资源名称');

权限的本地化Key应该就是权限名称

权限检查

可以使用[Authorize]属性以声明的方式检查权限,也可以使用IAuthorizationService以编程方式检查权限

[Authorize("权限名称")],权限名称作为字符串参数,也就是策略名称,在需要指定策略名称的任何位置使用权限名称

public class TEntityController : Controller
{
    public async Task<List<TEntityDto>> GetListAsync()
    {

    }

    [Authorize("XXX.TEntity.Create")]
    public async Task CreateAsync(CreateTEntityDto input)
    {

    }

    [Authorize("XXX.TEntity.Delete")]
    public async Task DeleteAsync(Guid id)
    {

    }
}

如果使用XXXPermissions类声明权限,则可以通过XXXPermissions引用来代替权限名称字符串

public class TEntityController : Controller
{
    public async Task<List<TEntityDto>> GetListAsync()
    {

    }

    [Authorize(XXXPermissions.TEntitys1.Create)]
    public async Task CreateAsync(CreateTEntityDto input)
    {

    }

    [Authorize(XXXPermissions.TEntitys1.Delete)]
    public async Task DeleteAsync(Guid id)
    {

    }
}

[Authorize("权限名称")]声明式授权易于使用,建议尽可能使用。但是,当你想要有条件地检查权限或执行未授权案例的逻辑时,它是有限的,对于这种情况,可以注入并使用IAuthorizationService,例如

public class TEntityController : Controller
{
    private readonly IAuthorizationService _authorizationService;

    public TEntityController(IAuthorizationService authorizationService)
    {
        this._authorizationService = authorizationService;
    }

    public async Task CreateAsync(CreateTEntityDto input)
    {
        if(await this._authorizationService.IsGrantedAsync("权限名称"))
        {
            //权限验证通过
        }
    }
}

IsGrantedAsync()方法检查给定的权限,如果当前用户(或用户的角色)已被授予权限,则返回true。如果你有自定义逻辑的权限要求,这将非常有用,但是,如果你只想检查权限,并对未经授权的情况抛出异常,CheckAsync()方法更实用
如果用户没有该操作权限,CheckAsync()方法会引发AbpAuthorizationException异常,该异常由ABP框架处理,并向客户端返回HTTP响应,IsGrantedAsync()CheckAsync()方法是ABP框架定义的有用的扩展方法

    public async Task CreateAsync(CreateTEntityDto input)
    {
        await this._authorizationService.CheckAsync("权限名称"))
        //权限验证通过
    }

建议TEntityController继承AbpController类,而不是标准Controller类,因为它内部做了扩展,定义了一些有用的属性,比如AuthorizationService属性(IAuthorization类型),可以直接使用,无需手动注入

客户端权限

服务器上的权限检查是一种常见的方法,但是,你可能还需要检查客户端的权限
ABP公开了一个标准的HTTP API,其URL为/api/abp/application-configuration,返回包含本地化文本、设置、权限等的JSON数据,客户端可以使用该API来检查权限或在客户端执行本地化

不同的客户端类型可能会提供不同的服务来检查权限,例如,在MVC/Razor Pages中,可以使用abp.authJavaScript API检查权限

abp.auth.isGranted("权限名称");

这是一个全局函数,如果当前用户具有给定的权限,则返回true,否则,返回false

Blazor应用程序中,可以重用系统的[Authorize]属性和IAuthorizationService

基于策略的授权

定义权限需求

public class CreateTEntityRequirement : IAuthorizationRequirement
{

}

CreateTEntityRequirement是一个空类,仅实现IAuthorizationRequirement接口,然后,为该需求定义一个授权处理程序CreateTEntityRequirementHandler

public class CreateTEntityRequirementHandler : AuthorizationHandler<CreateTEntityRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,CreateTEntityRequirementHandler requirement)
    {
        if(context.User.HasClaim(c => c.Type == "权限名称"))
        {
            context.Succeed(requirement);
        }
        return Task.CompletedTask;
    }
}

定义权限需求和处理程序后,需要在Module类的ConfigureServices方法中注册它们

public override void ConfigureServices(ServiceConfigurationContext context)
{
    Configure<AuthorizationOptions>(options => {
        options.AddPolicy("权限名称");
        policy => policy.Requirements.Add(new CreateTEntityRequirementHandler());
    });
    context.Services.AddSingleton<IAuthorizationHandler,CreateTEntityRequirementHandler>();
}

下载,假设我对ControllerAction使用[Authorize("权限名称")]属性,或者使用IAuthorizationService检查策略,我的自定义授权处理程序就可以进行逻辑处理了

基于资源的授权

基于资源的授权是一种允许你基于对象(如实体)控制策略的功能,例如,你可以控制删除特定实体的访问权限,而不是对所有产品拥有共同的删除权限。这个内容可以看ASP.NET Core[Authorize]属性

控制器之外的授权

ASP.NET Core可以在Razor页面、Razor组件和Web层中的一些地方使用[Authorize]IAuthorizationService
ABP框架则更进一步,允许对服务类和方法使用[Authorize]属性,而不依赖Web层,即使在非Web应用程序中也是如此。因此,这种用法完全有效

public class TEntityAppService : ApplicationService, ITEntityService
{
    [Authorize("权限名称")]
    public Task CreateAsync(CreateTEntityDto input)
    {

    }
}

Auto API

这个就是根据Service的方法命名自动创建Controller中的方法,并分配路由,手写的路由是不受影响的
这个配置在HttpApi.Host项目中,生成的路由默认以/api/app/TEntity开头

private void ConfigureConventionalControllers()
{
    Configure<AbpAspNetCoreMvcOptions>(options =>
    {
        options.ConventionalControllers.Create(typeof(UserCenterApplicationModule).Assembly);
    });
}

生成的路由

  • Get:如果方法名称以GetListGetAllGet开头
  • Put:如果方法名称以PutUpdate开头
  • Delete:如果方法名称以DeleteRemove开头
  • Post:如果方法名称以CreateAddInsertPost开头
  • Patch:如果方法名称以Patch开头
  • 其他情况,Post为默认方式

这些路由都是根据Service自动生成的,包括参数也是,如果你手写一个实体的Controller,在swagger中是可以看到手写的和生成的同时存在,参数也一致
比如

[ApiController]
[Route("/api/v1/[controller]")]
public class MenuController : AbpControllerBase
{
    private readonly IMenuAppService _menuAppService;

    public MenuController(IMenuAppService menuAppService)
    {
        this._menuAppService = menuAppService;
    }

    [HttpGet("GetOne/{id}")]
    public async Task<ActionResult<MenuDto>> GetOne([FromRoute] string id)
    {
        var menuDto = await this._menuAppService.GetAsync(Guid.Parse(id));

        return Ok(menuDto);
    }

    [HttpPost("GetList")]
    public async Task<ActionResult<PagedResultDto<MenuDto>>> GetList([FromBody] GetMenuListDto input)
    {
        var list = await this._menuAppService.GetListAsync(input);

        return Ok(list);
    }

    [HttpPost("Create")]
    public async Task<ActionResult<MenuDto>> Create([FromBody] CreateMenuDto input)
    {
        var menuDto = await this._menuAppService.CreateAsync(input);

        return Ok(menuDto);
    }

    [HttpPut("Update/{id}")]
    public async Task<ActionResult<MenuDto>> Update([FromRoute] string id, [FromBody] UpdateMenuDto input)
    {
        var menuDto = await this._menuAppService.UpdateAsync(Guid.Parse(id), input);

        return Ok(menuDto);
    }

    [HttpDelete("Delete/{id}")]
    public async Task<IActionResult> Delete([FromRoute] string id)
    {
        await this._menuAppService.DeleteAsync(Guid.Parse(id));

        return Ok();
    }

}

这是结果,符合预期

如果需要修改路由的规则的话,举例

private void ConfigureConventionalControllers()
{
    Configure<AbpAspNetCoreMvcOptions>(options =>
    {
        options.ConventionalControllers.Create(typeof(abpdemoApplicationModule).Assembly, options =>
         {
             options.RootPath = "demo/v1";
         });
    });
}

这样生成的路由就是/app/demo/v1/TEntity开头,具体可以去参考官方文档

至于为什么要手写控制器,因为有时需要传递复杂对象,比如分页、筛选、排序字段,虽然RESTful的规范是请求数据用GetGet请求也是支持Body传递数据的,但不是所有的前端请求框架都支持在Get里加入Body参数,比如axios,需要使用PostPostman是支持在Get中添加body参数的,而且工作中我还真遇到过Get请求的url过长的问题,那是一个没有规范的项目

据说[FromQuery][FromUri]是可以传递复杂参数,但是我没成功过,前端参数的序列化也有些问题,也许可以自己写一个参数解析的Attribute来解决,我懒了

Abp vNext 基本使用 结束

posted @ 2022-10-12 16:59  .NET好耶  阅读(3815)  评论(1编辑  收藏  举报