自定义应用模块:覆盖服务
您可能需要为应用程序更改依赖模块的行为(业务逻辑)。在这种情况下,可以利用 依赖注入系统 的功能,用您自己的实现替换依赖模块的服务、控制器甚至页面模型。
替换服务适用于任何注册到依赖注入中的类类型,包括ABP的服务。
根据需求不同,您有多种选择,这些将在后续章节中详细说明。
请注意,某些服务方法可能不是虚方法,因此可能无法重写。我们在设计时已将所有方法设为虚方法。如果发现任何不可重写的方法,请在 GitHub 上 提交问题 或自行修改并发送拉取请求。
替换接口
如果给定服务定义了接口,例如IdentityUserAppService类实现了IIdentityUserAppService接口,您可以重新实现同一接口,并用您的类替换当前实现。示例:
public class MyIdentityUserAppService : IIdentityUserAppService, ITransientDependency
{
//...
}
MyIdentityUserAppService通过命名约定(因为两者都以IdentityUserAppService结尾)替换了IIdentityUserAppService。如果您的类名不匹配,需要手动暴露服务接口:
[ExposeServices(typeof(IIdentityUserAppService))]
public class TestAppService : IIdentityUserAppService, ITransientDependency
{
//...
}
依赖注入系统允许为同一接口注册多个服务。注入接口时,将使用最后注册的服务。显式替换服务是一种良好实践。
示例:
[Dependency(ReplaceServices = true)]
[ExposeServices(typeof(IIdentityUserAppService))]
public class TestAppService : IIdentityUserAppService, ITransientDependency
{
//...
}
这样,IIdentityUserAppService接口将只有一个实现,虽然在这种情况下结果不变。也可以通过代码替换服务:
context.Services.Replace(
ServiceDescriptor.Transient<IIdentityUserAppService, MyIdentityUserAppService>()
);
您可以在模块的ConfigureServices方法中编写此代码。
重写服务类
在大多数情况下,您可能只想更改服务当前实现的一个或几个方法。重新实现整个接口在这种情况下效率不高。更好的方法是继承原始类并重写所需方法。
示例:重写应用服务
[Dependency(ReplaceServices = true)]
[ExposeServices(typeof(IIdentityUserAppService), typeof(IdentityUserAppService), typeof(MyIdentityUserAppService))]
public class MyIdentityUserAppService : IdentityUserAppService
{
//...
public MyIdentityUserAppService(
IdentityUserManager userManager,
IIdentityUserRepository userRepository,
IGuidGenerator guidGenerator
) : base(
userManager,
userRepository,
guidGenerator)
{
}
public async override Task<IdentityUserDto> CreateAsync(IdentityUserCreateDto input)
{
if (input.PhoneNumber.IsNullOrWhiteSpace())
{
throw new AbpValidationException(
"新用户必须提供电话号码!",
new List<ValidationResult>
{
new ValidationResult(
"电话号码不能为空!",
new []{"PhoneNumber"}
)
}
); }
return await base.CreateAsync(input);
}
}
此类重写了IdentityUserAppService 应用服务 的CreateAsync方法,以检查电话号码。然后调用基方法继续执行底层业务逻辑。通过这种方式,您可以在基逻辑之前和之后执行额外的业务逻辑。
您也可以完全重写用户创建的整个业务逻辑,而不调用基方法。
示例:重写领域服务
[Dependency(ReplaceServices = true)]
[ExposeServices(typeof(IdentityUserManager))]
public class MyIdentityUserManager : IdentityUserManager
{
public MyIdentityUserManager(
IdentityUserStore store,
IIdentityRoleRepository roleRepository,
IIdentityUserRepository userRepository,
IOptions<IdentityOptions> optionsAccessor,
IPasswordHasher<IdentityUser> passwordHasher,
IEnumerable<IUserValidator<IdentityUser>> userValidators,
IEnumerable<IPasswordValidator<IdentityUser>> passwordValidators,
ILookupNormalizer keyNormalizer,
IdentityErrorDescriber errors,
IServiceProvider services,
ILogger<IdentityUserManager> logger,
ICancellationTokenProvider cancellationTokenProvider) :
base(store,
roleRepository,
userRepository,
optionsAccessor,
passwordHasher,
userValidators,
passwordValidators,
keyNormalizer,
errors,
services,
logger,
cancellationTokenProvider)
{
}
public async override Task<IdentityResult> CreateAsync(IdentityUser user)
{
if (user.PhoneNumber.IsNullOrWhiteSpace())
{
throw new AbpValidationException(
"新用户必须提供电话号码!",
new List<ValidationResult>
{
new ValidationResult(
"电话号码不能为空!",
new []{"PhoneNumber"}
)
}
);
}
return await base.CreateAsync(user);
}
}
此示例类继承自IdentityUserManager 领域服务,并重写CreateAsync方法以执行上述相同的电话号码检查。结果相同,但这次我们在领域服务中实现了它,假设这是我们系统的核心领域逻辑。
这里需要
[ExposeServices(typeof(IdentityUserManager))]属性,因为IdentityUserManager没有定义接口(如IIdentityUserManager),并且依赖注入系统默认不会为继承类暴露服务(不像实现的接口那样)。
查看 本地化系统 了解如何本地化错误消息。
示例:重写仓储
public class MyEfCoreIdentityUserRepository : EfCoreIdentityUserRepository
{
public MyEfCoreIdentityUserRepository(
IDbContextProvider<IIdentityDbContext> dbContextProvider)
: base(dbContextProvider)
{
}
/* 您可以在此重写任何基方法 */
}
在此示例中,我们重写了由 身份模块 定义的 EfCoreIdentityUserRepository 类。这是用户仓储的 Entity Framework Core 实现。
得益于命名约定( MyEfCoreIdentityUserRepository 以 EfCoreIdentityUserRepository 结尾),无需额外设置。您可以重写任何基方法以满足需求。
但是,如果您注入 IRepository<IdentityUser> 或 IRepository<IdentityUser, Guid> ,它仍将使用默认的仓储实现。要替换默认的仓储实现,请在模块类的ConfigureServices方法中编写以下代码:
context.Services.AddDefaultRepository(
typeof(Volo.Abp.Identity.IdentityUser),
typeof(MyEfCoreIdentityUserRepository),
replaceExisting: true
);
这样,当您注入IRepository<IdentityUser>、IRepository<IdentityUser, Guid>或IIdentityUserRepository时,将使用您的实现。
如果您想向仓储添加额外方法并在自己的代码中使用,可以定义一个接口并从您的仓储实现中暴露它。您还可以扩展预构建的仓储接口。示例:
public interface IMyIdentityUserRepository : IIdentityUserRepository
{
public Task DeleteByEmailAddress(string email);
}
IMyIdentityUserRepository接口扩展了身份模块的IIdentityUserRepository接口。然后您可以如下示例实现它:
[ExposeServices(typeof(IMyIdentityUserRepository), IncludeDefaults = true)]
public class MyEfCoreIdentityUserRepository
: EfCoreIdentityUserRepository, IMyIdentityUserRepository
{
public MyEfCoreIdentityUserRepository(
IDbContextProvider<IIdentityDbContext> dbContextProvider)
: base(dbContextProvider)
{
}
public async Task DeleteByEmailAddress(string email)
{
var dbContext = await GetDbContextAsync();
var user = await dbContext.Users.FirstOrDefaultAsync(u => u.Email == email);
if (user != null)
{
dbContext.Users.Remove(user);
}
}
}
MyEfCoreIdentityUserRepository类实现了IMyIdentityUserRepository接口。需要ExposeServices属性,因为ABP无法通过命名约定暴露IMyIdentityUserRepository(MyEfCoreIdentityUserRepository不以MyIdentityUserRepository结尾)。现在,您可以将IMyIdentityUserRepository接口注入到您的服务中,并调用其DeleteByEmailAddress方法。
示例:重写控制器
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Volo.Abp.Account;
using Volo.Abp.DependencyInjection;
namespace MyProject.Controllers
{
[Dependency(ReplaceServices = true)]
[ExposeServices(typeof(AccountController))]
public class MyAccountController : AccountController
{
public MyAccountController(IAccountAppService accountAppService)
: base(accountAppService)
{
}
public async override Task SendPasswordResetCodeAsync(
SendPasswordResetCodeDto input)
{
Logger.LogInformation("您的自定义逻辑...");
await base.SendPasswordResetCodeAsync(input);
}
}
}
此示例替换了AccountController(在 账户模块 中定义的API控制器),并重写了SendPasswordResetCodeAsync方法。
[ExposeServices(typeof(AccountController))]在此至关重要,因为它将此控制器注册到依赖注入系统中的AccountController。也推荐使用[Dependency(ReplaceServices = true)]来清除旧的注册(即使ASP.NET Core DI系统会选择最后注册的一个)。
此外,MyAccountController将从 ApplicationModel 中移除,因为它定义了ExposeServicesAttribute。
如果指定了IncludeSelf = true,即[ExposeServices(typeof(AccountController), IncludeSelf = true)],那么AccountController将被移除。这对于扩展控制器很有用。
如果您不想移除任一控制器,可以配置AbpAspNetCoreMvcOptions:
Configure<AbpAspNetCoreMvcOptions>(options =>
{
options.IgnoredControllersOnModelExclusion
.AddIfNotContains(typeof(MyAccountController));
});
重写其他类
重写控制器、框架服务、视图组件类以及任何其他注册到依赖注入中的类,都可以像上述示例一样进行。
扩展数据传输对象
扩展 实体 是可能的,如 扩展实体文档 所述。通过这种方式,您可以为实体添加自定义属性,并通过如上所述重写相关服务来执行额外的业务逻辑。
也可以扩展应用服务使用的数据传输对象(DTOs)。通过这种方式,您可以从UI(或客户端)获取额外属性,并从服务返回额外属性。
示例
假设您已按照 扩展实体文档 添加了SocialSecurityNumber,并希望在从IdentityUserAppService的GetListAsync方法获取用户列表时包含此信息。
您可以使用 对象扩展系统 将属性添加到IdentityUserDto。在应用程序启动模板中的YourProjectNameDtoExtensions类中编写此代码:
ObjectExtensionManager.Instance
.AddOrUpdateProperty<IdentityUserDto, string>(
"SocialSecurityNumber"
);
此代码为IdentityUserDto类定义了一个SocialSecurityNumber属性,类型为string。就这样。现在,如果您从REST API客户端调用/api/identity/users HTTP API(内部使用IdentityUserAppService),您将在extraProperties部分看到SocialSecurityNumber值。
{
"totalCount": 1,
"items": [{
"tenantId": null,
"userName": "admin",
"name": "admin",
"surname": null,
"email": "admin@abp.io",
"emailConfirmed": false,
"phoneNumber": null,
"phoneNumberConfirmed": false,
"twoFactorEnabled": false,
"lockoutEnabled": true,
"lockoutEnd": null,
"concurrencyStamp": "b4c371a0ab604de28af472fa79c3b70c",
"isDeleted": false,
"deleterId": null,
"deletionTime": null,
"lastModificationTime": "2020-04-09T21:25:47.0740706",
"lastModifierId": null,
"creationTime": "2020-04-09T21:25:46.8308744",
"creatorId": null,
"id": "8edecb8f-1894-a9b1-833b-39f4725db2a3",
"extraProperties": {
"SocialSecurityNumber": "123456789"
}
}]
}
目前手动向数据库添加了123456789值。
所有预构建模块都支持其DTO中的额外属性,因此您可以轻松配置。
定义检查
当您为实体 定义 额外属性时,它不会自动出现在所有相关的DTO中,这是出于安全考虑。额外属性可能包含敏感数据,您可能不希望默认将其暴露给客户端。
因此,如果您希望DTO可用该属性,需要显式为相应的DTO定义相同的属性(如上所述)。如果希望在用户创建时设置它,还需要为IdentityUserCreateDto定义它。
如果属性不太安全,这可能很繁琐。对象扩展系统允许您忽略对所需属性的定义检查。参见以下示例:
ObjectExtensionManager.Instance
.AddOrUpdateProperty<IdentityUser, string>(
"SocialSecurityNumber",
options =>
{
options.MapEfCore(b => b.HasMaxLength(32));
options.CheckPairDefinitionOnMapping = false;
}
);
这是为实体定义属性的另一种方法(ObjectExtensionManager有更多方法,参见 其文档 )。这次,我们将CheckPairDefinitionOnMapping设置为false,以在实体与DTO相互映射时跳过定义检查。
如果您不喜欢这种方法,但想更轻松地向多个对象(DTOs)添加单个属性,AddOrUpdateProperty可以获取类型数组来添加额外属性:
ObjectExtensionManager.Instance
.AddOrUpdateProperty<string>(
new[]
{
typeof(IdentityUserDto),
typeof(IdentityUserCreateDto),
typeof(IdentityUserUpdateDto)
},
"SocialSecurityNumber"
);
关于用户界面
此系统允许您向实体和DTOs添加额外属性并执行自定义业务代码,但它与用户界面无关。
有关UI部分,请参阅 重写用户界面 指南。
抠丁客


