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.Shared
Domain
- 应用层
Application.Contracts
Application
- 持久化
EntityFrameworkCore
DbMigrator
- 远程服务层
HttpApi
HttpApi.Client
- 展示(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
项目引用
如果需要通过程序包管理器生成数据库或代码,需要引用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
中需要手动编写
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
:分页查询 -
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扩展方法
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>
提供基本的存储库方法,但它们不支持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
继承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.auth
JavaScript 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
来解决,我懒了