Abp vNext 基本使用
Abp vNext 基本使用
本文将介绍Abp vNext的使用方法
使用.Net 6.0和Visual Studio 2022
Abp vNext 版本:5.3
目录
安装 abp cli
dotnet tool install -g Volo.Abp.Cli
安装之后,就可以使用abp命令创建项目了
创建项目
有两种方法,一种是去abp官网,生成项目,然后下载

第二种是通过abp命令创建项目,我个人更倾向于这种
- 首先,
cmd或powershell进入空文件夹 - 然后,输入命令
abp new 项目名称 - 如果要创建
Web Api项目,可以在创建项目时使用一些参数,abp new 项目名称 -t app --ui none,即,该项目没有使用UI框架,abp new 项目名称 -t app --ui none --separate-auth-server这个表示分离验证服务
运行项目
- 首先,打开生成的项目,将项目中
appsettings.json中有关数据库连接字符串的部分修改 - 然后,运行解决方案下的
Migration项目,生成数据库 - 之后,就可以正常运行解决方案下的
Web项目了
项目结构
- 领域层
Domain.SharedDomain
- 应用层
Application.ContractsApplication
- 持久化
EntityFrameworkCoreDbMigrator
- 远程服务层
HttpApiHttpApi.Client
- 展示(UI)层
Api.HostWebBlazor
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项目引用
如果需要通过程序包管理器生成数据库或代码,需要引用EntityFrameworkCore和Application.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项目
前端
MVC、Blazor、Vue、Angular等等,可能会包含ViewModel
命名空间
除了Domain.Shared是使用自己的命名空间,其它项目的命名空间都是一样的
模块化
解决方案下的每一个项目都有一个Module类,继承AbpModule,注意设置项目的默认命名空间和目标框架,项目框架可能需要设置成.NET Standard 2.0,因为Domain.Shared、Application.Contracts、HttpApi.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本地事件AggregateRoot:ExtraProperties扩展属性、ConcurrencyStamp并发同步标志CreationAuditedAggregateRoot:CreationTime创建时间、CreatorId创建者IdAuditedAggregateRoot:LastModificationTime最后修改时间、LastModifierId最后修改者IdFullAuditedAggregateRoot:IsDeleted是否删除、DeleterId删除者Id、DeletionTime删除时间
实体(Entity)
继承Entity<TKey>
带复合主键实体
继承Entity,overrideGetKeys函数
public override object[] GetKeys()
{
return new object[]{XId,XXId};
}
GUID主键
GUID vs 自增
- GUID优点:
- GUID全局唯一,适合分布式系统,方便拆分或合并表
- 无需数据库往返即可在客户端生成GUID
- GUID是无法猜测的,某些情况下它们可能更安全(例如,如果最终用户看到一个实体的Id,他们就找不到另一个实体的Id)
- GUID缺点:
- GUID占16个字节,int占4个字节,long占8个字节
- GUID本质上不是连续的,这会导致聚集索引出现性能问题
ABP提供IGuidGenerator默认生成顺序Guid值,解决了聚集索引的性能问题,建议用IGuidGenerator设置Id,如果你不设置Id,存储库默认会使用IGuidGenerator
使用存储库
存储库提供了一种标准方法来为实体执行常见的数据库操作
通用存储库
XXX指项目名称,TEntity指实体名称,TKey指主键类型,一般主键类型都是Guid
- 在
EntityFrameworkCore项目中创建类XXXDbContext,继承AbpDbContext<XXXDbContext>
在这个类中编写构造函数XXXDbContext(DbContextOptions<XXXDbContext> options)和overrideOnModelCreating函数,并在该函数中建立实体映射关系
类上方添加[ConnectionStringName("Default")],这个Default是在DbMigrator项目的appsettings.json中配置 - 在
EntityFrameworkCore项目中创建类XXXDbContextFactory实现IDesignTimeDbContextFactory<XXXDbContext>接口,这是一个工厂类,用于创建XXXDbContext,编写构造函数和BuildConfiguration,具体看创建项目时生成的代码 - 在
EntityFrameworkCore项目中创建类EntityFrameworkCoreabpdemoDbSchemaMigrator,实现IXXXDbSchemaMigrator和ITransientDependency接口,用于数据库迁移
IXXXDbSchemaMigrator在Domain.Data中创建,里面只有Task MigrateAsync()一个函数 - 在
Domain.Data中创建XXXDbMigrationService类,这是数据库迁移服务,具体看abp生成的代码 - 接下来就可以定义存储类,在
Domain项目的TEntitys目录下创建ITEntityRepository存储库接口,继承IRepository<TEntity, TKey>接口,在EntityFrameworkCore项目的TEntitys目录下创建EfCoreTEntityRepository类,实现EfCoreRepository<XXXDbContext, TEntity, TKey>和ITEntityRepository接口 - 在
Application.Contracts项目的TEntitys目录下创建ITEntityAppService接口,继承IApplicationService,在Application项目的TEntitys目录下创建TEntityAppService服务类,实现ITEntityAppService和XXXAppService接口,XXXAppService在Application项目下定义,在TEntityAppService中依赖注入ITEntityRepository就可以使用了
也可以在TEntityService中使用IRepository<聚合根,TKey>来注入
增删改查
ABP自带增删改查,这些函数在IRepository自带,在IService中需要手动编写
InsertAsyncInsertManyAsyncUpdateAsyncUpdateManyAsyncDeleteAsyncDeleteManyASync
所有存储库方法都是异步的,尽可能使用异步模式,因为在.NET中,将异步与同步混合潜在死锁、超时和可伸缩性问题,不容易检测
autoSave
await this._TEntityRepository.InsertAsync(new TEntity(),autoSave:true);
在EF Core中。使用更改跟踪系统,对数据库的操作需要SaveChanges才会保存更改,如果需要立即执行更改,则把autoSave设置为true
CancellationToken
所有存储库默认带有一个CancellationToken参数,在需要的时候用来取消数据库操作,比如关闭浏览器后,无需继续执行冗长的数据库查询操作。大部分情况下,我们无需手动传入cancellationToken,因为ABP框架会自动从HTTP请求中捕捉并使用取消令牌
查询单个实体
GetAsync:根据Id或表达式返回单个实体。如果未找到请求的实体,则抛出EntityNotFoundExceptionFindAsync:根据Id或表达式返回单个实体。如果未找到请求的实体,则返回null
FindAsync适用于有自定义逻辑,否则使用GetAsync
查询实体列表
-
GetListAsync:返回满足给定条件的所有实体或实体列表 -
GetPagedListAsync:分页查询 -
GetAsync和FindAsync方法带有默认值为true的includeDetails. -
GetListAsync和GetPagedListAsync方法带有默认值为false的includeDetails.
这意味着,默认情况下返回包含子对象的单个实体,而列表返回方法则默认不包括子对象信息.你可以明确通过 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扩展方法
AllAsyncAnyASyncAverageAsyncContainsAsyncCountAsyncFirstAsyncFirstOrDefaultAsyncLastAsyncLastOrDefaultAsyncLongCountAsyncMaxAsyncMinAsyncSingleAsyncSingleOrDefaultAsyncSumAsyncToArrayAsyncToListAsync
以上方法只对IRepository有效
复合主键查询
复合主键不能使用IRepository<TEntity,TKey>接口,因为它是获取单个PK(Id)类型,可以使用IRepository<TEntity>接口
其它存储库类型
IBasicRepository<TEntity,TPrimaryKey>和IBasicRepository<TEntity>提供基本的存储库方法,但它们不支持LINQ和IQueryable功能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>,CreateTEntityDto、GetTEntityListDto、UpdateTEntityDto不需要继承EntityDto<TKey>
映射在Application项目下的XXXApplicationAutoMapperProfile类下
public class XXXApplicationAutoMapperProfile : Profile
{
public XXXApplicationAutoMapperProfile()
{
CreateMap<TEntity, TEntityDto>();
CreateMap<CreateTEntityDto, TEntity>();
CreateMap<UpdateTEntityDto, TEntity>();
}
}
数据迁移
配置好后,利用Code First的自动迁移功能进行迁移,对比传统迁移好处:
- 高效快速
- 增量更新
- 版本管理
两种方法
EntityFramework Core框架自带的生成DbMigrator自带的生成
如果通过程序包管理器生成数据库或代码,记得解决DbMigrator项目依赖问题,看一看abp生成的DbMigrator代码
- 程序包源:
全部 - 默认项目:
EntityFrameworkCore
生成的迁移文件在EntityFrameworkCore项目中
如果使用DbMigrator项目生成数据库或代码,只需要在EntityFrameworkCore项目中,使用程序包管理器,执行Add-migration "XXX"生成迁移文件,然后执行DbMigrator项目就可以了
数据加载
使用场景:实体带有导航属性或带有其它实体的集合
- 显式加载
- EnsurePropertyLoadedAsync
- EnsureCollectionLoadedAsync
上面俩个函数使用存储库调用,参数为实体和实体的导航属性
- 延迟加载
首次访问才做加载,非默认启用,以下是启用流程
- 在
EntityFrameworkCore项目中安装Microsoft.EntityFrameworkCore.Proxies - 配置时使用
UseLazyLoadingProxies方法
Configure<AbpDbContextOptions>(options=>
{
options.PreConfigure<XXXAppDbContext>(opts=>
{
opts.DbContextOptions.UseLazyLoadingProxies();
});
options.UseSqlServere();
});
- 确保导航属性和集合属性在实体中是
virtual
延迟加载缺陷:
- 无法使用异步,
async/await 1+N性能问题
- 立即加载
顾名思义,首次查询立即加载相关数据
- 在
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>CreateTEntityDto、GetTEntityListDto、UpdateTEntityDto不需要继承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继承XXXAppServiceTEntityAppService实现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>();
}
下载,假设我对Controller或Action使用[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:如果方法名称以GetList、GetAll或Get开头Put:如果方法名称以Put或Update开头Delete:如果方法名称以Delete或Remove开头Post:如果方法名称以Create、Add、Insert或Post开头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的规范是请求数据用Get,Get请求也是支持Body传递数据的,但不是所有的前端请求框架都支持在Get里加入Body参数,比如axios,需要使用Post,Postman是支持在Get中添加body参数的,而且工作中我还真遇到过Get请求的url过长的问题,那是一个没有规范的项目
据说[FromQuery]和[FromUri]是可以传递复杂参数,但是我没成功过,前端参数的序列化也有些问题,也许可以自己写一个参数解析的Attribute来解决,我懒了

浙公网安备 33010602011771号