Graph Definitions
A graph is a contract that declares exactly which data a screen needs from the server. It drives three things:
- OData query generation — the graph produces the
$selectand$expandparameters so the API returns only the declared properties. - Runtime validation — reading or writing a property path that is not in the graph throws immediately, preventing silent data bugs.
- UI rendering —
DataGraphandPropertyComponentuse 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 Properties
.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:
GuidandRecordReadOnlyare auto-included on every entity- References add a
$selecton the referenced entity - Nested navigations become nested
$expandclauses
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 readonlyfields — 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.,
ViewGraphandEditGraph). - References auto-add display properties.
.Reference("Customer")gives youCustomer.Titlewithout extra configuration. Only add properties inside the reference if you need more than the title.