@ -0,0 +1,287 @@ |
|||||
|
```json |
||||
|
//[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](designer.md) and [React Runtime](react-runtime.md) 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](reference-entities.md) 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: |
||||
|
|
||||
|
````csharp |
||||
|
AbpDynamicEntityConfig.ReferencedEntityList.Add<IdentityUser>( |
||||
|
"UserName", |
||||
|
"UserName", |
||||
|
"Email" |
||||
|
); |
||||
|
```` |
||||
|
|
||||
|
Then use the registered CLR type name in a foreign key: |
||||
|
|
||||
|
````csharp |
||||
|
[DynamicForeignKey("Volo.Abp.Identity.IdentityUser", "UserName")] |
||||
|
public Guid? OwnerId { get; set; } |
||||
|
```` |
||||
|
|
||||
|
or in a descriptor file: |
||||
|
|
||||
|
```json |
||||
|
{ |
||||
|
"name": "OwnerId", |
||||
|
"foreignKey": { |
||||
|
"entityName": "Volo.Abp.Identity.IdentityUser" |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
Scripts can also query the same reference entity: |
||||
|
|
||||
|
```javascript |
||||
|
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](reference-entities.md). |
||||
|
|
||||
|
## 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: |
||||
|
|
||||
|
````csharp |
||||
|
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: |
||||
|
|
||||
|
````csharp |
||||
|
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: |
||||
|
|
||||
|
````csharp |
||||
|
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: |
||||
|
|
||||
|
````csharp |
||||
|
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`: |
||||
|
|
||||
|
````csharp |
||||
|
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: |
||||
|
|
||||
|
````csharp |
||||
|
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 |
||||
|
|
||||
|
* [Reference Entities](reference-entities.md) |
||||
|
* [Attributes & Fluent API](fluent-api.md) |
||||
|
* [Model Descriptor Files](model-json.md) |
||||
|
* [React Runtime](react-runtime.md) |
||||
|
* [Scripting API](scripting-api.md) |
||||
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 60 KiB |