版本
语言

自定义应用模块: 重写服务

你可能想要更改依赖模块的行为(业务逻辑). 在这种情况下,你可以使用依赖注入的能力替换服务,控制器甚至页面模型到你自己的实现.

注册到依赖注入的任何类,包括ABP框架的服务都可以被替换.

你可以根据自己的需求使用不同的选项,下面的章节中将介绍这些选项.

请注意,某些服务方法可能不是virtual,你可能无法override,我们会通过设计将其virtual,如果你发现任何方法不可以被覆盖,请创建一个issue或者你直接修改后并发送pull request到GitHub.

替换接口

如果给定的服务定义了接口,像 IdentityUserAppService 类实现了 IIdentityUserAppService 接口,你可以为这个接口创建自己的实现并且替换当前的实现. 例如:

public class MyIdentityUserAppService : IIdentityUserAppService, ITransientDependency
{
    //...
}

MyIdentityUserAppService 通过命名约定替换了 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 方法编写替换服务代码.

重写一个服务类

大多数情况下,你会仅想改变服务当前实现的一个或几个方法. 重新实现完整的接口变的繁琐,更好的方法是继承原始类并重写方法.

示例: 重写服务方法

//[RemoteService(IsEnabled = false)] // 如果你在使用动态控制器,为了避免为应用服务创建重复的控制器, 你可以禁用远程访问.
[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 override async Task<IdentityUserDto> CreateAsync(IdentityUserCreateDto input)
    {
        if (input.PhoneNumber.IsNullOrWhiteSpace())
        {
            throw new AbpValidationException(
                "Phone number is required for new users!",
                new List<ValidationResult>
                {
                    new ValidationResult(
                        "Phone number can not be empty!",
                        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 override async Task<IdentityResult> CreateAsync(IdentityUser user)
    {
        if (user.PhoneNumber.IsNullOrWhiteSpace())
        {
            throw new AbpValidationException(
                "Phone number is required for new users!",
                new List<ValidationResult>
                {
                    new ValidationResult(
                        "Phone number can not be empty!",
                        new []{"PhoneNumber"}
                    )
                }
            );
        }

        return await base.CreateAsync(user);
    }
}

示例中类继承了 IdentityUserManager 领域服务,并且重写了 CreateAsync 方法进行了与之前相同的手机号码检查. 结果也是一样的,但是这次我们在领域服务实现了它,假设这是我们系统的核心领域逻辑.

这里需要 [ExposeServices(typeof(IdentityUserManager))] attribute,因为 IdentityUserManager 没有定义接口 (像 IIdentityUserManager) ,依赖注入系统并不会按照约定公开继承类的服务(如已实现的接口).

参阅本地化系统了解如何自定义错误消息.

重写其他服务

控制器,框架服务,视图组件类以及其他类型注册到依赖注入的类都可以像上面的示例那样被重写.

扩展数据传输对象

你可以如扩展实体文档所述扩展实体. 并使用上面介绍的重写相关服务使用自定义属性****执行其他业务逻辑.

应用程序使用的数据传输对象(DTO)同样可扩展. 这样你可以使服务返回其他属性并在UI(或其他客户端)得到其他属性.

示例

假设你已经按照扩展实体文档中的说明添加了 SocialSecurityNumber 并希望从 IdentityUserAppService的GetListAsync 方法获取用户列表时包括此属性.

你可以使用对象扩展系统将属性添加到 IdentityUserDto. 在应用程序启动模板带有的 YourProjectNameDtoExtensions 类中编写以下代码:

ObjectExtensionManager.Instance
    .AddOrUpdateProperty<IdentityUserDto, string>(
        "SocialSecurityNumber"
    );

这段代码为 IdentityUserDto 类添加了 string 类型的 SocialSecurityNumber 属性. 现在你可以在RREST 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时会跳过定义检查.

如果你不喜欢这种方法,但想简单的向多个对象(DTO)添加单个属, AddOrUpdateProperty 可以使用类型数组添加额外的属性:

ObjectExtensionManager.Instance
    .AddOrUpdateProperty<string>(
        new[]
        {
            typeof(IdentityUserDto), 
            typeof(IdentityUserCreateDto), 
            typeof(IdentityUserUpdateDto)
        },
        "SocialSecurityNumber"
    );

关于用户界面

该系统允许你向实体和DTO添加额外的属性并执行自定义业务代码,但它与用户界面无关.

参阅 重写用户界面 指南了解关于UI部分.

如何找到服务?

模块文档 包含了定义的主要服务列表. 另外 你也可以查看源码找到所有的服务.

本页对您有帮助吗?
请进行选择。
感谢您的宝贵意见!

请注意,虽然我们无法回复反馈意见,但我们的团队会根据您的意见改进体验。

在本文档中
Mastering ABP Framework Book
掌握 ABP 框架

本书将帮助你全面了解框架和现代Web应用程序开发技术。