Object Extensions

ABP Framework provides an object extension system to allow you to add extra properties to an existing object without modifying the related class. This allows to extend functionalities implemented by a depended application module, especially when you want to extend entities and DTOs defined by the module.

Object extension system normally is not needed for your own objects since you can easily add regular properties to your own classes.

IHasExtraProperties Interface

This is the interface to make a class extensible. It simply defines a Dictionary property:

Dictionary<string, object> ExtraProperties { get; }

Then you can add or get extra properties using this dictionary.

Base Classes

IHasExtraProperties interface is implemented by several base classes by default:

  • Implemented by the AggregateRoot class (see entities).
  • Implemented by ExtensibleEntityDto, ExtensibleAuditedEntityDto... base DTO classes.
  • Implemented by the ExtensibleObject, which is a simple base class can be inherited for any type of object.

So, if you inherit from these classes, your class will also be extensible. If not, you can always implement it manually.

Fundamental Extension Methods

While you can directly use the ExtraProperties property of a class, it is suggested to use the following extension methods while working with the extra properties.

SetProperty

Used to set the value of an extra property:

user.SetProperty("Title", "My Title");
user.SetProperty("IsSuperUser", true);

SetProperty returns the same object, so you can chain it:

user.SetProperty("Title", "My Title")
    .SetProperty("IsSuperUser", true);

GetProperty

Used to read the value of an extra property:

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

if (user.GetProperty<bool>("IsSuperUser"))
{
    //...
}
  • GetProperty is a generic method and takes the object type as the generic parameter.
  • Returns the default value if given property was not set before (default value is 0 for int, false for bool... etc).
Non Primitive Property Types

If your property type is not a primitive (int, bool, enum, string... etc) type, then you need to use non-generic version of the GetProperty which returns an object.

HasProperty

Used to check if the object has a property set before.

RemoveProperty

Used to remove a property from the object. Use this methods instead of setting a null value for the property.

Some Best Practices

Using magic strings for the property names is dangerous since you can easily type the property name wrong - it is not type safe. Instead;

  • Define a constant for your extra property names
  • Create extension methods to easily set your extra properties.

Example:

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);
    }
}

Then you can easily set or get the Title property:

user.SetTitle("My Title");
var title = user.GetTitle();

Object Extension Manager

While you can set arbitrary properties to an extensible object (which implements the IHasExtraProperties interface), ObjectExtensionManager is used to explicitly define extra properties for extensible classes.

Explicitly defining an extra property has some use cases:

  • Allows to control how the extra property is handled on object to object mapping (see the section below).
  • Allows to define metadata for the property. For example, you can map an extra property to a table field in the database while using the EF Core.

ObjectExtensionManager implements the singleton pattern (ObjectExtensionManager.Instance) and you should define object extensions before your application startup. The application startup template has some pre-defined static classes to safely define object extensions inside.

AddOrUpdate

AddOrUpdate is the main method to define a extra properties or update extra properties for an object.

Example: Define extra properties for the IdentityUser entity:

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

AddOrUpdateProperty

While AddOrUpdateProperty can be used on the options as shown before, if you want to define a single extra property, you can use the shortcut extension method too:

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

Sometimes it would be practical to define a single extra property to multiple types. Instead of defining one by one, you can use the following code:

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

Property Configuration

AddOrUpdateProperty can also get an action that can perform additional configuration on the property definition:

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

options has a dictionary, named Configuration which makes the object extension definitions even extensible. It is used by the EF Core to map extra properties to table fields in the database. See the extending entities document.

The following sections explain the fundamental property configuration options.

Default Value

A default value is automatically set for the new property, which is the natural default value for the property type, like null for string, false for bool or 0 for int.

There are two ways to override the default value:

DefaultValue Option

DefaultValue option can be set to any value:

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

DefaultValueFactory can be set to a function that returns the default value:

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

options.DefaultValueFactory has a higher priority than the options.DefaultValue .

Tip: Use DefaultValueFactory option only if the default value may change over the time (like DateTime.Now in this example). If it is a constant value, then use the DefaultValue option.

CheckPairDefinitionOnMapping

Controls how to check property definitions while mapping two extensible objects. See the "Object to Object Mapping" section to understand the CheckPairDefinitionOnMapping option better.

Validation

You may want to add some validation rules for the extra properties you've defined. AddOrUpdateProperty method options allows two ways of performing validation:

  1. You can add data annotation attributes for a property.
  2. You can write an action (code block) to perform a custom validation.

Validation works when you use the object in a method that is automatically validated (e.g. controller actions, page handler methods, application service methods...). So, all extra properties are validated whenever the extended object is being validated.

Data Annotation Attributes

All of the standard data annotation attributes are valid for extra properties. Example:

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

With this configuration, IdentityUserCreateDto objects will be invalid without a valid SocialSecurityNumber value provided.

Default Validation Attributes

There are some attributes automatically added when you create certain type of properties;

  • RequiredAttribute is added for non nullable primitive property types (e.g. int, bool, DateTime...) and enum types.
  • EnumDataTypeAttribute is added for enum types, to prevent to set invalid enum values.

Use options.Attributes.Clear(); if you don't want these attributes.

Custom Validation

If you need, you can add a custom action that is executed to validate the extra properties. Example:

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(
                            "Invalid social security number: " + socialSecurityNumber,
                            new[] { "SocialSecurityNumber" }
                        )
                    );
                }
            });
        });

context.ServiceProvider can be used to resolve a service dependency for advanced scenarios.

In addition to add custom validation logic for a single property, you can add a custom validation logic that is executed in object level. Example:

ObjectExtensionManager.Instance
.AddOrUpdate<IdentityUserCreateDto>(objConfig =>
{
    //Define two properties with their own validation rules
    
    objConfig.AddOrUpdateProperty<string>("Password", propertyConfig =>
    {
        propertyConfig.Attributes.Add(new RequiredAttribute());
    });

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

    //Write a common validation logic works on multiple properties
    
    objConfig.Validators.Add(context =>
    {
        if (context.ValidatingObject.GetProperty<string>("Password") !=
            context.ValidatingObject.GetProperty<string>("PasswordRepeat"))
        {
            context.ValidationErrors.Add(
                new ValidationResult(
                    "Please repeat the same password!",
                    new[] { "Password", "PasswordRepeat" }
                )
            );
        }
    });
});

Object to Object Mapping

Assume that you've added an extra property to an extensible entity object and used auto object to object mapping to map this entity to an extensible DTO class. You need to be careful in such a case, because the extra property may contain a sensitive data that should not be available to clients.

This section offers some good practices to control your extra properties on object mapping.

MapExtraPropertiesTo

MapExtraPropertiesTo is an extension method provided by the ABP Framework to copy extra properties from an object to another in a controlled manner. Example usage:

identityUser.MapExtraPropertiesTo(identityUserDto);

MapExtraPropertiesTo requires to define properties (as described above) in both sides (IdentityUser and IdentityUserDto in this case) in order to copy the value to the target object. Otherwise, it doesn't copy the value even if it does exists in the source object (identityUser in this example). There are some ways to overload this restriction.

MappingPropertyDefinitionChecks

MapExtraPropertiesTo gets an additional parameter to control the definition check for a single mapping operation:

identityUser.MapExtraPropertiesTo(
    identityUserDto,
    MappingPropertyDefinitionChecks.None
);

Be careful since MappingPropertyDefinitionChecks.None copies all extra properties without any check. MappingPropertyDefinitionChecks enum has other members too.

If you want to completely disable definition check for a property, you can do it while defining the extra property (or update an existing definition) as shown below:

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

Ignored Properties

You may want to ignore some properties on a specific mapping operation:

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

Ignored properties are not copied to the target object.

AutoMapper Integration

If you're using the AutoMapper library, the ABP Framework also provides an extension method to utilize the MapExtraPropertiesTo method defined above.

You can use the MapExtraProperties() method inside your mapping profile.

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

It has the same parameters with the MapExtraPropertiesTo method.

Entity Framework Core Database Mapping

If you're using the EF Core, you can map an extra property to a table field in the database. Example:

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

See the Entity Framework Core Integration document for more.

In this document