DataGraph
Overview
DataGraph is the root container for any data-editing or data-viewing page. It wires together a DataSet, a GraphDefinition built from a GraphBuilder factory, entity metadata, and user access control, then cascades a DataGraphContext to all descendant components. PropertyComponent, PropertyGroup, and custom components all read from that context — they do not accept data directly.
When DataGraph initializes, it resolves metadata for the root entity, evaluates the current user's access, filters the graph definition to only the paths the user is permitted to see, and then either loads an existing entity or creates a new one. Child content does not render until that sequence completes.
Parameters
Builder (required)
[Parameter] public required Func<GraphBuilder> Builder { get; set; }
A factory function called once per cache scope to define the data shape. The returned GraphBuilder is seeded with the root entity name and populated with the page's properties, navigations, and references. The composed GraphDefinition is cached by GraphComposer; the factory is not re-invoked unless the cache scope changes.
DataSet
[Parameter] public DataSet? DataSet { get; set; }
A pre-created DataSet to use. When omitted or null, DataGraph clears any previously initialized context and renders nothing. The caller is responsible for creating the DataSet — typically in OnInitialized — and passing it in. See DATASETS.md.
EntityGuid
[Parameter] public Guid? EntityGuid { get; set; }
The GUID of the entity to load. When this has a value, DataGraph calls DataSet.LoadAsync(guid) during initialization. When null, it calls DataSet.CreateAsync() to begin a new entity creation flow. Changing this parameter after initial render triggers re-initialization.
DisableLoadBoundary
[Parameter] public bool DisableLoadBoundary { get; set; } = false;
When false (the default), DataGraph wraps its child content in a DataGraphLoadBoundry that suppresses rendering until IsInitialized is true. Set to true to skip that wrapper and render child content immediately, checking context.IsInitialized manually. Useful when you need fine-grained control over what renders during loading.
ChildContent
[Parameter] public RenderFragment? ChildContent { get; set; }
The content rendered inside DataGraph after initialization. All PropertyComponent and custom component instances go here.
DisplayMode
[Parameter] public DisplayMode DisplayMode { get; set; } = DisplayMode.Edit;
Propagated through DataGraphContext to all descendant PropertyComponent instances. Controls whether components render their edit or view variants. If model access or RBAC blocks updates, DataGraph uses DisplayMode.View.
DataGraphContext
DataGraphContext is the cascading value produced by DataGraph and consumed by any descendant component via [CascadingParameter]. It carries everything a child component needs to render, read, or write data without taking parameters directly.
Properties
| Property | Type | Description |
|---|---|---|
DataSet |
IDataSet |
The active data set backing this graph. |
Definition |
GraphDefinition |
The access-filtered graph definition. Paths the user cannot see are removed before this is set. |
RootEntityMetaData |
EntityMetaData |
Metadata for the root entity resolved from IModelProvider. |
DisplayMode |
DisplayMode |
The effective display mode after access control evaluation. |
DynamicComponentProvider |
DynamicComponentProvider |
Resolves which Blazor component type to render for a given property metadata. |
Access |
UserAccess? |
The current user's access snapshot. null when RBAC is not configured. |
IsInitialized |
bool |
true after LoadAsync or CreateAsync has completed successfully. |
IsEntityAccessible |
bool |
true when model access and RBAC allow reading the root entity. |
Methods
GetPropertyPermission
public PropertyPermissionLevel GetPropertyPermission(string path)
Combines model access and RBAC for each part of a dotted path. Model restrictions also apply to administrators.
IsMethodAllowed
public bool IsMethodAllowed(string methodName)
Returns whether the named method is permitted on the root entity. When Access is null or HasFullAccess is true, returns true. Otherwise delegates to EntityAccess.IsMethodAllowed for the root entity.
CanWrite
Use CanWrite(path) to check model access, RBAC, and record business rules before editing.
DataGraphLoadBoundry
DataGraphLoadBoundry is a thin Blazor component that reads DataGraphContext via [CascadingParameter] and suppresses its child content until context.IsInitialized is true.
@if (Context?.IsInitialized == true)
{
@ChildContent?.Invoke(Context.DataSet)
}
DataGraph wraps its ChildContent in this component by default. The practical effect is that no PropertyComponent or custom child renders until the entity load or create call has returned successfully. This prevents components from attempting to read property values from an empty data set.
When DisableLoadBoundary is true, the DataGraphLoadBoundry wrapper is skipped entirely. Child content renders on the first render pass and is responsible for checking context.IsInitialized before reading from the data set.
Display Modes
DisplayMode is a flags enum:
| Value | Integer | Meaning |
|---|---|---|
View |
1 |
Read-only display. PropertyComponent renders its view variant (plain text, links, formatted values). |
Edit |
2 |
Editable fields. PropertyComponent renders its input variant. |
DataGrid |
4 |
Optimized for inline grid cells. Used by data grid components, not typically set on a standalone page. |
Pass DisplayMode.View to DataGraph to make every PropertyComponent on the page render in read-only mode without any additional configuration per component. This is the simplest way to build a detail/summary view from the same graph definition as an edit page.
DataGraph checks ApiAccess, RBAC, and business rules, including display-mode overrides.
Edits use PATCH and need Update, list access, and field-write permission. Creating records needs model and RBAC create access.
Access Control Integration
DataGraph loads UserAccess through IAccessProvider. GraphDefinition.FilterByAccess removes paths blocked by the model or RBAC.
Model restrictions also apply to administrators. DataGraphContext.Definition stores the filtered graph.
Consequences:
- The OData query built from the definition omits hidden properties entirely. The server never sends data the user cannot see.
PropertyComponentinstances whose paths were removed from the definition find no metadata and render nothing.GetPropertyPermissionon a path that survived filtering still returns the exact permission level (ReadorWrite) from the user's access snapshot.
If the user has no access at all to the root entity, DataGraph sets accessDenied internally and does not produce a context. Child content is not rendered.
Code Examples
1. Minimal Edit Page
Two properties on an Order entity. DataSet is created in OnInitialized and passed to DataGraph. EntityGuid comes from the route parameter.
@page "/orders/{OrderGuid:guid}"
@page "/orders/new"
@using Benevia.Core.Blazor
@using Benevia.Core.Client.DataSets
@using Benevia.Core.Client.GraphDefinitions.Builders
@using Benevia.Core.Client.OData
@inject IODataClient OData
<DataGraph Builder="@BuildGraph" DataSet="@_dataSet" EntityGuid="@OrderGuid">
<PropertyComponent Path="OrderNumber" />
<PropertyComponent Path="Status" />
</DataGraph>
using Benevia.Core.Blazor;
using Benevia.Core.Client.DataSets;
using Benevia.Core.Client.GraphDefinitions.Builders;
using Benevia.Core.Client.OData;
using Microsoft.AspNetCore.Components;
namespace MyApp.Pages.Orders;
public partial class OrderEditPage : ComponentBase
{
[Parameter] public Guid? OrderGuid { get; set; }
[Inject] public required IODataClient OData { get; set; }
private DataSet _dataSet = null!;
private static GraphBuilder BuildGraph() => new GraphBuilder("Order")
.Property("OrderNumber")
.Property("Status");
protected override void OnInitialized()
=> _dataSet = new DataSet(OData, BuildGraph().Build());
}
2. View-Only Page
Pass DisplayMode.View to render all properties as read-only. No other changes are needed — each PropertyComponent switches to its view variant automatically.
@page "/orders/{OrderGuid:guid}/view"
@using Benevia.Core.Blazor
@using Benevia.Core.Client.DataSets
@using Benevia.Core.Client.GraphDefinitions.Builders
@using Benevia.Core.Client.OData
@inject IODataClient OData
<DataGraph Builder="@BuildGraph" DataSet="@_dataSet" EntityGuid="@OrderGuid"
DisplayMode="DisplayMode.View">
<PropertyComponent Path="OrderNumber" />
<PropertyComponent Path="Status" />
<PropertyComponent Path="Customer" />
<PropertyComponent Path="TotalAmount" />
</DataGraph>
using Benevia.Core.Blazor;
using Benevia.Core.Client.DataSets;
using Benevia.Core.Client.GraphDefinitions.Builders;
using Benevia.Core.Client.OData;
using Microsoft.AspNetCore.Components;
namespace MyApp.Pages.Orders;
public partial class OrderDetailPage : ComponentBase
{
[Parameter] public Guid? OrderGuid { get; set; }
[Inject] public required IODataClient OData { get; set; }
private DataSet _dataSet = null!;
private static GraphBuilder BuildGraph() => new GraphBuilder("Order")
.Property("OrderNumber")
.Property("Status")
.Reference("Customer")
.Property("TotalAmount");
protected override void OnInitialized()
=> _dataSet = new DataSet(OData, BuildGraph().Build());
}
3. New Entity Flow
When EntityGuid is null, DataGraph calls DataSet.CreateAsync() automatically. No special handling is needed in the page — just omit EntityGuid or leave it null.
@page "/orders/new"
@using Benevia.Core.Blazor
@using Benevia.Core.Client.DataSets
@using Benevia.Core.Client.GraphDefinitions.Builders
@using Benevia.Core.Client.OData
@inject IODataClient OData
<DataGraph Builder="@BuildGraph" DataSet="@_dataSet">
<PropertyComponent Path="OrderNumber" />
<PropertyComponent Path="Status" />
<PropertyComponent Path="Customer" />
</DataGraph>
using Benevia.Core.Blazor;
using Benevia.Core.Client.DataSets;
using Benevia.Core.Client.GraphDefinitions.Builders;
using Benevia.Core.Client.OData;
using Microsoft.AspNetCore.Components;
namespace MyApp.Pages.Orders;
public partial class NewOrderPage : ComponentBase
{
[Inject] public required IODataClient OData { get; set; }
private DataSet _dataSet = null!;
private static GraphBuilder BuildGraph() => new GraphBuilder("Order")
.Property("OrderNumber")
.Property("Status")
.Reference("Customer");
protected override void OnInitialized()
=> _dataSet = new DataSet(OData, BuildGraph().Build());
}
4. Loading an Existing Entity from a Route Parameter
Declare two routes — one for editing an existing entity by GUID and one for creation — and bind the route segment to EntityGuid. DataGraph calls LoadAsync or CreateAsync based on whether EntityGuid has a value.
@page "/products/{ProductGuid:guid}"
@page "/products/new"
@using Benevia.Core.Blazor
@using Benevia.Core.Client.DataSets
@using Benevia.Core.Client.GraphDefinitions.Builders
@using Benevia.Core.Client.OData
@inject IODataClient OData
<DataGraph Builder="@BuildGraph" DataSet="@_dataSet" EntityGuid="@ProductGuid">
<PropertyComponent Path="Name" />
<PropertyComponent Path="Sku" />
<PropertyComponent Path="UnitPrice" />
<PropertyComponent Path="Category" />
</DataGraph>
using Benevia.Core.Blazor;
using Benevia.Core.Client.DataSets;
using Benevia.Core.Client.GraphDefinitions.Builders;
using Benevia.Core.Client.OData;
using Microsoft.AspNetCore.Components;
namespace MyApp.Pages.Products;
public partial class ProductPage : ComponentBase
{
[Parameter] public Guid? ProductGuid { get; set; }
[Inject] public required IODataClient OData { get; set; }
private DataSet _dataSet = null!;
private static GraphBuilder BuildGraph() => new GraphBuilder("Product")
.Property("Name")
.Property("Sku")
.Property("UnitPrice")
.Reference("Category");
protected override void OnInitialized()
=> _dataSet = new DataSet(OData, BuildGraph().Build());
}
5. Accessing DataGraphContext from a Custom Child Component
Declare a [CascadingParameter] of type DataGraphContext in any descendant component. The context is available as soon as the component renders — which, because of DataGraphLoadBoundry, is after IsInitialized is true.
@* OrderStatusBadge.razor *@
@using Benevia.Core.Blazor
@using Benevia.Core.Client.Access
@if (Context is not null)
{
var level = Context.GetPropertyPermission("Status");
@if (level != PropertyPermissionLevel.Hide)
{
var status = Context.DataSet.Get<string>("Status");
<MudChip Color="@GetColor(status)">@status</MudChip>
}
}
@code {
[CascadingParameter] public DataGraphContext? Context { get; set; }
private Color GetColor(string? status) => status switch
{
"Open" => Color.Primary,
"Closed" => Color.Default,
"Cancelled" => Color.Error,
_ => Color.Default
};
}
Place the component anywhere inside a DataGraph:
<DataGraph Builder="@BuildGraph" DataSet="@_dataSet" EntityGuid="@OrderGuid">
<OrderStatusBadge />
<PropertyComponent Path="OrderNumber" />
</DataGraph>
6. Using DisableLoadBoundary with a Manual IsInitialized Check
When you set DisableLoadBoundary="true", child content renders immediately on the first pass. Check context.IsInitialized before reading values or rendering data-dependent UI.
@page "/orders/{OrderGuid:guid}"
@using Benevia.Core.Blazor
@using Benevia.Core.Client.DataSets
@using Benevia.Core.Client.GraphDefinitions.Builders
@using Benevia.Core.Client.OData
@inject IODataClient OData
<DataGraph Builder="@BuildGraph" DataSet="@_dataSet" EntityGuid="@OrderGuid"
DisableLoadBoundary="true">
<OrderLoadingShell />
</DataGraph>
@* OrderLoadingShell.razor *@
@using Benevia.Core.Blazor
@if (Context?.IsInitialized == true)
{
<PropertyComponent Path="OrderNumber" />
<PropertyComponent Path="Status" />
}
else
{
<MudProgressLinear Indeterminate="true" />
}
@code {
[CascadingParameter] public DataGraphContext? Context { get; set; }
}
Use this pattern when the default loading boundary does not give you enough control over the loading state UI — for example, when you need to render a skeleton layout or show partial content while the entity is being fetched.