|
After Width: | Height: | Size: 82 KiB |
@ -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) |
||||
@ -0,0 +1,182 @@ |
|||||
|
```json |
||||
|
//[doc-seo] |
||||
|
{ |
||||
|
"Description": "Define ABP Low-Code dashboard pages, visualizations, filters, layout, permissions, and React runtime data flow." |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
# Dashboards |
||||
|
|
||||
|
Dashboards are low-code pages that render charts, lists, and number widgets from low-code entity data. A dashboard is still a page, so it uses the normal page name, title, icon, order, group, and permission model, but its page type is `dashboard` and its page definition carries a nested `dashboard` payload. The screenshot below shows an inventory-style dashboard from the demo app, and the JSON that follows is a simplified descriptor that uses the same layout concepts. |
||||
|
|
||||
|
The runtime below was generated from a low-code dashboard page definition: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
## Dashboard Pages |
||||
|
|
||||
|
Dashboard pages live in the normal page descriptor collection. In source-controlled models, a dashboard page still belongs under `pages/`, not a separate dashboard folder. |
||||
|
|
||||
|
Typical page-level fields include: |
||||
|
|
||||
|
* `name` |
||||
|
* `title` |
||||
|
* `icon` |
||||
|
* `type: "dashboard"` |
||||
|
* `group` |
||||
|
* `order` |
||||
|
* `permissionConfig` |
||||
|
* `dashboard` |
||||
|
|
||||
|
Runtime routes use the same dynamic page convention: |
||||
|
|
||||
|
```text |
||||
|
/dynamic/<page-name> |
||||
|
``` |
||||
|
|
||||
|
## Layout Model |
||||
|
|
||||
|
The stored dashboard descriptor uses a **flat visualization list**. Each visualization defines its own placement: |
||||
|
|
||||
|
* `row`: zero-based visual row index |
||||
|
* `order`: order within the row |
||||
|
* `width`: current dashboard grid width, typically `1` or `2` |
||||
|
|
||||
|
At runtime, the React UI groups those flat visualizations into rendered rows. This is similar to form layouts: storage stays flat, rendering derives the grouped structure. |
||||
|
|
||||
|
```json |
||||
|
{ |
||||
|
"name": "inventory-overview", |
||||
|
"title": "Inventory Overview", |
||||
|
"type": "dashboard", |
||||
|
"group": "inventory", |
||||
|
"dashboard": { |
||||
|
"description": "Operational inventory view", |
||||
|
"visualizations": [ |
||||
|
{ |
||||
|
"name": "product-count", |
||||
|
"type": "numberContainer", |
||||
|
"title": "Product Count", |
||||
|
"row": 0, |
||||
|
"order": 0, |
||||
|
"width": 1, |
||||
|
"numberContainer": { |
||||
|
"items": [ |
||||
|
{ |
||||
|
"name": "total-products", |
||||
|
"title": "Total Products", |
||||
|
"entityName": "Acme.Catalog.Product", |
||||
|
"aggregation": "count", |
||||
|
"format": "number" |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
"name": "stock-by-product", |
||||
|
"type": "chart", |
||||
|
"title": "Stock by Product", |
||||
|
"row": 0, |
||||
|
"order": 1, |
||||
|
"width": 1, |
||||
|
"entityName": "Acme.Catalog.Product", |
||||
|
"chart": { |
||||
|
"chartType": "bar", |
||||
|
"xAxis": { "property": "Name" }, |
||||
|
"yAxis": [ |
||||
|
{ "aggregation": "sum", "property": "StockCount", "label": "Stock" } |
||||
|
] |
||||
|
} |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
## Visualization Types |
||||
|
|
||||
|
The current dashboard visualization types are: |
||||
|
|
||||
|
* `chart` |
||||
|
* `list` |
||||
|
* `numberContainer` |
||||
|
|
||||
|
### Chart |
||||
|
|
||||
|
Chart visualizations define: |
||||
|
|
||||
|
* `chartType`: `bar`, `line`, `pie`, or `donut` |
||||
|
* `xAxis` |
||||
|
* one or more `yAxis` aggregation series |
||||
|
* optional `maxItems` |
||||
|
* optional `showRecordCount` |
||||
|
* optional bar orientation |
||||
|
|
||||
|
### List |
||||
|
|
||||
|
List visualizations define: |
||||
|
|
||||
|
* `fields` |
||||
|
* optional `sortBy` |
||||
|
* `maxRows` |
||||
|
* `rowHeight` |
||||
|
* optional `colorBy` |
||||
|
|
||||
|
### Number Container |
||||
|
|
||||
|
Number containers hold one or more number items. Each item can define: |
||||
|
|
||||
|
* `aggregation` |
||||
|
* `aggregationProperty` |
||||
|
* `format` |
||||
|
* `color` |
||||
|
* entity-specific filters and global date filter linkage |
||||
|
* click-through behavior |
||||
|
|
||||
|
The sample descriptor above combines a number container and a chart in the same `Inventory Overview` page. |
||||
|
|
||||
|
## Filters and Interactivity |
||||
|
|
||||
|
Dashboards support three filter layers: |
||||
|
|
||||
|
* `globalFilters` for page-level controls such as date range |
||||
|
* visualization-level `filter` for fixed query constraints |
||||
|
* visualization `userFilters` for interactive filtering exposed to the runtime user |
||||
|
|
||||
|
Other useful dashboard interaction fields include: |
||||
|
|
||||
|
* `globalDateFilterProperty` |
||||
|
* `showDescriptionAsTooltip` |
||||
|
* `clickToSeeRecords` |
||||
|
|
||||
|
These options let a dashboard stay compact while still allowing drill-down behavior in the runtime. |
||||
|
|
||||
|
## Runtime Shape |
||||
|
|
||||
|
The runtime definition exposed to React is grouped by rows and items, even though the stored descriptor is flat. The React runtime uses: |
||||
|
|
||||
|
* `useDashboardDefinition` |
||||
|
* `useDashboardData` |
||||
|
* `GET /api/low-code/ui/dashboards/{pageName}` |
||||
|
* `POST /api/low-code/dashboards/{pageName}/data` |
||||
|
|
||||
|
See [React Runtime](react-runtime.md) for hook-level details and route integration. |
||||
|
|
||||
|
## Permissions and Menu Placement |
||||
|
|
||||
|
Dashboard pages are read-oriented. Generated dashboard page operations are **view only**, so the usual CRUD operation set does not apply. |
||||
|
|
||||
|
Dashboard pages can still: |
||||
|
|
||||
|
* appear inside a [Page Group](page-groups.md) |
||||
|
* carry a menu icon and order |
||||
|
* use explicit or generated page permission configuration |
||||
|
|
||||
|
Menu placement is handled at the page level through the normal `group` and `order` fields. |
||||
|
|
||||
|
## See Also |
||||
|
|
||||
|
* [React Runtime](react-runtime.md) |
||||
|
* [Low-Code Designer](designer.md) |
||||
|
* [Page Groups](page-groups.md) |
||||
|
* [Model Descriptor Files](model-json.md) |
||||
@ -0,0 +1,83 @@ |
|||||
|
```json |
||||
|
//[doc-seo] |
||||
|
{ |
||||
|
"Description": "Review the ABP Low-Code model health snapshot to inspect entities, pages, forms, page groups, permissions, and script assets before publishing runtime changes." |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
# Health |
||||
|
|
||||
|
The **Health** section in the Low-Code Designer is a selected-layer readiness view. It helps you review the resolved low-code model for the currently selected layer before publishing changes or relying on the generated runtime pages. |
||||
|
|
||||
|
Health works on the **resolved view of the selected layer**, not on raw JSON files. In practice, this means it shows the selected layer after lower-layer inheritance and descriptor materialization have been applied. It does not silently merge unrelated edits from another layer. To review runtime-only overrides, switch the Designer to **Runtime JSON** or use the runtime-focused [MCP Integration](mcp.md) workflow. |
||||
|
|
||||
|
## What Health Reads |
||||
|
|
||||
|
The health snapshot is a combined view of the current low-code model in the selected layer. It includes: |
||||
|
|
||||
|
* Entities |
||||
|
* Enums |
||||
|
* Page groups |
||||
|
* Pages |
||||
|
* Forms |
||||
|
* Custom permissions |
||||
|
* Effective page permission configuration |
||||
|
* Custom endpoints |
||||
|
* Script event handlers |
||||
|
* Script background jobs |
||||
|
* Script background workers |
||||
|
|
||||
|
This makes Health the best place to review cross-descriptor relationships instead of checking each designer tab in isolation. |
||||
|
|
||||
|
## What It Helps You Catch |
||||
|
|
||||
|
Health is most useful for selected-layer consistency problems that often appear only after several descriptors start referencing each other. |
||||
|
|
||||
|
Typical problem classes include: |
||||
|
|
||||
|
* Page to form mismatches, where a page references a form that does not exist or belongs to another entity |
||||
|
* Page-type configuration errors, such as a kanban page missing `groupByProperty`, a calendar page using invalid date or time fields, or a gallery page pointing to a non-image property |
||||
|
* Page-group structure problems such as missing parents, circular references, or nesting deeper than the supported depth |
||||
|
* Form layout issues where fields exist but valid placements do not |
||||
|
* Permission and page configuration mismatches that would affect runtime visibility or access |
||||
|
* Script assets that exist in the model but still need review in the broader context of entities, pages, and permissions |
||||
|
|
||||
|
Some of these issues are also enforced by runtime validation and mutation rules. Health gives you a selected-layer review point before users discover the problem in the runtime UI. |
||||
|
|
||||
|
## Use It When |
||||
|
|
||||
|
Use Health in these moments: |
||||
|
|
||||
|
* After changing entities, pages, forms, page groups, or permissions in the Designer |
||||
|
* After applying MCP-driven runtime mutations |
||||
|
* Before publishing a set of runtime changes |
||||
|
* After copying or importing source-controlled descriptors into an application |
||||
|
|
||||
|
If you automate low-code changes through [MCP Integration](mcp.md), re-read the health snapshot after apply and treat that review as part of the success criteria. |
||||
|
|
||||
|
## Health and Source-Controlled Models |
||||
|
|
||||
|
Health is not a replacement for source-controlled file validation. |
||||
|
|
||||
|
When you edit `_Dynamic/model/**/*.json` directly: |
||||
|
|
||||
|
1. Run the generated model file checker: |
||||
|
|
||||
|
```bash |
||||
|
dotnet run --project <startup-project> -- --check-lowcode-model-files |
||||
|
``` |
||||
|
|
||||
|
2. Run the required migration workflow for schema-affecting entity changes. |
||||
|
3. Open the Designer and review **Health** after the model has loaded successfully. |
||||
|
|
||||
|
The file checker catches JSON and category-level problems. Health complements it by helping you inspect the resolved model after descriptors, references, permissions, pages, and forms are all combined. |
||||
|
|
||||
|
A useful example is form layout drift: a descriptor can still pass `--check-lowcode-model-files` while a source-controlled form renders with no visible fields because its layout placements are stale. Health and runtime preview are the places to catch that class of issue. |
||||
|
|
||||
|
## See Also |
||||
|
|
||||
|
* [Low-Code Designer](designer.md) |
||||
|
* [MCP Integration](mcp.md) |
||||
|
* [Model Descriptor Files](model-json.md) |
||||
|
* [Dashboards](dashboards.md) |
||||
|
* [Page Groups](page-groups.md) |
||||
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 32 KiB |
@ -0,0 +1,158 @@ |
|||||
|
```json |
||||
|
//[doc-seo] |
||||
|
{ |
||||
|
"Description": "Connect remote MCP clients to the ABP Low-Code Designer MCP endpoint with OpenIddict client credentials tokens." |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
# MCP Integration |
||||
|
|
||||
|
> **Preview:** The low-code MCP surface is part of the preview Low-Code System. Endpoint behavior, authorization requirements, and mutation metadata may change before general availability. |
||||
|
|
||||
|
The Low-Code Designer exposes a **remote Model Context Protocol (MCP) server** from the running ABP HTTP API host. It is not a local `stdio` tool package. MCP clients connect to your application over HTTP, authenticate with an OpenIddict access token, and work against the same **runtime database-backed** model that the Designer edits in the **Runtime JSON** layer. |
||||
|
|
||||
|
Use [Model Descriptor Files](model-json.md) when you need source-controlled `_Dynamic/model/**/*.json` files. Use MCP when an agent or automation needs authenticated, structured runtime access that the Designer can immediately show. |
||||
|
|
||||
|
## Remote Endpoint |
||||
|
|
||||
|
The low-code MCP endpoint is hosted by the backend application that includes the Low-Code Designer HTTP API module: |
||||
|
|
||||
|
```text |
||||
|
https://localhost:<host-port>/mcp/low-code-designer |
||||
|
``` |
||||
|
|
||||
|
Connection details: |
||||
|
|
||||
|
* **Transport:** Streamable HTTP / HTTP MCP transport |
||||
|
* **Authentication:** `Authorization: Bearer <access-token>` |
||||
|
* **Server name:** `abp-low-code-runtime-designer` |
||||
|
* **Scope:** Runtime database-backed low-code model only |
||||
|
|
||||
|
Use the backend host URL, not the React runtime URL and not the Admin Console SPA URL. In tiered solutions, connect to the HTTP API host where the low-code designer API module is loaded. |
||||
|
|
||||
|
## Required Permissions |
||||
|
|
||||
|
The endpoint requires the Low-Code Designer permission group: |
||||
|
|
||||
|
| Permission | Required for | |
||||
|
|------------|--------------| |
||||
|
| `AbpLowCodeDesigner.Default` | Connecting and reading runtime model metadata | |
||||
|
| `AbpLowCodeDesigner.Edit` | Validating and applying runtime model changes | |
||||
|
| `AbpLowCodeDesigner.ScriptTest` | Running script dry-runs through MCP | |
||||
|
|
||||
|
Grant these permissions to the **OpenIddict application/client** that will request the token, not only to a user or role. In Admin Console, use the permission action on the OpenIddict application row and grant the required Low-Code Designer permissions for the client. |
||||
|
|
||||
|
See the [Permission Management Module](../modules/permission-management.md#permission-management-dialog) for the standard permission dialog behavior. |
||||
|
|
||||
|
## Create an Access Token |
||||
|
|
||||
|
For unattended MCP automation, create a dedicated OpenIddict application instead of reusing an interactive user token. |
||||
|
|
||||
|
1. Open **Admin Console**. |
||||
|
2. Go to **OpenIddict** > **Applications**. |
||||
|
3. Create a new application, for example `LowCodeMcpClient`. |
||||
|
4. Set **Client type** to `Confidential client`. |
||||
|
5. Set a strong **Client secret** and store it securely. |
||||
|
6. Open the **Authorization** tab and enable **Allow client credentials flow**. |
||||
|
7. Open the **Scopes** tab and allow the API scope used by the backend that hosts the low-code APIs, for example `<YourAppName>`. |
||||
|
8. Save the application. |
||||
|
9. From the application row, open **Permissions** and grant the Low-Code Designer permissions required by your MCP scenario. |
||||
|
|
||||
|
The OpenIddict application and scope management screens are described in [Creating a Client Credentials Application](../modules/openiddict-pro.md#creating-a-client-credentials-application). |
||||
|
|
||||
|
The Admin Console application screen should show the confidential client with **Allow client credentials flow** enabled: |
||||
|
|
||||
|
 |
||||
|
|
||||
|
Request an access token from the OpenIddict authority with the saved client id, client secret, and selected API scope. In non-tiered solutions this is usually the same backend host; in tiered solutions it is usually the Auth Server URL: |
||||
|
|
||||
|
```bash |
||||
|
curl -X POST "https://localhost:<auth-server-port>/connect/token" \ |
||||
|
-H "Content-Type: application/x-www-form-urlencoded" \ |
||||
|
-d "grant_type=client_credentials" \ |
||||
|
-d "client_id=LowCodeMcpClient" \ |
||||
|
-d "client_secret=<client-secret>" \ |
||||
|
-d "scope=<YourAppName>" |
||||
|
``` |
||||
|
|
||||
|
Use the returned `access_token` as the bearer token in the MCP client configuration. |
||||
|
|
||||
|
```text |
||||
|
Authorization: Bearer <access-token> |
||||
|
``` |
||||
|
|
||||
|
## Connect From An MCP Client |
||||
|
|
||||
|
For ABP AI Management or another MCP client that supports remote HTTP servers, create a server entry with: |
||||
|
|
||||
|
| Setting | Value | |
||||
|
|---------|-------| |
||||
|
| Name | `ABP Low-Code Designer` | |
||||
|
| Transport | HTTP / Streamable HTTP | |
||||
|
| Endpoint URL | `https://localhost:<host-port>/mcp/low-code-designer` | |
||||
|
| Authentication | Bearer token | |
||||
|
| Token | The OpenIddict access token created for `LowCodeMcpClient` | |
||||
|
|
||||
|
For clients that use JSON configuration, the same connection usually looks like this: |
||||
|
|
||||
|
```json |
||||
|
{ |
||||
|
"mcpServers": { |
||||
|
"abp-low-code-designer": { |
||||
|
"type": "streamable-http", |
||||
|
"url": "https://localhost:<host-port>/mcp/low-code-designer", |
||||
|
"headers": { |
||||
|
"Authorization": "Bearer <access-token>" |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
Client configuration names differ by product. If your client uses `http`, `streamableHttp`, or a separate `authorization` field, keep the same endpoint URL and bearer token. |
||||
|
|
||||
|
## Runtime-Only Scope |
||||
|
|
||||
|
The MCP surface is **runtime-only**: |
||||
|
|
||||
|
* It always reads and writes the database-backed runtime model. |
||||
|
* It does not choose between source-controlled and runtime layers at call time. |
||||
|
* It does not edit source-controlled `_Dynamic/model/**/*.json` descriptor files. |
||||
|
* It does not replace the normal migration workflow for source-controlled model changes. |
||||
|
|
||||
|
Runtime MCP writes can extend descriptors that originate from lower layers, but inherited storage details remain restricted. For example, runtime edits can typically change display labels and runtime-owned validators, but cannot change inherited property type, database mapping, required or unique behavior, foreign key settings, or file and image storage options. |
||||
|
|
||||
|
## Safe Automation Workflow |
||||
|
|
||||
|
After the MCP client connects, let it discover the available capabilities from the server. The normal write workflow should still be conservative: |
||||
|
|
||||
|
1. Read the current runtime item before planning a change. |
||||
|
2. Fetch the current mutation metadata and concurrency stamp. |
||||
|
3. Build a small mutation batch that touches only the changed semantic paths. |
||||
|
4. Validate the batch before writing. |
||||
|
5. Apply the validated batch. |
||||
|
6. Re-read the changed item and review [Health](health.md). |
||||
|
|
||||
|
Treat validation and health review as part of the normal MCP workflow. They are especially important for forms, dashboard layouts, relation changes, and delete operations that can leave broken references. |
||||
|
|
||||
|
## Troubleshooting |
||||
|
|
||||
|
| Problem | Check | |
||||
|
|---------|-------| |
||||
|
| `401 Unauthorized` | The MCP client is not sending `Authorization: Bearer <access-token>`, the token expired, or the token was issued by an authority that the backend host does not trust. | |
||||
|
| `403 Forbidden` | The OpenIddict client does not have the required `AbpLowCodeDesigner.*` permissions. | |
||||
|
| Client cannot connect | Verify that the backend host is running and that the URL points to `/mcp/low-code-designer` on the HTTP API host. | |
||||
|
| Writes fail | Grant `AbpLowCodeDesigner.Edit`, refresh the token, and rebuild the change from the latest runtime metadata. | |
||||
|
| Script dry-run fails with authorization | Grant `AbpLowCodeDesigner.ScriptTest` and generate a new token. | |
||||
|
|
||||
|
## Designer and MCP Together |
||||
|
|
||||
|
Use the Designer when you want interactive editing and visual review. Use MCP when a remote agent or automation needs authenticated, repeatable runtime changes. After MCP-driven changes, reopen the relevant Designer section or review [Health](health.md) before treating the runtime model as ready. |
||||
|
|
||||
|
## See Also |
||||
|
|
||||
|
* [Low-Code Designer](designer.md) |
||||
|
* [Health](health.md) |
||||
|
* [Model Descriptor Files](model-json.md) |
||||
|
* [OpenIddict Module (Pro)](../modules/openiddict-pro.md#creating-a-client-credentials-application) |
||||
|
* [Permission Management Module](../modules/permission-management.md#permission-management-dialog) |
||||
@ -0,0 +1,123 @@ |
|||||
|
```json |
||||
|
//[doc-seo] |
||||
|
{ |
||||
|
"Description": "Define ABP Low-Code page groups to organize runtime pages into dynamic menu folders with ordering, icons, and nesting." |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
# Page Groups |
||||
|
|
||||
|
Page groups are dynamic menu folders used by low-code pages. They let you organize runtime pages into nested navigation structures without hardcoding menu items in the frontend. |
||||
|
|
||||
|
## What a Page Group Stores |
||||
|
|
||||
|
A page group descriptor stores: |
||||
|
|
||||
|
| Field | Description | |
||||
|
|-------|-------------| |
||||
|
| `name` | Stable identifier used by pages in their `group` field | |
||||
|
| `title` | Display label shown in the runtime menu | |
||||
|
| `icon` | CSS class string for the menu icon | |
||||
|
| `order` | Sort order among sibling groups | |
||||
|
| `parent` | Optional parent group name for nesting | |
||||
|
|
||||
|
If a group does not define an icon, the runtime menu uses a folder-style default icon. |
||||
|
|
||||
|
## How Pages Use Groups |
||||
|
|
||||
|
Pages reference a group by name: |
||||
|
|
||||
|
```json |
||||
|
{ |
||||
|
"name": "products", |
||||
|
"title": "Products", |
||||
|
"type": "dataGrid", |
||||
|
"entityName": "Acme.Catalog.Product", |
||||
|
"group": "inventory" |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
If `group` is omitted, the page becomes a top-level runtime menu item. |
||||
|
|
||||
|
A group is only a container. It does not define page data, routes, or entity behavior by itself. |
||||
|
|
||||
|
## Nesting Rules |
||||
|
|
||||
|
Groups can nest by pointing `parent` to another page group name. |
||||
|
|
||||
|
Important constraints: |
||||
|
|
||||
|
* Group nesting cannot be circular. |
||||
|
* Maximum depth is `3` levels. |
||||
|
* Root groups are groups whose `parent` is empty. |
||||
|
|
||||
|
This keeps runtime navigation predictable and avoids unbounded nesting in the generated menu. |
||||
|
|
||||
|
## Visibility and Permissions |
||||
|
|
||||
|
Page groups do not replace page permissions. Runtime visibility still depends on the pages inside the group. |
||||
|
|
||||
|
In practice: |
||||
|
|
||||
|
* Pages are added to the runtime menu only if the current user can access them. |
||||
|
* Groups are containers for those visible pages. |
||||
|
* Groups without visible page descendants are omitted from the runtime root menu. |
||||
|
|
||||
|
This means page permissions remain the real security boundary, while page groups control menu organization. |
||||
|
|
||||
|
## Split Descriptor Example |
||||
|
|
||||
|
In split descriptor projects, a page group file in `pageGroups/` stores **one descriptor object**, not a wrapper document: |
||||
|
|
||||
|
```json |
||||
|
{ |
||||
|
"name": "inventory-insights", |
||||
|
"title": "Insights", |
||||
|
"icon": "fa-solid fa-folder-tree", |
||||
|
"order": 20, |
||||
|
"parent": "inventory" |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
For example, `pageGroups/inventory-insights.json` would use that shape directly. |
||||
|
|
||||
|
If you are looking at the logical aggregate model instead of split files, the same data appears under the top-level `pageGroups` array: |
||||
|
|
||||
|
```json |
||||
|
{ |
||||
|
"pageGroups": [ |
||||
|
{ |
||||
|
"name": "inventory", |
||||
|
"title": "Inventory", |
||||
|
"icon": "fa-solid fa-boxes-stacked", |
||||
|
"order": 10 |
||||
|
}, |
||||
|
{ |
||||
|
"name": "inventory-insights", |
||||
|
"title": "Insights", |
||||
|
"icon": "fa-solid fa-folder-tree", |
||||
|
"order": 20, |
||||
|
"parent": "inventory" |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
In split descriptor projects, page groups belong to the `pageGroups/` descriptor category. |
||||
|
|
||||
|
## Icon Guidance |
||||
|
|
||||
|
Use CSS class strings for icons, for example: |
||||
|
|
||||
|
* `fa-solid fa-folder` |
||||
|
* `fa-solid fa-folder-tree` |
||||
|
* `fa-solid fa-boxes-stacked` |
||||
|
|
||||
|
Do not treat `icon` as an image URL or file path. |
||||
|
|
||||
|
## See Also |
||||
|
|
||||
|
* [Dashboards](dashboards.md) |
||||
|
* [React Runtime](react-runtime.md) |
||||
|
* [Low-Code Designer](designer.md) |
||||
|
* [Model Descriptor Files](model-json.md) |
||||