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 the current user lacks Create-level permission on the root entity, DataGraph overrides this to DisplayMode.View regardless of what is passed.

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 Access is null (no RBAC) or when the user has any access to the root entity.

Methods

GetPropertyPermission

public PropertyPermissionLevel GetPropertyPermission(string path)

Returns the effective permission level for a dotted property path. When Access is null or HasFullAccess is true, returns PropertyPermissionLevel.Write. Otherwise walks the navigation chain to find the owning entity, checks whether both the owning entity and any reference target entity are accessible, and returns the level from EntityAccess. Returns PropertyPermissionLevel.Hide if any entity in the chain is inaccessible.

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.

ResolvePropertyOwner

public EntityMetaData ResolvePropertyOwner(string path)

Walks the navigation chain described by the dotted path and returns the EntityMetaData that owns the leaf property. For a path like PrimaryContact.MailingAddress.Street, this returns the metadata for MailingAddress. Used internally by GetPropertyPermission but also available to custom components that need to resolve ownership for display logic.

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.

If the current user has less than EntityPermissionLevel.Create access to the root entity, DataGraph forces DisplayMode.View regardless of the parameter value.

Access Control Integration

During initialization, DataGraph fetches the current user's UserAccess via IAccessProvider and passes it through GraphDefinition.FilterByAccess. This removes from the definition every property path the user is not permitted to see. The filtered definition — not the original — is stored in DataGraphContext.Definition.

Consequences:

  • The OData query built from the definition omits hidden properties entirely. The server never sends data the user cannot see.
  • PropertyComponent instances whose paths were removed from the definition find no metadata and render nothing.
  • GetPropertyPermission on a path that survived filtering still returns the exact permission level (Read or Write) 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.