项目

对象扩展

ABP提供了一套对象扩展系统,允许您在不修改相关类的情况下为现有对象添加额外属性。这使得您可以扩展由依赖应用模块实现的功能,特别是当您需要扩展实体和模块定义的DTO时。

对于您自己的对象通常不需要使用对象扩展系统,因为您可以轻松地为自己定义的类添加常规属性。

IHasExtraProperties 接口

这是使类可扩展的接口。它简单地定义了一个Dictionary属性:

ExtraPropertyDictionary ExtraProperties { get; }

ExtraPropertyDictionary类继承自Dictionary<string, object>类。您可以使用此字典添加或获取扩展属性。

基类

IHasExtraProperties接口默认由多个基类实现:

  • AggregateRoot类实现(参见实体
  • ExtensibleEntityDtoExtensibleAuditedEntityDto等基础DTO类实现
  • ExtensibleObject实现,这是一个简单基类,可被任何类型的对象继承

因此,如果您继承这些类,您的类也将是可扩展的。如果没有,您可以随时手动实现它。

基础扩展方法

虽然您可以直接使用类的ExtraProperties属性,但建议在处理扩展属性时使用以下扩展方法。

SetProperty

用于设置扩展属性的值:

user.SetProperty("Title", "我的标题");
user.SetProperty("IsSuperUser", true);

SetProperty返回同一对象,因此您可以链式调用:

user.SetProperty("Title", "我的标题")
    .SetProperty("IsSuperUser", true);

GetProperty

用于读取扩展属性的值:

var title = user.GetProperty<string>("Title");

if (user.GetProperty<bool>("IsSuperUser"))
{
    //...
}
  • GetProperty是一个泛型方法,将对象类型作为泛型参数
  • 如果之前未设置给定属性,则返回默认值(int的默认值为0bool的默认值为false等)
非原始属性类型

如果属性类型不是原始类型(int、bool、enum、string等),则需要使用非泛型版本的GetProperty,它返回一个object

HasProperty

用于检查对象之前是否设置了某个属性。

RemoveProperty

用于从对象中移除属性。使用此方法而不是为属性设置null值。

一些最佳实践

对属性名称使用魔术字符串是危险的,因为您很容易输错属性名称——这不是类型安全的。相反:

  • 为扩展属性名称定义一个常量
  • 创建扩展方法以轻松设置扩展属性

示例:

public static class IdentityUserExtensions
{
    private const string TitlePropertyName = "Title";

    public static void SetTitle(this IdentityUser user, string title)
    {
        user.SetProperty(TitlePropertyName, title);
    }

    public static string GetTitle(this IdentityUser user)
    {
        return user.GetProperty<string>(TitlePropertyName);
    }
}

然后您可以轻松设置或获取Title属性:

user.SetTitle("我的标题");
var title = user.GetTitle();

对象扩展管理器

虽然您可以为可扩展对象(实现IHasExtraProperties接口的对象)设置任意属性,但ObjectExtensionManager用于显式定义可扩展类的扩展属性。

显式定义扩展属性有一些用例:

  • 允许控制对象到对象映射时如何处理扩展属性(参见下文)
  • 允许为属性定义元数据。例如,在使用EF Core时,您可以将扩展属性映射到数据库表字段

ObjectExtensionManager实现了单例模式(ObjectExtensionManager.Instance),您应在应用程序启动前定义对象扩展。应用程序启动模板提供了一些预定义的静态类,用于安全地在内部定义对象扩展。

AddOrUpdate

AddOrUpdate是定义或更新对象扩展属性的主要方法。

示例:为IdentityUser实体定义扩展属性:

ObjectExtensionManager.Instance
    .AddOrUpdate<IdentityUser>(options =>
        {
            options.AddOrUpdateProperty<string>("SocialSecurityNumber");
            options.AddOrUpdateProperty<bool>("IsSuperUser");
        }
    );

AddOrUpdateProperty

虽然可以在options上使用AddOrUpdateProperty,但如果您想定义单个扩展属性,也可以使用快捷扩展方法:

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

有时将单个扩展属性定义到多个类型会很实用。您可以逐个定义,也可以使用以下代码:

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

属性配置

AddOrUpdateProperty还可以获取一个操作,用于对属性定义执行额外配置:

ObjectExtensionManager.Instance
    .AddOrUpdateProperty<IdentityUser, string>(
        "SocialSecurityNumber",
        options =>
        {
            //配置选项...
        });

options有一个名为Configuration的字典,使对象扩展定义更具可扩展性。EF Core使用它将扩展属性映射到数据库表字段。请参阅扩展实体文档。

以下部分解释了基本的属性配置选项。

默认值

新属性会自动设置默认值,这是属性类型的自然默认值,如stringnullboolfalseint0

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

DefaultValue 选项

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

ObjectExtensionManager.Instance
    .AddOrUpdateProperty<IdentityUser, int>(
        "MyIntProperty",
        options =>
        {
            options.DefaultValue = 42;
        });
DefaultValueFactory 选项

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

ObjectExtensionManager.Instance
    .AddOrUpdateProperty<IdentityUser, DateTime>(
        "MyDateTimeProperty",
        options =>
        {
            options.DefaultValueFactory = () => DateTime.Now;
        });

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

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

CheckPairDefinitionOnMapping

控制在映射两个可扩展对象时如何检查属性定义。请参阅“对象到对象映射”部分以更好地理解CheckPairDefinitionOnMapping选项。

验证

您可能希望为已定义的扩展属性添加一些验证规则AddOrUpdateProperty方法选项允许两种验证方式:

  1. 可以为属性添加数据注解属性
  2. 可以编写一个操作(代码块)来执行自定义验证

当您在自动验证的方法(例如控制器操作、页面处理程序方法、应用服务方法等)中使用对象时,验证会生效。因此,每当扩展对象被验证时,所有扩展属性都会被验证。

数据注解属性

所有标准数据注解属性都适用于扩展属性。示例:

ObjectExtensionManager.Instance
    .AddOrUpdateProperty<IdentityUserCreateDto, string>(
        "SocialSecurityNumber",
        options =>
        {
            options.Attributes.Add(new RequiredAttribute());
            options.Attributes.Add(
                new StringLengthAttribute(32) {
                    MinimumLength = 6 
                }
            );
        });

通过此配置,如果没有提供有效的SocialSecurityNumber值,IdentityUserCreateDto对象将无效。

默认验证属性

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

  • 对于不可为空的原始属性类型(例如intboolDateTime等)和enum类型,会添加RequiredAttribute
  • 对于枚举类型,会添加EnumDataTypeAttribute以防止设置无效的枚举值

如果您不需要这些属性,请使用options.Attributes.Clear();

自定义验证

如果需要,可以添加一个自定义操作来验证扩展属性。示例:

ObjectExtensionManager.Instance
    .AddOrUpdateProperty<IdentityUserCreateDto, string>(
        "SocialSecurityNumber",
        options =>
        {
            options.Validators.Add(context =>
            {
                var socialSecurityNumber = context.Value as string;

                if (socialSecurityNumber == null ||
                    socialSecurityNumber.StartsWith("X"))
                {
                    context.ValidationErrors.Add(
                        new ValidationResult(
                            "无效的社会安全号码: " + socialSecurityNumber,
                            new[] { "SocialSecurityNumber" }
                        )
                    );
                }
            });
        });

context.ServiceProvider可用于解析服务依赖以处理高级场景。

除了为单个属性添加自定义验证逻辑外,您还可以添加在对象级别执行的自定义验证逻辑。示例:

ObjectExtensionManager.Instance
.AddOrUpdate<IdentityUserCreateDto>(objConfig =>
{
    //定义两个属性及其验证规则
    
    objConfig.AddOrUpdateProperty<string>("Password", propertyConfig =>
    {
        propertyConfig.Attributes.Add(new RequiredAttribute());
    });

    objConfig.AddOrUpdateProperty<string>("PasswordRepeat", propertyConfig =>
    {
        propertyConfig.Attributes.Add(new RequiredAttribute());
    });

    //编写适用于多个属性的通用验证逻辑
    
    objConfig.Validators.Add(context =>
    {
        if (context.ValidatingObject.GetProperty<string>("Password") !=
            context.ValidatingObject.GetProperty<string>("PasswordRepeat"))
        {
            context.ValidationErrors.Add(
                new ValidationResult(
                    "请重复相同的密码!",
                    new[] { "Password", "PasswordRepeat" }
                )
            );
        }
    });
});

对象到对象映射

假设您已向可扩展实体对象添加了扩展属性,并使用自动对象到对象映射将此实体映射到可扩展DTO类。在这种情况下您需要小心,因为扩展属性可能包含不应向客户端提供的敏感数据

本节提供了一些良好实践来控制对象映射时的扩展属性。

MapExtraPropertiesTo

MapExtraPropertiesTo是ABP提供的一个扩展方法,用于以受控方式将扩展属性从一个对象复制到另一个对象。示例用法:

identityUser.MapExtraPropertiesTo(identityUserDto);

MapExtraPropertiesTo需要在双方(此示例中为IdentityUserIdentityUserDto定义属性(如上所述)才能将值复制到目标对象。否则,即使源对象(此示例中的identityUser)中存在该值,也不会复制。有一些方法可以重写此限制。

MappingPropertyDefinitionChecks

MapExtraPropertiesTo获取一个额外参数来控制单个映射操作的定义检查:

identityUser.MapExtraPropertiesTo(
    identityUserDto,
    MappingPropertyDefinitionChecks.None
);

注意,MappingPropertyDefinitionChecks.None会在没有任何检查的情况下复制所有扩展属性。MappingPropertyDefinitionChecks枚举还有其他成员。

如果希望完全禁用属性的定义检查,可以在定义扩展属性(或更新现有定义)时执行此操作,如下所示:

ObjectExtensionManager.Instance
    .AddOrUpdateProperty<IdentityUser, string>(
        "SocialSecurityNumber",
        options =>
        {
            options.CheckPairDefinitionOnMapping = false;
        });

忽略属性

您可能希望在特定映射操作中忽略某些属性:

identityUser.MapExtraPropertiesTo(
    identityUserDto,
    ignoredProperties: new[] {"MySensitiveProp"}
);

忽略的属性不会复制到目标对象。

AutoMapper 集成

如果您使用AutoMapper库,ABP还提供了一个扩展方法来利用上述MapExtraPropertiesTo方法。

您可以在映射配置文件中使用MapExtraProperties()方法。

public class MyProfile : Profile
{
    public MyProfile()
    {
        CreateMap<IdentityUser, IdentityUserDto>()
            .MapExtraProperties();
    }
}

它具有与MapExtraPropertiesTo方法相同的参数。

Mapperly 集成

如果您使用Mapperly库,ABP还提供了一个扩展方法来利用上述MapExtraPropertiesTo方法。

您可以使用MapExtraProperties属性到Mapperly类:

[Mapper]
[MapExtraProperties]
public partial class IdentityUserToProfileDtoMapper : MapperBase<IdentityUser, IdentityUserDto>
{
    public override partial IdentityUserDto Map(IdentityUser source);
    public override partial void Map(IdentityUser source, IdentityUserDto destination);
}

Entity Framework Core 数据库映射

如果您使用EF Core,可以将扩展属性映射到数据库表字段。示例:

ObjectExtensionManager.Instance
    .AddOrUpdateProperty<IdentityUser, string>(
        "SocialSecurityNumber",
        options =>
        {
            options.MapEfCore(b => b.HasMaxLength(32));
        }
    );

有关更多信息,请参阅Entity Framework Core 集成文档

另请参阅

在本文档中