docs/en/low-code/fluent-api.md
//[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."
}
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.
Attributes and Fluent API can define or override:
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.
[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; }
}
AbpDynamicEntityConfig.SourceAssemblies.Add(
new DynamicEntityAssemblyInfo(typeof(MyDomainModule).Assembly)
);
await DynamicModelManager.Instance.InitializeAsync();
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"
);
});
From lowest to highest priority:
[DynamicEntity], property attributes, and CLR validation attributes_DynamicAbpDynamicEntityConfig.EntityConfigurationsA 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.
| 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"
}
}
| Attribute | Use it for | Key members |
|---|---|---|
| <code class="text-nowrap">[DynamicPropertyUI]</code> | Sets the property display name | DisplayName |
| <code class="text-nowrap">[Display(Name = ...)]</code> | Sets the property display name with standard .NET metadata | Name |
| <code class="text-nowrap">[DynamicPropertyType]</code> | Overrides the inferred low-code type | Type |
| <code class="text-nowrap">[DynamicPropertyDefaultValue]</code> | Sets the descriptor default value | DefaultValue |
| <code class="text-nowrap">[DynamicPropertyNotMapped]</code> | Stores the property in the data dictionary instead of a dedicated DB field | No properties |
| <code class="text-nowrap">[DynamicForeignKey]</code> | Configures a lookup relation | EntityName, DisplayPropertyName, Access, DependsOnPropertyName, DependsOnFilterPropertyName |
| <code class="text-nowrap">[DynamicPropertySetByClients]</code> | Controls whether clients may write the field | Allow |
| <code class="text-nowrap">[DynamicPropertyServerOnly]</code> | Hides the field from clients entirely | No properties |
| <code class="text-nowrap">[DynamicPropertyUnique]</code> | Marks the field as unique | No properties |
| <code class="text-nowrap">[DynamicPropertyFileOptions]</code> | Marks the field as a file and applies upload constraints | MaxSizeBytes, AllowedContentTypes |
| <code class="text-nowrap">[DynamicPropertyImageOptions]</code> | Marks the field as an image and applies upload and resize constraints | MaxSizeBytes, MaxWidth, MaxHeight, ResizeMode, AllowedContentTypes |
[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.
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 MaxHeightfill: crops from the center and scales the image to fill the target box; use it when both MaxWidth and MaxHeight are setIf 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.
Code-defined low-code properties also consume standard .NET metadata:
[Display(Name = ...)] sets the descriptor DisplayNameValidationAttribute on the CLR property is copied into the descriptor[Required] and non-nullable types contribute to IsRequiredThe required-state rules are:
[Required] always makes the property requiredint, Guid, DateTime, decimal, and bool are treated as requiredint? and Guid? are optionalstring? are optional[Required] stay optional for backward compatibilityFor 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.
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.
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 SurfaceThe 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 SurfaceAddOrGetProperty(...), FindProperty(...), and GetProperty(...) work with EntityPropertyDescriptor.
| 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 |
| 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 |
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.'; }"
});
}
);
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.
The final model is merged in this order:
Fluent API > JSON descriptors > Code attributes and CLR metadata > Defaults
For example:
[DynamicPropertyUnique] marks a field as unique but a descriptor sets "isUnique": false, the descriptor winsDisplayName, the Fluent API wins