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.
 
 
 
 
 
 

11 KiB

//[doc-seo]
{
    "Description": "Connect ABP Low-Code entities with regular C# code. Register reference entities for low-code lookups and access low-code entities from application code via typed repositories, DynamicEntity, or IDynamicPageAppService."
}

Code Integration

Preview: Low-Code integration APIs are part of the preview Low-Code System. Registration details, repository behavior, and page service contracts may change before general availability.

Use the Low-Code Designer and React Runtime for standard CRUD screens. This page covers the integration points for the cases where low-code models and regular application code need to reach each other directly.

Choose the Right Integration Path

Need Recommended approach
A low-code field should point to an existing C# entity such as IdentityUser Register a reference entity and use its CLR full name in the foreign key definition
Application code should work with a low-code entity that has a real C# class Inject IRepository<TEntity, Guid> for that [DynamicEntity] class
Application code should work with a descriptor-only or runtime-defined low-code entity Inject IRepository<DynamicEntity, Guid> and call SetEntityName(...)
Application code should reuse runtime page behavior such as low-code permissions, foreign-access checks, files, attachments, or child-page rules Inject IDynamicPageAppService and work by pageName

Low-Code to Existing C# Entities

Low-code entities can point to existing entities that are not managed by the Low-Code System itself. Register them once during startup:

AbpDynamicEntityConfig.ReferencedEntityList.Add<IdentityUser>(
    "UserName",
    "UserName",
    "Email"
);

Then use the registered CLR type name in a foreign key:

[DynamicForeignKey("Volo.Abp.Identity.IdentityUser", "UserName")]
public Guid? OwnerId { get; set; }

or in a descriptor file:

{
  "name": "OwnerId",
  "foreignKey": {
    "entityName": "Volo.Abp.Identity.IdentityUser"
  }
}

Scripts can also query the same reference entity:

var user = await db.get('Volo.Abp.Identity.IdentityUser', ownerId);

Reference entities are read-only from the low-code side. For the full registration model and metadata details, see Reference Entities.

C# Code to Low-Code Entities

Option 1: Typed Repository for Code-Defined Dynamic Entities

If the low-code entity is declared as a normal C# class with [DynamicEntity] and DynamicEntityBase, use it like any other ABP entity. The low-code demo's Customer entity is a typical example:

public class CustomerSyncService : ITransientDependency
{
    private readonly IRepository<Customer, Guid> _customerRepository;

    public CustomerSyncService(IRepository<Customer, Guid> customerRepository)
    {
        _customerRepository = customerRepository;
    }

    public async Task UpdateAsync(Guid id)
    {
        var customer = await _customerRepository.GetAsync(id);

        customer.SetData("Telephone", "5550001122");
        customer.SetData("CreditLimit", 119.99m);

        await _customerRepository.UpdateAsync(customer);
    }
}

This is the simplest path when a CLR type already exists. GetData(...) and SetData(...) still work for dynamic or extra properties on that typed entity.

Option 2: Generic Repository for Descriptor-Only or Runtime-Defined Entities

If the low-code entity only exists in descriptor files or runtime metadata, inject IRepository<DynamicEntity, Guid>. Set the entity name before repository operations:

public class ProductImportService : ITransientDependency
{
    private readonly IRepository<DynamicEntity, Guid> _dynamicEntityRepository;

    public ProductImportService(IRepository<DynamicEntity, Guid> dynamicEntityRepository)
    {
        _dynamicEntityRepository = dynamicEntityRepository;
    }

    public async Task<Guid> CreateAsync()
    {
        _dynamicEntityRepository.SetEntityName("LowCodeDemo.Products.Product");

        var product = new DynamicEntity("LowCodeDemo.Products.Product", Guid.NewGuid())
        {
            ["Name"] = "Road Helmet",
            Data =
            {
                ["Price"] = 125m,
                ["StockCount"] = 40
            }
        };

        await _dynamicEntityRepository.InsertAsync(product);
        return product.Id;
    }

    public async Task<decimal> GetPriceAsync(Guid id)
    {
        _dynamicEntityRepository.SetEntityName("LowCodeDemo.Products.Product");

        var product = await _dynamicEntityRepository.GetAsync(id);
        return product.GetData<decimal>("Price");
    }
}

Call SetEntityName(...) again whenever you switch to another low-code entity name. After that, standard repository operations such as GetAsync, GetListAsync, InsertAsync, UpdateAsync, DeleteAsync, and GetQueryableAsync work normally.

Use GetData(...) and SetData(...) for normal read/write code. Use the indexers only when you need to target the underlying storage explicitly, such as query expressions or manual DynamicEntity construction.

API Use it for
entity.GetData<T>("FieldName") General read API; it resolves CLR properties, mapped fields, and extra properties through one call
entity.SetData("FieldName", value) General write API; it chooses the mapped field or Data dictionary automatically
entity["FieldName"] Direct access to a dynamic property with isMappedToDbField: true
entity.Data["FieldName"] Direct access to a dynamic property without isMappedToDbField: true

Query Expressions: entity["Field"] vs entity.Data["Field"]

For materialized entities, GetData(...) is the safest read API because it hides the storage details. For IQueryable expressions, use the access form that matches the property's storage shape so EF Core and the low-code query layer can translate it correctly:

Property shape Query form
Normal CLR property x.PropertyName
Dynamic property with isMappedToDbField: true x["PropertyName"]
Dynamic property without isMappedToDbField: true x.Data["PropertyName"]

The low-code demo uses both patterns in the same query. In LowCodeDemo.Orders.OrderLine, ProductId is mapped to a DB field, while Amount stays in the Data dictionary:

var orderLineQuery = await _dynamicEntityRepository
    .SetEntityName("LowCodeDemo.Orders.OrderLine")
    .GetQueryableAsync();

var filtered = orderLineQuery
    .Where(line => line.Data["Amount"] != null && (decimal?)line.Data["Amount"] > 1000m)
    .Select(line => new
    {
        ProductId = (Guid?)line["ProductId"],
        Amount = (decimal?)line.Data["Amount"]
    });

The same rule applies to joins. ProductId is joined through the entity indexer because it is mapped, while CustomerId on LowCodeDemo.Orders.Order is joined through Data because it is not:

var orderLineQuery = await _dynamicEntityRepository
    .SetEntityName("LowCodeDemo.Orders.OrderLine")
    .GetQueryableAsync();

var orderQuery = await _dynamicEntityRepository
    .SetEntityName("LowCodeDemo.Orders.Order")
    .GetQueryableAsync();

var query =
    from orderLine in orderLineQuery
    join order in orderQuery
        on (Guid?)orderLine["OrderId"] equals order.Id
    select new
    {
        OrderId = order.Id,
        CustomerId = (Guid?)order.Data["CustomerId"],
        Amount = (decimal?)orderLine.Data["Amount"]
    };

Use GetData(...) again after the query has been materialized into entity objects.

Option 3: IDynamicPageAppService for Runtime-Like Behavior

Use IDynamicPageAppService when custom code should go through the same page-level behavior as the generated runtime: low-code permissions, foreign-access validation, lookup rules, file handling, attachments, export, and child records.

This API works with pageName, not entityName:

public class ProductRuntimeService : ITransientDependency
{
    private readonly IDynamicPageAppService _dynamicPageAppService;

    public ProductRuntimeService(IDynamicPageAppService dynamicPageAppService)
    {
        _dynamicPageAppService = dynamicPageAppService;
    }

    public async Task<DynamicEntityDto> CreateAsync()
    {
        return await _dynamicPageAppService.CreateAsync(
            "products",
            new DynamicEntityCreateInput
            {
                Properties = new Dictionary<string, string?>
                {
                    ["Name"] = "Road Helmet",
                    ["Price"] = "125",
                    ["StockCount"] = "40"
                }
            }
        );
    }

    public async Task<PagedResultDto<DynamicEntityDto>> GetListAsync()
    {
        return await _dynamicPageAppService.GetDataAsync(
            "products",
            new DynamicEntityListRequestDto
            {
                MaxResultCount = 20
            }
        );
    }
}

Reach for this service when your custom code should behave like the runtime page rather than like a raw repository call.

Joining Low-Code and Regular Entities in LINQ

Low-code entities can be composed with typed repositories in the same LINQ query. The low-code demo does this by joining dynamic order lines and orders with the typed Customer repository:

var orderLineQuery = await _dynamicEntityRepository
    .SetEntityName("LowCodeDemo.Orders.OrderLine")
    .GetQueryableAsync();

var orderQuery = (await _dynamicEntityRepository
    .SetEntityName("LowCodeDemo.Orders.Order")
    .GetQueryableAsync())
    .As<IQueryable<DynamicEntity>>();

var customerQuery = (await _customerRepository.GetQueryableAsync())
    .As<IQueryable<IEntity<Guid>>>();

var query = orderLineQuery
    .Where(line => line.Data["Amount"] != null && (decimal?)line.Data["Amount"] > 1000m)
    .GroupJoin(
        orderQuery,
        orderLine => (Guid?)orderLine["OrderId"],
        order => order.Id,
        (orderLine, orders) => new { orderLine, orders }
    )
    .SelectMany(
        x => x.orders.DefaultIfEmpty(),
        (x, order) => new { x.orderLine, order }
    )
    .GroupJoin(
        customerQuery,
        x => (Guid?)x.order!.Data["CustomerId"],
        customer => customer.Id,
        (x, customers) => new { x.orderLine, x.order, customers }
    );

This is useful when a business rule or reporting service needs both low-code entities and typed C# entities in one application-layer query.

See Also