模块实体扩展系统
模块实体扩展系统是一个高级扩展系统,允许您为依赖模块的现有实体定义新属性。它能够在单一位置自动将属性添加到实体、数据库、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类型为null,bool类型为false,int类型为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验证文档。
默认验证属性
创建某些类型的属性时,会自动添加一些属性:
- 对于非可空原始属性类型(例如
int、bool、DateTime等)和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.ServiceProvider为null时,您应执行回退逻辑。对于此示例,您将添加一个非本地化的错误消息。这不是问题,因为将无效值设置为属性通常是程序员的错误,并且在这种情况下大多不需要本地化。无论如何,即使在常规属性设置器中,您也无法使用本地化。但是,如果您对本地化很认真,可以抛出业务异常(请参阅 异常处理文档 以了解如何本地化业务异常)。
UI可见性
当您定义一个属性时,它会出现在相关UI页面的数据表、创建和编辑表单上。但是,您可以单独控制每一项。示例:
property =>
{
property.UI.OnTable.IsVisible = false;
//...其他配置
}
使用property.UI.OnCreateForm和property.UI.OnEditForm也可以控制表单。如果一个属性是必需的,但未添加到创建表单中,您肯定会收到验证异常,因此请谨慎使用此选项。但是,如果这是您的要求,必需的属性可能不会出现在编辑表单中。
UI顺序
当您定义一个属性时,它会出现在相关UI页面的数据表、创建和编辑表单上。但是,您可以控制其顺序。示例:
property =>
{
property.UI.Order = 1;
//...其他配置
}
使用property.UI.OnCreateForm和property.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.OnCreate和property.Api.OnGet以精细控制API端点。
特殊类型
枚举
模块扩展系统天然支持enum类型。
一个枚举类型示例:
public enum UserType
{
Regular,
Moderator,
SuperUser
}
您可以像添加其他属性一样添加枚举属性:
user.AddOrUpdateProperty<UserType>("Type");
枚举属性在创建/编辑表单中显示为组合框(选择):
本地化
默认情况下,枚举成员名称显示在表格和表单上。如果您希望本地化它,只需在 本地化 文件中创建一个新条目:
"Enum:UserType.0": "超级用户"
以下名称之一可用作本地化键:
Enum:UserType.0Enum:UserType.SuperUserUserType.0UserType.SuperUserSuperUser
本地化系统按给定顺序搜索键。本地化文本用于表格和创建/编辑表单。
导航属性/外键
支持向实体添加扩展属性,该属性是另一个实体的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-Migration和Update-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上低级控制如何在数据表中添加/渲染字段。
抠丁客











