项目

模块实体扩展系统

模块实体扩展系统是一个高级扩展系统,允许您为依赖模块的现有实体定义新属性。它能够在单一位置自动将属性添加到实体、数据库、HTTP API和用户界面中。

模块必须基于模块实体扩展系统进行开发。所有官方模块在可能的情况下都支持此系统。

快速示例

打开解决方案中Domain.Shared项目内的YourProjectNameModuleExtensionConfigurator类,按如下方式修改ConfigureExtraProperties方法,为 身份模块IdentityUser实体添加一个SocialSecurityNumber属性:

public static void ConfigureExtraProperties()
{
    OneTimeRunner.Run(() =>
    {
        ObjectExtensionManager.Instance.Modules()
            .ConfigureIdentity(identity =>
            {
                identity.ConfigureUser(user =>
                {
                    user.AddOrUpdateProperty<string>( //属性类型:字符串
                        "SocialSecurityNumber", //属性名称
                        property =>
                        {
                            //验证规则
                            property.Attributes.Add(new RequiredAttribute());
                            property.Attributes.Add(
                                new StringLengthAttribute(64) {
                                    MinimumLength = 4
                                }
                            );

                            //...该属性的其他配置
                        }
                    );
                });
            });
    });
}

此方法在应用程序启动时于YourProjectNameDomainSharedModule中被调用。OneTimeRunner是一个工具类,确保这段代码在应用程序中仅执行一次,因为多次调用是不必要的。

  • ObjectExtensionManager.Instance.Modules()是配置模块的起点。ConfigureIdentity(...)方法用于配置身份模块的实体。
  • identity.ConfigureUser(...)用于配置身份模块的用户实体。并非所有实体都被设计为可扩展的(因为不需要)。使用智能感知来发现可扩展的模块和实体。
  • user.AddOrUpdateProperty<string>(...)用于向用户实体添加一个string类型的新属性(对于同一实体的同一属性,可以多次调用AddOrUpdateProperty方法。每次调用都可以配置同一属性的选项,但只有一个具有相同属性名称的属性会被添加到实体中)。您可以使用不同的属性名称调用此方法来添加更多属性。
  • SocialSecurityNumber是新属性的名称。
  • AddOrUpdateProperty接受第二个参数(property => lambda表达式)来为新属性配置附加选项。
    • 我们可以像这里展示的那样添加数据注解属性,就像向类属性添加数据注解属性一样。

创建与更新表单

一旦定义了一个属性,它就会出现在相关实体的创建和更新表单中:

用户表单添加新属性

SocialSecurityNumber字段出现在表单中。后续章节将解释这个新属性的本地化和验证。

数据表

新属性也会出现在相关页面的数据表中:

用户表单添加新属性

SocialSecurityNumber列出现在表格中。后续章节将解释如何从数据表中隐藏此列。

属性选项

在定义新属性时,您可以配置一些选项。

显示名称

您可能希望为在用户界面上显示的属性设置一个不同的(人类可读的)显示名称。

不需要本地化?

如果您的应用程序没有本地化,可以直接将属性的DisplayName设置为一个FixedLocalizableString对象。示例:

property =>
{
    property.DisplayName = new FixedLocalizableString("社会保障号");
}

本地化显示名称

如果您希望本地化显示名称,有两种选择。

按约定本地化

无需设置property.DisplayName,您可以直接打开您的本地化文件(如en.json)并在texts部分添加以下条目:

"SocialSecurityNumber": "社会保障号"

为您支持的每种语言在本地化文件中定义相同的SocialSecurityNumber键(您之前定义的属性名称)。这样就完成了!

在某些情况下,本地化键可能与本地化文件中的其他键冲突。在这种情况下,您可以在本地化文件中为显示名称使用DisplayName:前缀(此示例的本地化键为DisplayName:SocialSecurityNumber)。扩展系统首先查找带前缀的版本,然后回退到无前缀的名称(如果未本地化,则进一步回退到属性名称)。

推荐使用这种方法,因为它简单且适用于大多数场景。

使用DisplayName属性进行本地化

如果您想指定本地化键或本地化资源,仍然可以设置DisplayName选项:

property =>
{
    property.DisplayName =
        LocalizableString.Create<MyProjectNameResource>(
            "UserSocialSecurityNumberDisplayName"
        );
}
  • MyProjectNameResource是本地化资源,UserSocialSecurityNumberDisplayName是本地化资源中的本地化键。

如果您想了解更多关于本地化系统的信息,请参阅 本地化文档

默认值

新属性会自动设置一个默认值,这是属性类型的自然默认值,例如string类型为nullbool类型为falseint类型为0

有两种方法可以覆盖默认值:

DefaultValue选项

DefaultValue选项可以设置为任何值:

property =>
{
    property.DefaultValue = 42;
}
DefaultValueFactory选项

DefaultValueFactory可以设置为一个返回默认值的函数:

property =>
{
    property.DefaultValueFactory = () => DateTime.Now;
}

options.DefaultValueFactory的优先级高于options.DefaultValue

提示:仅当默认值可能随时间变化(如此示例中的DateTime.Now)时,才使用DefaultValueFactory选项。如果是常量值,则使用DefaultValue选项。

DataTypeAttribute

DataTypeAttribute用于指定属性的类型。它用于确定如何在用户界面上渲染属性:

property =>
{
    property.Attributes.Add(new DataTypeAttribute(DataType.Date));
}

验证

实体扩展系统允许您以几种方式为扩展属性定义验证。

数据注解属性

Attributes 是与该属性关联的属性列表。以下示例代码向属性添加了两个 数据注解验证属性

property =>
{
    property.Attributes.Add(new RequiredAttribute());
    property.Attributes.Add(new StringLengthAttribute(64) {MinimumLength = 4});
}

当您运行应用程序时,您会看到验证开箱即用:

用户表单添加新属性验证错误

由于我们添加了RequiredAttribute,它不允许留空。验证系统工作于:

  • 用户界面(自动本地化)。
  • HTTP API。即使您直接执行HTTP请求,也会收到带有适当HTTP状态码的验证错误。
  • 实体上的SetProperty(...)方法(如果您想知道SetProperty()方法是什么,请参阅 文档)。

因此,它自动实现了全栈验证。

有关基于属性的验证的更多信息,请参阅 ASP.NET Core MVC验证文档

默认验证属性

创建某些类型的属性时,会自动添加一些属性:

  • 对于非可空原始属性类型(例如intboolDateTime等)和enum类型,会自动添加RequiredAttribute。如果您希望允许空值,请使属性可空(例如int?)。
  • 对于枚举类型,会自动添加EnumDataTypeAttribute,以防止设置无效的枚举值。

如果不希望这些属性,请使用property.Attributes.Clear();

验证操作

验证操作允许您执行自定义代码来执行验证。以下示例检查SocialSecurityNumber是否以B开头,如果是,则添加验证错误:

property =>
{
    property.Attributes.Add(new RequiredAttribute());
    property.Attributes.Add(new StringLengthAttribute(64) {MinimumLength = 4});

    property.Validators.Add(context =>
    {
        if (((string) context.Value).StartsWith("B"))
        {
            context.ValidationErrors.Add(
                new ValidationResult(
                    "社会保障号不能以字母'B'开头,抱歉!",
                    new[] {"extraProperties.SocialSecurityNumber"}
                )
            );
        }
    });

}

在这种情况下,使用RegularExpressionAttribute可能更好,但这只是一个示例。无论如何,如果您输入一个以字母B开头的值,您会在保存表单时收到以下错误:

用户表单添加新属性自定义验证错误

上下文对象

context对象具有有用的属性,可在您的自定义验证操作中使用。例如,您可以使用context.ServiceProvider依赖注入系统 解析服务。以下示例获取本地化器并添加本地化的错误消息:

if (((string) context.Value).StartsWith("B"))
{
    var localizer = context.ServiceProvider
        .GetRequiredService<IStringLocalizer<MyProjectNameResource>>();

    context.ValidationErrors.Add(
        new ValidationResult(
            localizer["SocialSecurityNumberCanNotStartWithB"],
            new[] {"extraProperties.SocialSecurityNumber"}
        )
    );
}

context.ServiceProvider可为空!仅当您在对象上使用SetProperty(...)方法时,它才可能为null。因为此时DI系统不可用。虽然这种情况很少见,但当context.ServiceProvidernull时,您应执行回退逻辑。对于此示例,您将添加一个非本地化的错误消息。这不是问题,因为将无效值设置为属性通常是程序员的错误,并且在这种情况下大多不需要本地化。无论如何,即使在常规属性设置器中,您也无法使用本地化。但是,如果您对本地化很认真,可以抛出业务异常(请参阅 异常处理文档 以了解如何本地化业务异常)。

UI可见性

当您定义一个属性时,它会出现在相关UI页面的数据表、创建和编辑表单上。但是,您可以单独控制每一项。示例:

property =>
{
    property.UI.OnTable.IsVisible = false;
    //...其他配置
}

使用property.UI.OnCreateFormproperty.UI.OnEditForm也可以控制表单。如果一个属性是必需的,但未添加到创建表单中,您肯定会收到验证异常,因此请谨慎使用此选项。但是,如果这是您的要求,必需的属性可能不会出现在编辑表单中。

UI顺序

当您定义一个属性时,它会出现在相关UI页面的数据表、创建和编辑表单上。但是,您可以控制其顺序。示例:

property =>
{
    property.UI.Order = 1;
    //...其他配置
}

使用property.UI.OnCreateFormproperty.UI.OnEditForm也可以控制表单。如果一个属性是必需的,但未添加到创建表单中,您肯定会收到验证异常,因此请谨慎使用此选项。但是,如果这是您的要求,必需的属性可能不会出现在编辑表单中。

HTTP API可用性

即使您在UI上禁用一个属性,它仍然可以通过HTTP API可用。默认情况下,属性在所有API端点都可用。

使用property.Api选项可以使属性在某些API端点中不可用。

property =>
{
    property.Api.OnUpdate.IsAvailable = false;
}

在此示例中,更新HTTP API将不允许为此属性设置新值。在这种情况下,您还希望在编辑表单上禁用此属性:

property =>
{
    property.Api.OnUpdate.IsAvailable = false;
    property.UI.OnEditForm.IsVisible = false;
}

除了property.Api.OnUpdate,您还可以设置property.Api.OnCreateproperty.Api.OnGet以精细控制API端点。

特殊类型

枚举

模块扩展系统天然支持enum类型。

一个枚举类型示例:

public enum UserType
{
    Regular,
    Moderator,
    SuperUser
}

您可以像添加其他属性一样添加枚举属性:

user.AddOrUpdateProperty<UserType>("Type");

枚举属性在创建/编辑表单中显示为组合框(选择):

添加新属性枚举

本地化

默认情况下,枚举成员名称显示在表格和表单上。如果您希望本地化它,只需在 本地化 文件中创建一个新条目:

"Enum:UserType.0": "超级用户" 

以下名称之一可用作本地化键:

  • Enum:UserType.0
  • Enum:UserType.SuperUser
  • UserType.0
  • UserType.SuperUser
  • SuperUser

本地化系统按给定顺序搜索键。本地化文本用于表格和创建/编辑表单。

导航属性/外键

支持向实体添加扩展属性,该属性是另一个实体的Id(外键)。

示例:将部门与用户关联

ObjectExtensionManager.Instance.Modules()
    .ConfigureIdentity(identity =>
    {
        identity.ConfigureUser(user =>
        {
            user.AddOrUpdateProperty<Guid>(
                "DepartmentId",
                property =>
                {
                    property.UI.Lookup.Url = "/api/departments";
                    property.UI.Lookup.DisplayPropertyName = "name";
                }
            );
        });
    });

UI.Lookup.Url选项接受一个URL,以获取在编辑/创建表单上选择的部门列表。此端点可以是典型的控制器、自动API控制器 或任何返回适当JSON响应的端点类型。

返回固定部门列表的示例实现(实际中,您从数据源获取列表):

[Route("api/departments")]
public class DepartmentController : AbpController
{
    [HttpGet]
    public async Task<ListResultDto<DepartmentDto>> GetAsync()
    {
        return new ListResultDto<DepartmentDto>(
            new[]
            {
                new DepartmentDto
                {
                    Id = Guid.Parse("6267f0df-870f-4173-be44-d74b4b56d2bd"),
                    Name = "人力资源"
                },
                new DepartmentDto
                {
                    Id = Guid.Parse("21c7b61f-330c-489e-8b8c-80e0a78a5cc5"),
                    Name = "生产部"
                }
            }
        );
    }
}

此API返回如下JSON响应:

{
    "items": [{
        "id": "6267f0df-870f-4173-be44-d74b4b56d2bd",
        "name": "人力资源"
    }, {
        "id": "21c7b61f-330c-489e-8b8c-80e0a78a5cc5",
        "name": "生产部"
    }]
}

ABP现在可以显示一个自动完成选择组件,以便在创建或编辑用户时选择部门:

扩展导航属性表单

并在数据表上显示部门名称:

扩展导航属性表单

查找选项

UI.Lookup有以下选项来自定义如何读取从Url返回的响应:

  • Url:获取目标实体列表的端点。用于编辑和创建表单。
  • DisplayPropertyName:JSON响应中用于读取目标实体显示名称的属性,以在UI上显示。默认:text
  • ValuePropertyName:JSON响应中用于读取目标实体Id的属性。默认:id
  • FilterParamName:ABP允许在编辑/创建表单上搜索/过滤实体列表。如果目标列表包含大量项目,这尤其有用。在这种情况下,您可以返回一个有限的列表(例如,前100个),并允许用户在列表上搜索。ABP将过滤文本发送到服务器(作为简单的查询字符串),并使用此选项的名称。默认:filter
  • ResultListPropertyName:默认情况下,返回的JSON结果应在items数组中包含实体列表。您可以更改此字段的名称。默认:items

查找属性:显示名称如何工作?

您可能想知道ABP如何在上面的数据表上显示部门名称。

很容易理解如何填充编辑和创建表单的下拉列表:ABP向给定URL发出AJAX请求。每当用户键入以过滤项目时,它会重新请求。

但是,对于数据表,多个项目显示在UI上,并且为每一行执行单独的AJAX调用以获取部门的显示名称效率不高。

相反,外部实体的显示名称也作为实体的额外属性保存(请参阅 实体 文档的额外属性部分),除了外部实体的Id。如果您检查数据库,您可以在数据库表的ExtraProperties字段中看到DepartmentId_Text

{"DepartmentId":"21c7b61f-330c-489e-8b8c-80e0a78a5cc5","DepartmentId_Text":"生产部"}

因此,这是一种数据重复。如果您的目标实体的名称后来在数据库中更改,则没有自动同步系统。系统按预期工作,但您在数据表上看到的是旧名称。如果这对您来说是个问题,您应该在自己实体显示名称更改时注意更新此信息。

数据库映射

对于关系数据库,所有扩展属性值都存储在表的单个字段中:

用户数据库额外属性添加新属性

ExtraProperties字段将属性存储为JSON对象。虽然这在某些场景下没问题,但您可能希望为新属性创建一个专用字段。幸运的是,配置非常容易。

如果您使用Entity Framework Core数据库提供程序,可以按如下方式配置数据库映射:

ObjectExtensionManager.Instance
    .MapEfCoreProperty<IdentityUser, string>(
        "SocialSecurityNumber",
        (entityBuilder, propertyBuilder) =>
        {
            propertyBuilder.HasMaxLength(64);
        }
    );

在您的.EntityFrameworkCore项目中的YourProjectNameEfCoreEntityExtensionMappings类中编写此代码。然后您需要使用标准的Add-MigrationUpdate-Database命令创建新的数据库迁移并将更改应用到您的数据库。

Add-Migration创建一个新的迁移,如下所示:

public partial class Added_SocialSecurityNumber_To_IdentityUser : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.AddColumn<string>(
            name: "SocialSecurityNumber",
            table: "AbpUsers",
            maxLength: 128,
            nullable: true);
    }

    protected override void Down(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.DropColumn(
            name: "SocialSecurityNumber",
            table: "AbpUsers");
    }
}

一旦更新数据库,您将看到AbpUsers表将新属性作为标准表字段:

用户数据库字段添加新属性

如果您首先创建了一个没有数据库表字段的属性,然后后来需要将此属性移动到数据库表字段,建议在迁移中执行SQL命令将旧值复制到新字段。

但是,如果您不这样做,ABP会无缝管理它。它使用新的数据库字段,但如果为null,则回退到ExtraProperties字段。当您保存实体时,它将值移动到新字段。

有关更多信息,请参阅 扩展实体 文档。

更多

有关所有可扩展性选项的总体索引,请参阅 自定义模块 指南。

这里有一些您可以做的事情:

  • 您可以创建第二个实体,该实体映射到同一数据库表,并将额外属性作为标准类属性(如果您已定义EF Core映射)。对于上面的示例,您可以在应用程序中的AppUser实体中添加一个public string SocialSecurityNumber {get; set;}属性,因为AppUser实体映射到同一AbpUser表。仅在需要时执行此操作,因为它会给应用程序带来更多复杂性。
  • 您可以覆盖域或应用程序服务以使用新属性执行自定义逻辑。
  • 您可以在UI上低级控制如何在数据表中添加/渲染字段。

另请参阅

在本文档中