DataSets

A DataSet turns a graph definition into live entity state. It manages a local copy of the server data, tracks changes, validates access against the graph contract, and coordinates PATCH updates. You read values with Get<T>(path), write with SetAsync(path, value), and the DataSet keeps everything in sync.

Creating a DataSet

A DataSet requires an IODataClient (for server communication) and a GraphDefinition (the data contract):

using Benevia.Core.Client.DataSets;
using Benevia.Core.Client.OData;

@inject IODataClient OData

private DataSet _dataSet = null!;

protected override void OnInitialized()
    => _dataSet = new DataSet(OData, CustomerGraph);

The DataSet does not load data on construction. You must explicitly load or create an entity.

Loading and Creating Entities

Load an existing entity

var response = await _dataSet.LoadAsync(customerGuid);

if (response.IsSuccess(out var success, out var error))
{
    // Entity loaded — data is now available via Get<T>()
}
else
{
    // Handle error
    var exception = error.Exception;
}

LoadAsync sends a GET request using the OData query generated from the graph, then stores the response as local state.

Create a new entity

var response = await _dataSet.CreateAsync();

if (response.IsSuccess(out var success, out var error))
{
    // New entity created on the server — Guid is now available
    var newGuid = _dataSet.Guid;
}

CreateAsync sends a POST request to create the entity on the server and populates the DataSet with the response.

In practice, you usually don't call LoadAsync or CreateAsync directly. The DataGraph component handles this:

<DataGraph Definition="@Graph" DataSet="@_dataSet" EntityGuid="@CustomerGuid">
    @* If CustomerGuid has a value → LoadAsync, if null → CreateAsync *@
</DataGraph>

Reading Values

Get<T>(path) reads a value from the local state using a dot-separated property path:

string? id = _dataSet.Get<string>("Id");
string? email = _dataSet.Get<string>("PrimaryContact.PrimaryEmail");
string? city = _dataSet.Get<string>("PrimaryContact.MailingAddress.City");
bool isSameAddress = _dataSet.Get<bool>("PrimaryContact.IsMailingAddressSameAsPhysical");

The path must be declared in the graph. Accessing an undeclared path throws InvalidOperationException.

Writing Values

SetAsync(path, value) performs an optimistic update:

sequenceDiagram
    participant UI as PropertyComponent
    participant DS as DataSet
    participant API as Server
    UI->>DS: SetAsync("Title", "New")
    DS->>DS: Mutate local state
    DS->>UI: Changed event (immediate)
    DS->>API: PATCH /api/Customer(guid)
    API-->>DS: Response (computed values, validation, read-only)
    DS->>DS: Replace local with server response
    DS->>UI: Changed event (server truth)
  1. The local state is mutated immediately (so the UI reflects the change)
  2. A PATCH request is sent to the server
  3. On success, the server's response replaces local state (which may include computed values, changed read-only flags, or validation messages)
  4. On error, the local data is preserved so the user doesn't lose their input
var response = await _dataSet.SetAsync("PrimaryContact.PrimaryEmail", "new@example.com");

if (response.IsSuccess(out var success, out var error))
{
    // Server accepted the change
    // Local state now reflects any server-side computed values
}
else
{
    // Server rejected — user's input is preserved, error is available
}

Computed values and side effects

After a successful PATCH, the server returns the full graph-shaped response. This means:

  • Computed properties update automatically (e.g., FullName recomputes when FirstName or LastName changes)
  • Read-only flags may change (e.g., a status field becomes read-only after approval)
  • Validation messages are updated for all affected properties

You don't need to manually refresh — SetAsync keeps everything consistent.

Change Notifications

The DataSet fires a Changed event whenever data changes (after SetAsync, LoadAsync, RefreshAsync, or collection operations):

_dataSet.Changed += (sender, args) =>
{
    if (args.AffectsPath("PrimaryContact.IsMailingAddressSameAsPhysical"))
    {
        // Toggle visibility of physical address fields
        InvokeAsync(StateHasChanged);
    }
};

ChangedArgs

Method Returns true when
AffectsPath("Title") Title changed directly, or a blanket change occurred
AffectsPathOrDescendant("PrimaryContact") Any property under PrimaryContact changed

A blanket change (ChangedPaths == null) means the entire entity may have changed — this happens on full loads and refreshes. PropertyComponent handles change subscriptions internally, so you only need to subscribe when you have custom conditional logic.

Collections

For one-to-many relationships (like a Customer's additional contacts), use GetCollection:

var contacts = _dataSet.GetCollection("AdditionalContacts", "AdditionalContact");

Reading collection items

int count = contacts.Count;
IDataSet firstItem = contacts[0];
string? role = firstItem.Get<string>("Role.Name");

// Find by GUID
IDataSet? specificItem = contacts.GetByGuid(someGuid);

Each collection item is an IDataSet with the same Get<T>/SetAsync contract.

Adding items

using System.Text.Json.Nodes;

var response = await contacts.AddAsync(new JsonObject
{
    ["RoleGuid"] = someRoleGuid
});

Removing items

await contacts.RemoveAsync(itemGuid);

Collection tracking

Collection items are tracked by GUID. The collection object stays wired to the parent DataSet — when the parent refreshes, collections update automatically through the same object reference.

User Prompts and Validation

The server returns validation messages (user prompts) keyed by property path. Access them through the DataSet:

// Get prompts for a specific property
UserPrompt[] emailPrompts = _dataSet.GetUserPrompts("PrimaryContact.PrimaryEmail");

foreach (var prompt in emailPrompts)
{
    // prompt.Level: 0 = Error, 1 = Warning, 2 = Info
    // prompt.Text: The validation message
}

// Get all prompts (no path filter)
UserPrompt[] allPrompts = _dataSet.GetUserPrompts();

PropertyComponent renders these automatically as helper text on the corresponding control, so you typically don't need to access prompts directly.

Read-Only Status

The server can mark individual records as read-only via RecordReadOnly:

bool isReadOnly = _dataSet.IsReadOnly("PrimaryContact.PrimaryEmail");

Edit components automatically disable themselves when IsReadOnly returns true.

Refreshing

Force a full reload from the server:

await _dataSet.RefreshAsync();

This re-fetches the entity using the original graph query and replaces all local state. Collections are updated through the same object references.

GUID

After loading or creating an entity, the root entity's GUID is available:

Guid? entityGuid = _dataSet.Guid;

This is null before LoadAsync or CreateAsync completes.

Summary of Key Methods

Method Purpose
LoadAsync(guid) Load an existing entity by GUID
CreateAsync() Create a new entity on the server
Get<T>(path) Read a property value by path
SetAsync(path, value) Write a property value (optimistic update + PATCH)
GetCollection(path, entityName) Get a tracked collection for a navigation
GetUserPrompts(path?) Get validation messages for a property or all
IsReadOnly(path) Check if a property is server-marked read-only
RefreshAsync() Force a full reload from server
Guid The root entity's GUID (null before load/create)
Changed event Fires when data changes