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 renderingDataGraph 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;
  $orderby=LineOrder,Product/Sku;
  $expand=Product($select=Title,Guid,RecordReadOnly))

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
&$expand=BillingCustomer($select=Title,Guid,RecordReadOnly);
         PrimaryContact($select=PrimaryEmail,PrimaryPhone,Guid,RecordReadOnly;
           $expand=MailingAddress($select=Street,City,State,PostalCode,Guid,RecordReadOnly;
             $expand=Country($select=Title,Guid,RecordReadOnly)))

Notice:

  • Guid and RecordReadOnly 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.