Graph Definitions

A graph is a contract that declares exactly which data a screen needs from the server. It drives three things:

  1. OData query generation — the graph produces the $select and $expand parameters so the API returns only the declared properties.
  2. Runtime validation — reading or writing a property path that is not in the graph throws immediately, preventing silent data bugs.
  3. UI rendering — DataGraph and PropertyComponent use the graph to know which properties exist and how they nest.

GraphBuilder API

Build graphs with the fluent GraphBuilder:

using Benevia.Core.Client.GraphDefinitions;
using Benevia.Core.Client.GraphDefinitions.Builders;

private static readonly GraphDefinition graph = new GraphBuilder("Customer")
    .Property("Id")
    .Property("FullName")
    .Build();

Simple Properties

.Property("Name") declares a scalar property on the root entity.

new GraphBuilder("Customer")
    .Property("Id")        // string — the customer's ID code
    .Property("FullName")  // string — computed display name
    .Build();

Multiple Properties

.Properties(IEnumerable<string>) declares multiple scalar properties at once. This is useful for reducing repetition when adding several properties to the same entity or navigation.

new GraphBuilder("Customer")
    .Properties(["Id", "FullName", "Email", "Phone"])
    .Build();

This is equivalent to calling .Property() four times. The .Properties() method is an extension method defined in PropertyBuilderExtensions and works on any builder that implements IPropertyBuilder<TBuilder>.

Reference Properties

.Reference("Name") declares a foreign-key reference. It automatically adds:

  • {Name}Guid — the foreign key property
  • {Name}.Title — the default display property (or a custom one if specified)
new GraphBuilder("Customer")
    .Reference("BillingCustomer")  // Adds: BillingCustomerGuid, BillingCustomer.Title
    .Build();

To include additional properties on the referenced entity, pass a configuration action:

new GraphBuilder("Customer")
    .Reference("BillingCustomer", bc => bc
        .Property("Id")
        .Property("FullName"))
    .Build();
// Adds: BillingCustomerGuid, BillingCustomer.Id, BillingCustomer.FullName

You can also specify a single display property by name:

.Reference("BillingCustomer", "Id")
// Adds: BillingCustomerGuid, BillingCustomer.Id

.Navigation("Name", ...) declares a nested entity relationship (one-to-one or one-to-many). Unlike .Reference(), it does not auto-add a GUID property — it describes a nested object or collection within the parent.

new GraphBuilder("Customer")
    .Navigation("PrimaryContact", pc => pc
        .Property("PrimaryEmail")
        .Property("PrimaryPhone"))
    .Build();

Navigations can nest arbitrarily deep:

new GraphBuilder("Customer")
    .Navigation("PrimaryContact", pc => pc
        .Property("CompanyName")
        .Property("PrimaryEmail")
        .Property("PrimaryPhone")
        .Navigation("MailingAddress", ma => ma
            .Property("Street")
            .Property("City")
            .Property("State")
            .Property("PostalCode")
            .Reference("Country")))
    .Build();

References inside navigations work the same way — .Reference("Country") adds Country.Title and the GUID automatically.

Ordering Collection Navigations

Use .OrderBy(...) or .OrderByDescending(...) to emit OData $orderby for the current graph scope. This is especially useful for collection navigations where row order matters in the UI.

new GraphBuilder("SalesOrder")
    .Navigation("Details", details => details
        .Reference("Product")
        .Property("Quantity")
        .Property("UnitPrice")
        .OrderBy("LineOrder", "Product.Sku"))
    .Build();

The graph above produces an expand similar to:

$expand=Details($select=ProductGuid,Quantity,UnitPrice,Guid,RecordReadOnly,RecordHidden;
  $orderby=LineOrder,Product/Sku;
  $expand=Product($select=Title,Guid,RecordReadOnly,RecordHidden))

Groups

.Group("Name", ...) creates named property groups for UI organization. Groups do not affect the OData query — they provide layout hints that PropertyGroup components can render.

new GraphBuilder("SalesOrder")
    .Property("Title")
    .Property("Status")
    .Property("TotalPrice")
    .Property("Discount")
    .Group("Pricing", pricing => pricing
        .Property("TotalPrice")
        .Property("Discount"))
    .Build();

Groups can nest:

.Group("Pricing", pricing => pricing
    .Property("TotalPrice")
    .Group("Discounts", discounts => discounts
        .Property("PercentOff")
        .Property("PromotionCode")))

Building the Definition

.Build() returns an immutable GraphDefinition with:

Member Type Description
RootEntity string The entity name (e.g., "Customer")
Paths List<PropertyPath> All declared leaf property paths
Groups List<Group> Named groups for UI organization
Navigations Dictionary<string, List<PropertyPath>> Paths organized by navigation entity

OData Query Generation

GraphDefinition converts its paths into OData query parameters via ToODataQuery(). For the Customer graph:

private static readonly GraphDefinition CustomerGraph = new GraphBuilder("Customer")
    .Property("Id")
    .Property("FullName")
    .Reference("BillingCustomer")
    .Navigation("PrimaryContact", pc => pc
        .Property("PrimaryEmail")
        .Property("PrimaryPhone")
        .Navigation("MailingAddress", ma => ma
            .Property("Street")
            .Property("City")
            .Property("State")
            .Property("PostalCode")
            .Reference("Country")))
    .Build();

CustomerGraph.ToODataQuery() produces:

$select=Id,FullName,BillingCustomerGuid,Guid,RecordReadOnly,RecordHidden
&$expand=BillingCustomer($select=Title,Guid,RecordReadOnly,RecordHidden);
         PrimaryContact($select=PrimaryEmail,PrimaryPhone,Guid,RecordReadOnly,RecordHidden;
           $expand=MailingAddress($select=Street,City,State,PostalCode,Guid,RecordReadOnly,RecordHidden;
             $expand=Country($select=Title,Guid,RecordReadOnly,RecordHidden)))

Notice:

  • Guid, RecordReadOnly, and RecordHidden are auto-included on every entity
  • References add a $select on the referenced entity
  • Nested navigations become nested $expand clauses

Graph Contract Enforcement

At runtime, DataSet wraps the graph in a GraphContract that validates every Get<T>() and SetAsync() call. Accessing a path outside the graph throws immediately:

dataSet.Get<string>("PrimaryContact.PrimaryEmail"); // ✓ In graph
dataSet.Get<string>("PrimaryContact.Note");          // ✗ Not in graph → InvalidOperationException

This catches bugs at development time rather than serving stale or missing data silently.

Full Customer Graph Example

A comprehensive Customer graph covering profile, contact information, and addresses:

using Benevia.Core.Client.GraphDefinitions;
using Benevia.Core.Client.GraphDefinitions.Builders;

private static readonly GraphDefinition CustomerGraph = new GraphBuilder("Customer")
    .Property("Id")
    .Property("FullName")
    .Reference("BillingCustomer")
    .Navigation("PrimaryContact", pc => pc
        .Property("CompanyName")
        .Property("PrimaryEmail")
        .Property("SecondaryEmail")
        .Property("PrimaryPhone")
        .Property("SecondaryPhone")
        .Property("Website")
        .Property("Note")
        .Property("IsMailingAddressSameAsPhysical")
        .Navigation("MailingAddress", BuildAddress)
        .Navigation("PhysicalAddress", BuildAddress))
    .Build();

private static void BuildAddress(NavigationBuilder address)
{
    address
        .Properties(["Street", "City", "State", "PostalCode"])
        .Reference("Country");
}

This graph declares every path the Customer page will read or write. The DataSet built from this graph will reject any access to properties not listed here — for example, trying to read PrimaryContact.FirstName would throw because it was not included.

Tips

  • Declare only what you use. Each property in the graph adds to the OData query and server load. Don't include properties "just in case."
  • Graphs are static. Define them as static readonly fields — they are immutable and thread-safe.
  • Separate graphs for separate views. If your View mode needs fewer properties than Edit mode, define two graphs (e.g., ViewGraph and EditGraph).
  • References auto-add display properties. .Reference("Customer") gives you Customer.Title without extra configuration. Only add properties inside the reference if you need more than the title.