Open Source Web Application Framework for ASP.NET Core
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

22 KiB

//[doc-seo]
{
    "Description": "Define ABP Low-Code entities in C# with attributes and refine them with the Fluent API. This page documents the developer-facing code-first surface, including entity/property attributes, validation behavior, attachments, file/image options, enums, and fluent configuration helpers."
}

Attributes & Fluent API

Preview: Attributes and Fluent API configuration for the Low-Code System are preview APIs. Prefer the designer for day-to-day modeling work, and review release notes before relying on these APIs in long-lived integrations.

Use the Low-Code Designer for runtime pages, forms, filters, dashboards, menus, and page groups. The code-first surface documented here is for source-controlled model metadata: entity definitions, property metadata, enums, validation, attachments, relationships, and runtime overrides.

What This Surface Configures

Attributes and Fluent API can define or override:

  • entity registration and display names
  • parent-child entity structure
  • default display properties for lookups
  • property types, defaults, uniqueness, required state, and storage shape
  • foreign keys to dynamic or reference entities
  • file and image upload constraints
  • entity attachments
  • enum registration
  • command interceptors
  • cross-field validations

They do not define page titles, field placements, tabs, form layouts, filters, dashboards, menus, or page groups. Configure those in the designer or in Model Descriptor Files.

Quick Start

Step 1: Define a Dynamic Enum and Entity

[DynamicEnum]
public enum ProductStatus
{
    Draft = 0,
    Active = 1,
    Discontinued = 2
}

[DynamicEntity(DefaultDisplayPropertyName = nameof(Name))]
[DynamicEntityUI("Products")]
[DynamicEntityAttachments(
    "application/pdf",
    "image/png",
    "image/jpeg",
    MaxFileCount = 5,
    MaxFileSizeBytes = 5 * 1024 * 1024
)]
public class Product : DynamicEntityBase
{
    [Required]
    [StringLength(128)]
    [DynamicPropertyUnique]
    public string Name { get; set; } = null!;

    [Display(Name = "Unit Price")]
    [DynamicPropertyType(EntityPropertyType.Money)]
    [DynamicPropertyDefaultValue("0")]
    public decimal Price { get; set; }

    public ProductStatus Status { get; set; }

    [DynamicForeignKey("Catalog.Categories.Category", "Name", ForeignAccess.View)]
    public Guid? CategoryId { get; set; }

    [DynamicPropertyFileOptions(
        "application/pdf",
        MaxSizeBytes = 5 * 1024 * 1024
    )]
    public string? SpecSheet { get; set; }

    [DynamicPropertyImageOptions(
        "image/png",
        "image/jpeg",
        MaxWidth = 1024,
        MaxHeight = 1024,
        MaxSizeBytes = 2 * 1024 * 1024,
        ResizeMode = "fit"
    )]
    public string? HeroImage { get; set; }

    [DynamicPropertyNotMapped]
    public string? SearchSummary { get; set; }
}

Step 2: Register the Assembly and Initialize the Model

AbpDynamicEntityConfig.SourceAssemblies.Add(
    new DynamicEntityAssemblyInfo(typeof(MyDomainModule).Assembly)
);

await DynamicModelManager.Instance.InitializeAsync();

Step 3: Apply Fluent Overrides When Needed

AbpDynamicEntityConfig.EntityConfigurations.Configure<Product>(entity =>
{
    entity.WithDisplayName("Catalog Products");
    entity.EnableAttachments(
        10,
        5 * 1024 * 1024,
        20 * 1024 * 1024,
        "application/pdf",
        "image/png",
        "image/jpeg"
    );

    entity.AddOrGetProperty("DiscountedPrice")
        .AsMoney()
        .WithDefaultValue("0");

    entity.AddCrossFieldValidation(
        "DiscountedPrice",
        "Price",
        EntityCrossFieldValidationOperators.LessThanOrEqual,
        "Discounted price cannot exceed unit price."
    );

    entity.GetProperty("HeroImage").AsImage(
        2 * 1024 * 1024,
        1024,
        1024,
        "fit",
        "image/png",
        "image/jpeg"
    );
});

Three-Layer Configuration System

From lowest to highest priority:

  1. Code layer: C# classes with [DynamicEntity], property attributes, and CLR validation attributes
  2. JSON descriptor layer: source-controlled descriptor files under _Dynamic
  3. Fluent layer: AbpDynamicEntityConfig.EntityConfigurations

A DefaultLayer runs last to fill in conventions such as required validation for non-nullable properties.

When the same value is set in multiple layers, the higher-priority layer wins.

C# Attributes Reference

Entity-Level Attributes

Attribute Use it for Key members
[DynamicEntity] Marks a CLR class as a low-code entity Parent, DefaultDisplayPropertyName
[DynamicEntityUI] Sets the entity display name used by the model DisplayName
[DynamicEntityAttachments] Enables and limits entity attachments IsEnabled, MaxFileCount, MaxFileSizeBytes, MaxTotalSizeBytes, AllowedContentTypes
[DynamicEntityCommandInterceptor] Adds Create / Update / Delete JavaScript interceptors Name, Type, Javascript
[DynamicEnum] Registers a CLR enum for low-code enum usage No properties

[DynamicEntity]

Use [DynamicEntity] on any class that should participate in the low-code model.

[DynamicEntity(DefaultDisplayPropertyName = nameof(Name))]
public class Product : DynamicEntityBase
{
    public string Name { get; set; } = null!;
}

DefaultDisplayPropertyName defaults to "Id". Set it when lookups and relation labels should show another field such as Name or Code.

For parent-child structures, set Parent by full entity name. The attribute also has a Type constructor overload when the parent CLR type is available:

[DynamicEntity("Catalog.Orders.Order")]
public class OrderLine : DynamicEntityBase
{
    [DynamicForeignKey("Catalog.Products.Product", "Name")]
    public Guid ProductId { get; set; }
}

When Parent is set and the child class does not already declare <ParentName>Id, the code layer adds that mapped Guid foreign-key property automatically.

[DynamicEntityUI]

DynamicEntityUIAttribute only sets the entity DisplayName:

[DynamicEntity]
[DynamicEntityUI("Products")]
public class Product : DynamicEntityBase
{
}

It does not configure page titles, form layouts, menu placement, dashboards, or other screen metadata.

[DynamicEntityAttachments]

Use this when the entity itself should support attachments:

[DynamicEntity]
[DynamicEntityAttachments(
    "application/pdf",
    "image/png",
    MaxFileCount = 3,
    MaxFileSizeBytes = 5 * 1024 * 1024,
    MaxTotalSizeBytes = 15 * 1024 * 1024
)]
public class Product : DynamicEntityBase
{
}

Set IsEnabled = false to disable entity attachments explicitly in code.

[DynamicEntityCommandInterceptor]

This attribute can be applied multiple times to the same entity:

[DynamicEntity]
[DynamicEntityCommandInterceptor(
    "Create",
    InterceptorType.Pre,
    "if (!context.commandArgs.data['Name']) { globalError = 'Name is required.'; }"
)]
[DynamicEntityCommandInterceptor(
    "Delete",
    InterceptorType.Post,
    "context.log('Deleted: ' + context.commandArgs.entityId);"
)]
public class Product : DynamicEntityBase
{
    public string Name { get; set; } = null!;
}

Use Create, Update, or Delete for Name, and Pre, Post, or Replace for Type. See Interceptors for the full scripting context and replace-mode behavior.

[DynamicEnum]

Decorate CLR enums that should be visible to the low-code model:

[DynamicEnum]
public enum ProductStatus
{
    Draft = 0,
    Active = 1,
    Discontinued = 2
}

Then use the enum directly on a dynamic entity property:

[DynamicEntity]
public class Product : DynamicEntityBase
{
    public ProductStatus Status { get; set; }
}

Enum localization follows the usual ABP pattern:

{
  "culture": "en",
  "texts": {
    "Enum:ProductStatus.Draft": "Draft",
    "Enum:ProductStatus.Active": "Active",
    "Enum:ProductStatus.Discontinued": "Discontinued"
  }
}

Property-Level Attributes

Attribute Use it for Key members
[DynamicPropertyUI] Sets the property display name DisplayName
[Display(Name = ...)] Sets the property display name with standard .NET metadata Name
[DynamicPropertyType] Overrides the inferred low-code type Type
[DynamicPropertyDefaultValue] Sets the descriptor default value DefaultValue
[DynamicPropertyNotMapped] Stores the property in the data dictionary instead of a dedicated DB field No properties
[DynamicForeignKey] Configures a lookup relation EntityName, DisplayPropertyName, Access, DependsOnPropertyName, DependsOnFilterPropertyName
[DynamicPropertySetByClients] Controls whether clients may write the field Allow
[DynamicPropertyServerOnly] Hides the field from clients entirely No properties
[DynamicPropertyUnique] Marks the field as unique No properties
[DynamicPropertyFileOptions] Marks the field as a file and applies upload constraints MaxSizeBytes, AllowedContentTypes
[DynamicPropertyImageOptions] Marks the field as an image and applies upload and resize constraints MaxSizeBytes, MaxWidth, MaxHeight, ResizeMode, AllowedContentTypes

Display Names: [DynamicPropertyUI] and [Display]

Both attributes write to the same DisplayName field in the descriptor:

[DynamicPropertyUI(DisplayName = "SKU")]
public string Code { get; set; } = null!;

[Display(Name = "Unit Price")]
public decimal Price { get; set; }

If both are present on the same property, [Display(Name = ...)] takes precedence.

Like DynamicEntityUI, DynamicPropertyUI does not control list visibility, form placement, filters, quick-look order, tabs, or other runtime screen layout details. Those belong to page and form descriptors.

Type, Storage, Relations, and Upload Options

public class Product : DynamicEntityBase
{
    [DynamicPropertyType(EntityPropertyType.Money)]
    [DynamicPropertyDefaultValue("0")]
    public decimal Price { get; set; }

    [DynamicForeignKey(
        "Catalog.Categories.Category",
        "Name",
        ForeignAccess.View
    )]
    public Guid? CategoryId { get; set; }

    [DynamicForeignKey(
        "Catalog.Products.Product",
        "Name",
        ForeignAccess.View,
        DependsOnPropertyName = nameof(CategoryId),
        DependsOnFilterPropertyName = "CategoryId"
    )]
    public Guid? RelatedProductId { get; set; }

    [DynamicPropertyNotMapped]
    public string? SearchSummary { get; set; }

    [DynamicPropertySetByClients(false)]
    public string? GeneratedCode { get; set; }

    [DynamicPropertyServerOnly]
    public string? InternalNotes { get; set; }

    [DynamicPropertyUnique]
    public string Code { get; set; } = null!;

    [DynamicPropertyFileOptions(
        "application/pdf",
        MaxSizeBytes = 5 * 1024 * 1024
    )]
    public string? SpecSheet { get; set; }

    [DynamicPropertyImageOptions(
        "image/png",
        "image/jpeg",
        MaxWidth = 1024,
        MaxHeight = 1024,
        MaxSizeBytes = 2 * 1024 * 1024,
        ResizeMode = "fit"
    )]
    public string? HeroImage { get; set; }
}

DynamicPropertyFileOptions sets Type = File. DynamicPropertyImageOptions sets Type = Image. Use them instead of setting file or image types manually when you also need upload constraints.

For image fields, ResizeMode currently supports these values:

  • fit: preserves aspect ratio and scales the image down so it fits within MaxWidth and MaxHeight
  • fill: crops from the center and scales the image to fill the target box; use it when both MaxWidth and MaxHeight are set

If ResizeMode = "fill" is configured without both MaxWidth and MaxHeight, the current React runtime falls back to fit.

Resize is currently applied client-side by the React low-code runtime form before upload. Backend upload endpoints validate file size and content type, then store the stream as-is. If you upload images through scripting or direct API/backend calls instead of the React runtime form, no automatic resize is applied on the server.

Standard .NET Attributes and Required Inference

Code-defined low-code properties also consume standard .NET metadata:

  • [Display(Name = ...)] sets the descriptor DisplayName
  • any ValidationAttribute on the CLR property is copied into the descriptor
  • [Required] and non-nullable types contribute to IsRequired

The required-state rules are:

  • [Required] always makes the property required
  • non-nullable value types such as int, Guid, DateTime, decimal, and bool are treated as required
  • nullable value types such as int? and Guid? are optional
  • nullable reference types such as string? are optional
  • non-nullable reference types without an explicit [Required] stay optional for backward compatibility

For source-controlled low-code models, the built-in validation set that round-trips cleanly through the descriptor pipeline is:

  • [Required]
  • [StringLength]
  • [MaxLength]
  • [MinLength]
  • [Range]
  • [EmailAddress]
  • [Phone]
  • [Url]
  • [CreditCard]
  • [RegularExpression]

Example:

public class Product : DynamicEntityBase
{
    [Required]
    [StringLength(128, MinimumLength = 3)]
    public string Name { get; set; } = null!;

    [Range(0, 100000)]
    public decimal Price { get; set; }

    [EmailAddress]
    public string? ContactEmail { get; set; }
}

For advanced validation scenarios, JSON validators and cross-field validations are still the better fit than custom CLR validation attributes.

Fluent API

The Fluent API is the highest-priority model layer. Use it when you need runtime overrides, source-controlled patches over descriptor files, or descriptor-only properties without adding CLR members.

Registration and Initialization

Register source assemblies before initialization:

AbpDynamicEntityConfig.SourceAssemblies.Add(
    new DynamicEntityAssemblyInfo(
        typeof(MyDomainModule).Assembly,
        rootNamespace: "Catalog",
        projectRootPath: sourcePath
    )
);

await DynamicModelManager.Instance.InitializeAsync();

You can also register specific types directly:

AbpDynamicEntityConfig.DynamicEntityTypes.Add(typeof(Product));
AbpDynamicEntityConfig.DynamicEnumTypes.Add(typeof(ProductStatus));

Use the generic overload when a CLR type exists, or the string overload for descriptor-only entities:

AbpDynamicEntityConfig.EntityConfigurations.Configure<Product>(entity =>
{
    entity.WithDisplayName("Products");
});

AbpDynamicEntityConfig.EntityConfigurations.Configure(
    "Catalog.Products.Product",
    entity =>
    {
        entity.WithDisplayName("Products");
    }
);

EntityDescriptor Surface

The Configure(...) callback gives you an EntityDescriptor.

Member What it controls
DisplayName Entity display name
DefaultDisplayPropertyName Field used for lookups and relation labels
Parent Parent entity name for child entities
Attachments Raw EntityAttachmentDescriptor access
CrossFieldValidations Raw cross-field validation list
Interceptors Raw interceptor list
Properties Full property descriptor list
AddOrGetProperty(name) Returns an existing property or creates a new descriptor
FindProperty(name) Returns the property or null
GetProperty(name) Returns the property or throws
WithDisplayName(string) Fluent helper for DisplayName
EnableAttachments(...) Fluent helper for attachment configuration
DisableAttachments() Explicitly disables attachments
AddCrossFieldValidation(...) Adds an entity-level cross-field validation

EntityPropertyDescriptor Surface

AddOrGetProperty(...), FindProperty(...), and GetProperty(...) work with EntityPropertyDescriptor.

Direct members

Member What it controls
DisplayName Property display label
Type Low-code property type
IsRequired Required state
IsMappedToDbField Dedicated DB field vs data dictionary storage
DefaultValue Default value stored in the descriptor
IsUnique Uniqueness
AllowSetByClients Client write access
ServerOnly Hide from clients entirely
ForeignKey Foreign key descriptor
EnumType Enum CLR type name for descriptor-only enum fields
FileMaxSizeBytes File/image max size
FileAllowedContentTypes Allowed MIME types for file/image fields
ImageMaxWidth Image width constraint
ImageMaxHeight Image height constraint
ImageResizeMode Resize strategy
ValidationAttributes Raw validation descriptors

Extension methods

Method What it does
MapToDbField(bool isMappedToDbField = true) Sets IsMappedToDbField
AsType(EntityPropertyType type) Sets Type
AsMoney() Sets Type = Money
AsFile(long? maxSizeBytes = null, params string[] allowedContentTypes) Sets Type = File and file constraints
AsImage(long? maxSizeBytes = null, int? maxWidth = null, int? maxHeight = null, string? resizeMode = null, params string[] allowedContentTypes) Sets Type = Image and image constraints
WithDefaultValue(string? defaultValue) Sets DefaultValue
WithEnumType(string enumType) Sets Type = Enum and EnumType
WithForeignKey(string entityName, string? displayPropertyName = null, ForeignAccess access = ForeignAccess.None, string? dependsOnPropertyName = null, string? dependsOnFilterPropertyName = null) Configures the relation in one call
AsServerOnly(bool serverOnly = true) Sets ServerOnly
AsRequired(bool isRequired = true) Sets IsRequired

Fluent API Example

AbpDynamicEntityConfig.EntityConfigurations.Configure(
    "Catalog.Products.Product",
    entity =>
    {
        entity.WithDisplayName("Products");
        entity.DefaultDisplayPropertyName = "Name";
        entity.EnableAttachments(
            5,
            5 * 1024 * 1024,
            20 * 1024 * 1024,
            "application/pdf",
            "image/png"
        );

        entity.AddOrGetProperty("Code")
            .AsRequired()
            .WithDefaultValue("P-");

        entity.GetProperty("Code").DisplayName = "SKU";

        entity.AddOrGetProperty("Price")
            .AsMoney()
            .AsRequired();

        entity.GetProperty("Price").DisplayName = "Unit Price";

        entity.AddOrGetProperty("Status")
            .WithEnumType("Catalog.Products.ProductStatus");

        entity.AddOrGetProperty("CategoryId")
            .WithForeignKey("Catalog.Categories.Category", "Name", ForeignAccess.View);

        entity.AddOrGetProperty("HeroImage")
            .AsImage(
                2 * 1024 * 1024,
                1024,
                1024,
                "fit",
                "image/png",
                "image/jpeg"
            );

        entity.AddOrGetProperty("InternalNotes")
            .AsServerOnly();

        entity.AddOrGetProperty("DiscountedPrice")
            .AsMoney()
            .WithDefaultValue("0");

        entity.AddCrossFieldValidation(
            "DiscountedPrice",
            "Price",
            EntityCrossFieldValidationOperators.LessThanOrEqual,
            "Discounted price cannot exceed unit price."
        );

        entity.Interceptors.Add(new CommandInterceptorDescriptor("Create")
        {
            Type = InterceptorType.Pre,
            Javascript = "if (!context.commandArgs.data['Name']) { globalError = 'Name is required.'; }"
        });
    }
);

Fluent API Notes

  • Use attributes when the field already exists as a CLR property.
  • Use Fluent API when you need to override descriptor values without touching the CLR class.
  • Use Fluent API when you need descriptor-only properties or cross-field validations.
  • Use JSON descriptors or the designer for page/form/filter/dashboard/menu/page-group metadata.

EF Core Setup

Your DbContext should implement IDbContextWithDynamicEntities and call both ConfigureDynamicEntities() and ConfigureLowCode(). Call ConfigureDynamicEntities() before base.OnModelCreating(builder) so dynamic entity mappings are available when ABP applies the base model conventions:

public class CatalogDbContext : AbpDbContext<CatalogDbContext>, IDbContextWithDynamicEntities
{
    protected override void OnModelCreating(ModelBuilder builder)
    {
        builder.ConfigureDynamicEntities();

        base.OnModelCreating(builder);

        builder.ConfigureLowCode();
    }
}

Run low-code initialization before EF Core design-time migrations so the model is complete when migrations are generated.

Layer Precedence

The final model is merged in this order:

Fluent API > JSON descriptors > Code attributes and CLR metadata > Defaults

For example:

  • if [DynamicPropertyUnique] marks a field as unique but a descriptor sets "isUnique": false, the descriptor wins
  • if JSON sets a display name and Fluent API later changes DisplayName, the Fluent API wins

See Also