项目

自定义应用模块:覆盖服务

您可能需要为应用程序更改依赖模块的行为(业务逻辑)。在这种情况下,可以利用 依赖注入系统 的功能,用您自己的实现替换依赖模块的服务、控制器甚至页面模型。

替换服务适用于任何注册到依赖注入中的类类型,包括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 实现。

得益于命名约定( MyEfCoreIdentityUserRepositoryEfCoreIdentityUserRepository 结尾),无需额外设置。您可以重写任何基方法以满足需求。

但是,如果您注入 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无法通过命名约定暴露IMyIdentityUserRepositoryMyEfCoreIdentityUserRepository不以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,并希望在从IdentityUserAppServiceGetListAsync方法获取用户列表时包含此信息。

您可以使用 对象扩展系统 将属性添加到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部分,请参阅 重写用户界面 指南。

如何查找服务?

模块文档 包含了它们定义的主要服务列表。此外,您可以研究 它们的源代码 来探索所有服务。

在本文档中