Declare
Entity shape, validation, display metadata, CRUD mask, commands, queries.
Engineering reference · 01
The application author declares business meaning once. cwe-gen uses Roslyn to emit the repeatable server and client contract, and the UI binds one typed operations interface instead of rebuilding CRUD, transport, and authorization by hand.
[CxEntity]
[CxDisplayTemplate("{Name}")]
[CxAIEntity(Searchable = true)]
public partial class Department
{
[Required, StringLength(100)]
public string Name { get; private set; }
public bool IsActive { get; private set; }
}
The important split is ownership. Entity and page intent belong to the application. Repeated infrastructure belongs to the generator. Security remains a server decision even when the client reflects it.
Entity shape, validation, display metadata, CRUD mask, commands, queries.
Roslyn reads types and members; diagnostics reject ambiguous or unsafe declarations.
DTOs, strong ids, handlers, endpoints, permissions, operations, AI descriptors.
The page injects IDepartmentOperations; the correct host implementation is selected.
Build, tests, composition analysis, regeneration, and clean-tree convergence.
Attributes state validation, editing, display, hierarchy, and AI discoverability next to the property they describe. Domain methods keep business transitions explicit.
This is normal C# under the Server project’s Entities/ area. It is the durable source of truth and the only place changed when the business object evolves.
CxAIEntity opts the record into permission-gated AI search.[CxEntity]
[CxDisplayTemplate("{Name}")]
[CxAIEntity(
Summary = "A department in the company.",
Searchable = true,
SearchRoute = "/departments")]
public partial class Department
{
[NonEditable]
public DepartmentId Id { get; private set; }
[Required(ErrorMessage = "Department name is required.")]
[StringLength(100)]
[Display(Name = "Name", Order = 10)]
public string Name { get; private set; } = "";
[Display(Name = "Parent department")]
public DepartmentId? ParentId { get; private set; }
[DefaultValue(true)]
public bool IsActive { get; private set; } = true;
public void Rename(string name) => Name = name;
public void Deactivate() => IsActive = false;
}
Framework/ is generated output. If generated behavior is wrong, change the entity, command, query, or generator—never patch a generated file.
The contract and DTOs live where the browser can reference them. The Server implementation dispatches directly; the browser implementation performs typed HTTP calls. Components depend on neither.
IDepartmentOperationsThe interface inherits the uniform CRUD surface used by connected UI components and also exposes entity-specific generated queries.
DepartmentOperations.DepartmentClientOperations.HttpClient.public interface IDepartmentOperations
: ICxCrudOperations<DepartmentViewModel,
DepartmentEditModel, DepartmentId>
{
Task<CxResult<DepartmentId>>
CreateDepartmentAsync(DepartmentEditModel model, CancellationToken ct);
Task<CxPagedResult<DepartmentViewModel>>
GetDepartmentListAsync(CxQueryOptions options, CancellationToken ct);
Task<DepartmentViewModel?>
GetDepartmentByIdAsync(DepartmentId id, CancellationToken ct);
Task<IReadOnlyList<CxAILookupCandidate>>
FindDepartmentByNameAsync(string[] nameParts, int maxResults, CancellationToken ct);
}
Endpoints authenticate the route group and delegate. The generated handler independently resolves the current caller’s effective permission set before touching the database.
var group = endpoints
.MapGroup(CxEntityCatalog.Department.Route)
.WithTags("Department")
.RequireAuthorization();
group.MapPost("/", async (
DepartmentEditModel model,
IDepartmentOperations operations,
CancellationToken ct) =>
await operations.CreateDepartmentAsync(model, ct));
var permissions =
await permissionService.GetPermissionSetAsync(ct);
if (!permissions.IsGranted(DepartmentPermissions.Create))
return CxResult<DepartmentId>.Failure(
new CxError("forbidden", "Not authorized."));
var entity = Department.Create(request.DepartmentEditModel);
db.Set<Department>().Add(entity);
await db.SaveChangesAsync(ct);
Hiding the New button is user experience. This handler check is the security boundary. Calling the endpoint manually or modifying client state cannot bypass it.
The page declares columns, editing fields, and permission keys. Paging, sorting, searching, CRUD dialogs, typed results, and host selection are already owned by the connected component and generated contract.
This is where engineering judgment belongs: which facts matter, which actions make sense, and how the user edits the business object.
DepartmentEditModel.@inject IDepartmentOperations Departments
<CweEntityGrid
TRow="DepartmentViewModel"
TEdit="DepartmentEditModel"
TId="DepartmentId"
Operations="Departments"
Title="Departments"
CreatePermission="@DepartmentPermissions.Create"
EditPermission="@DepartmentPermissions.Update"
DeletePermission="@DepartmentPermissions.Delete">
<Columns>
<CweGridTextColumn Property="d => d.Name" />
<CweGridBoolColumn TRow="DepartmentViewModel"
Property="d => d.IsActive" Title="Status" />
</Columns>
<FormTemplate Context="m">
<CweTextBox Label="Name" @bind-Value="m.Name" Required />
<CweCheckbox Text="Active" @bind-Value="m.IsActive" />
</FormTemplate>
</CweEntityGrid>
Strong ids, view/edit models, endpoints, handlers, client operations, and permission catalogs.
Authenticated endpoints plus per-handler checks against the server-resolved caller.
Invalid component parameters, raw self-calls, missing validation, and unreadable grids surface early.
Regeneration must leave the tracked generated tree unchanged; drift fails CI.