|
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) |
|||