Cohesive Systems logoCOHESIVE SYSTEMS

Search Cohesive Systems

Ready

Search Cohesive Systems

Find product pages, building blocks, technical articles, and graph definitions.

Cohesive.Relations Guide

Getting Started

Author, evaluate, and materialize a typed relation using familiar C# expressions. Start with one supplied CLR object, then add related facts only when the definition needs them.

Start Small

The expression surface is the normal application API for Cohesive.Relations. It lets you describe sources, relationships, and projections with typed C# while Cohesive keeps the resulting definition available to evaluation and tooling.

This guide creates a rooted Load → LoadDto relation, executes it from a supplied object, and compiles the canonical result into a CLR DTO. It then extends the definition with a Customer relationship.

Install

Add the current prerelease package to a .NET 10 project:

dotnet add package Cohesive.Relations --prerelease

Import the namespaces used by this walkthrough:

using System.Text.Json.Serialization;
using Cohesive.Relations.Authoring;
using Cohesive.Relations.Execution;
using Cohesive.Relations.Mapping;

The current published prerelease is 0.1.0-alpha.6. Keep the application and documentation on the same prerelease while APIs are evolving.

Define the Shapes

Start with ordinary CLR types. Serialized names participate in the deterministic shape metadata discovered by the authoring session.

public sealed class Load
{
    [JsonPropertyName("id")]
    public required string Id { get; init; }
 
    [JsonPropertyName("customerId")]
    public required string CustomerId { get; init; }
 
    [JsonPropertyName("status")]
    public required string Status { get; init; }
}
 
public sealed class LoadDto
{
    [JsonPropertyName("id")]
    public required string Id { get; init; }
 
    [JsonPropertyName("status")]
    public required string Status { get; init; }
}

Source<T>() discovers a graph-qualified shape document from the CLR contract. Explicit imported shape documents and member-path overrides remain available for tooling and schemas that should not be convention-derived.

Author the Relation

Create an expression authoring session, declare the logical source, project the DTO, and finish with a rooted relation terminal:

var author = RelationQuery.Expression();
var loads = author.Source<Load>();
 
var loadDtos = author.Project(
    loads,
    (Load load) => new LoadDto
    {
        Id = load.Id,
        Status = load.Status
    });
 
var relation = loadDtos.BuildRelation(dto => dto.Id);

That is the complete semantic definition. The authoring calls have already produced canonical source, projection, assignment, binding, shape, identity, and relation-output structures.

RelationQuery.Expression()

What it contributes
A typed authoring session that lowers expressions into canonical relation/query IR.

Source<Load>()

What it contributes
A logical source and deterministic Load shape snapshot; it does not select storage.

Project(...)

What it contributes
The LoadDto output shape, field assignments, expressions, and field-level provenance.

BuildRelation(...)

What it contributes
A rooted relation contract with derived identity, name, source references, output identity, and validation.

Inspect relation.Validation for structured authoring diagnostics. When persistence or transport is needed, relation.CreateDocument() creates the validated and fingerprinted canonical envelope.

Evaluate Supplied Input

A definition is reusable. An evaluation is one invocation with an explicit identity, parameter values, selected outputs, and supplied-root evidence.

Create one Load and supply it to an evaluation:

var load = new Load
{
    Id = "load-42",
    CustomerId = "customer-7",
    Status = "Open"
};
 
var evaluation = author
    .Evaluate(relation, new("load-dto/load-42"))
    .Supply([load], static value => value.Id)
    .Build();

The evaluation remains target-neutral. It does not contain a database choice, source placement, adapter binding, or driver configuration.

For this one-source mapping, execute the complete canonical pipeline without configuring any I/O:

var outcome = await RelationQueryEvaluator
    .CreateSuppliedOnly()
    .EvaluateAsync(evaluation);
 
if (!outcome.IsSuccessful)
{
    throw new InvalidOperationException(
        $"Evaluation ended with {outcome.Status}; inspect its diagnostics.");
}

CreateSuppliedOnly() is deliberately narrow: it accepts a compiled relation plan with one supplied-root source and no retained relationship traversal. It still performs canonical compilation, realization, physical planning, interpretation, and result construction.

Materialize the DTO

Canonical interpretation produces relation output rows. Compile a CLR construction kernel against the exact plan, then map those rows into LoadDto values:

var mapperCompilation =
    RelationDtoMapperCompiler.Default.Compile<LoadDto>(
        outcome.Compilation.Plan!);
 
if (!mapperCompilation.IsSuccessful)
{
    throw new InvalidOperationException(
        string.Join(Environment.NewLine, mapperCompilation.Diagnostics));
}
 
var mapping = mapperCompilation.Mapper!.Map(
    outcome.PhysicalExecution!);
 
var dto = mapping.Rows.Single().Value;
 
Console.WriteLine($"{dto.Id}: {dto.Status}");
// load-42: Open

The mapper performs CLR construction and conversion over canonical output. It does not execute filters, acquire related observations, or maintain a second mapping definition.

Add a Relationship

Now introduce a related Customer and a flattened output:

public sealed class Customer
{
    [JsonPropertyName("id")]
    public required string Id { get; init; }
 
    [JsonPropertyName("name")]
    public required string Name { get; init; }
}
 
public sealed class LoadSearchDto
{
    [JsonPropertyName("id")]
    public required string Id { get; init; }
 
    [JsonPropertyName("customerId")]
    public required string CustomerId { get; init; }
 
    [JsonPropertyName("customerName")]
    public string? CustomerName { get; init; }
}

Declare Load.CustomerId → Customer identity inline with Traverse:

var customers = author.Traverse<Load, Customer>(
    loads,
    load => load.CustomerId);
 
var searchDocuments = author.Project(
    customers,
    (Load load, Customer customer) => new LoadSearchDto
    {
        Id = load.Id,
        CustomerId = load.CustomerId,
        CustomerName = customer.Name
    });
 
var enrichedRelation =
    searchDocuments.BuildRelation(dto => dto.Id);

The authoring session retains the convention-derived relationship in author.CreateRelationshipCatalogDocument(). The relation knows that the Customer is addressed through Load.CustomerId, including the direction and cardinality of that traversal.

At runtime, the same traversal may become a co-located PostgreSQL join, bounded Customer acquisition followed by local correlation, or in-memory interpretation over already supplied evidence. The relation does not change when that physical choice changes.

Unlike the one-source example, evaluating the enriched relation requires Customer facts. An application can supply both objects directly or configure source placement and bounded source readers. PostgreSQL and Cosmos DB currently provide production source-reader paths; the evaluator may also compose application-defined readers.

What You Have

The walkthrough now has four distinct pieces that can grow independently:

CLR types

What it provides
The familiar application objects supplied to and constructed from an evaluation.

Relation definition

What it provides
One reusable description of the Load-to-result derivation, including its field dependencies.

Evaluation

What it provides
One invocation with supplied roots, parameters, and selected outputs.

Source configuration

What it provides
An optional runtime boundary for obtaining related facts from storage without changing the relation.

From here, the same definition can support selective field demand, composed source reads, native backend compilation, dependency analysis, materialized views, and explain output.

Continue