Cohesive Systems logoCOHESIVE SYSTEMS

Building Blocks

Cohesive.Relations

Cohesive Relations icon

Cohesive.Relations is the semantic layer for describing how shapes relate, how data is projected, and how queries can be interpreted across various backends.

Define the meaning of a relationship first, then attach interpretations: in-memory execution, repository hydration, storage adapters, API contracts, UI query forms, search indexes, generated frontend types, or AI-assisted schema matching.

It goes beyond DTO mapping or a query builder: a Relation is not an opaque function or a SQL string, it is an explicit model of projection intent: sources, joins, filters, mappings, aggregations, materialization, and invariants that can be executed, inspected, and lowered into concrete implementations.

What It Models

Cohesive.Relations works over shape observations and shape metadata from Cohesive.Core. That gives the system a common substrate for mapping and querying data without binding the relation to a particular database, transport, or object model.

At the core is RelationDefinition, the canonical relation IR:

  • Stable RelationId and RelationName.
  • One or more semantic sources with aliases and cardinality.
  • Join definitions that preserve source identity and join intent.
  • Optional filters over the relation rowset.
  • Mapping definitions that project fields into a target shape.
  • Optional materialization policy for persisted read models or indexes.
  • Metadata such as determinism and code generation eligibility.
  • Invariants that describe expected semantic properties of the projection.

Around that IR are typed authoring APIs, query APIs, execution services, observation mappers, lineage records, and serialization support. The result is a relation model that can be authored ergonomically but still remain explicit enough for tooling.

Relation Authoring

The typed DSL lets developers declare projections using C# shapes while producing a relation definition that the rest of Cohesive can inspect.

Basic Projection

RelationDefinition relation = Relation<CarrierDto>
    .From<Carrier>()
    .MapFields()
        .Rename(c => c.LegalName, d => d.Name)
        .Rename(c => c.McNumber, d => d.Mc);
 
var outputs = await new RelationExecutor().ExecuteAsync(
    relation,
    [ToObservation(carrier)]
    );

MapFields() uses convention mapping where source and target names match, then allows explicit renames where the semantic field names differ. The output is still a RelationDefinition, not an opaque delegate.

Joined Projection

var relation = Relation<LoadSearchDocument>
    .From<Load>()
    .Join<Carrier>(static (load, carrier) => load.CarrierId == carrier.Id)
    .Select(static (load, carrier) => new LoadSearchDocument
    {
        LoadId = load.Id,
        CarrierName = carrier.LegalName,
        Amount = load.TotalAmount,
        CarrierSafetyScore = carrier.SafetyScore
    });

This relation describes a search document as a semantic projection over a Load source and a joined Carrier source. The join, aliases, field assignments, and target shape are preserved in the relation IR so the same definition can support execution, tracing, materialization, or future adapter lowering.

Grouping And Derived Relations

Relations can also derive intermediate read models and chain those models into subsequent projections.

var revenueRelation = Relation<CarrierRevenue>
    .From<Invoice>()
    .GroupBy(invoice => invoice.CarrierId)
    .Select(group => new CarrierRevenue
    {
        CarrierId = group.Key,
        TotalRevenue = group.Sum(invoice => invoice.Amount)
    });
 
var riskRelation = Relation<CarrierRisk>
    .From<CarrierRevenue>()
    .Select(revenue => new CarrierRisk
    {
        CarrierId = revenue.CarrierId,
        RiskTier =
            revenue.TotalRevenue > 1_000_000m ? "High"
            : revenue.TotalRevenue > 250_000m ? "Medium"
            : "Low"
    });

This is useful when a domain needs semantic stages: source events to summary, summary to risk model, risk model to network-level materialization.

Query And Aggregation

Cohesive.Relations also defines structured query semantics over entity observations. EntityQuery can request rows, aggregations, or both from the same predicate scope.

var compile = new EntityPredicate(
    new FieldPredicate(
        FieldPath.FromField(nameof(ProcessTaskRecord.ProcessType)),
        new ExactValuePredicate("compile")
        )
    );
 
var response = await repository.Query(
    OperationContext.Create(),
    EntityQuery.ForRowsAndAggregations(
        compile,
        new EntityRowQuery(
            Fields: FieldSelection.ForFields(nameof(ProcessTaskRecord.Id)),
            Window: new ResultPageOptions(
                Limit: 2,
                Offset: 0,
                OrderBy: [new(FieldPath.FromField(nameof(ProcessTaskRecord.Id)))],
                Mode: ResultPaginationMode.Offset
                )
        ),
        new EntityAggregationQuery(
        [
            new AggregationRoot(
                Name: "allCompile",
                Root: new GlobalAggregationPlan(),
                Statistics: [new CountAggregationStatistic()]
                )
        ])));

The same query model carries predicate semantics, field selection, pagination, ordering, and aggregation requests. A repository or adapter can execute it directly when it has the capability, or the runtime can evaluate supported semantics over observations.

Joins

For hydrated projections, the query builder can join observation sources and project results after the join context is assembled.

var query = Query.From<CustomerRecord>(
        [new("customer-1", "segment-enterprise")],
        rootId: static customer => customer.CustomerId
    )
    .JoinOne<CustomerRecord, string>(
        alias: "segment",
        source: SegmentSource,
        rootKeySelector: static customer => customer.SegmentId
    )
    .JoinMany<CustomerRecord, OrderRecord, string>(
        alias: "orders",
        source: OrderSource,
        rootKey: static customer => customer.CustomerId,
        foreignKey: static order => order.CustomerId
    )
    .Select(ctx => new CustomerProjection(
        CustomerId: ctx.RootAs<CustomerRecord>().CustomerId,
        Segment: ctx.RequireOne<SegmentRecord>("segment"),
        Orders: ctx.Many<OrderRecord>("orders")
    )
);

This makes query authoring useful even before a backend-specific compiler exists: the semantic join and projection can run against in-memory or repository-backed observations, then later be lowered where infrastructure supports it.

Use Cases

  • DTO and read-model projection where mapping intent must be fast and inspectable.
  • Search document and index projection from multiple data sources.
  • Cross-source joins over observations, repositories, or future storage-backed adapters.
  • Aggregations for dashboards, metrics, process summaries, and operational views.
  • Backend-declared query forms and result surfaces through Cohesive.Presentation.
  • Data-source normalization and schema mapping workflows, including Ari-style inferred relations.
  • Portable query semantics that can move across in-memory execution, storage repositories, APIs, and generated frontend contracts.
  • Materialized projections that keep semantic lineage attached to the generated read model.
  • Testable semantic invariants around projection correctness and query behavior.

How It Compares

SystemWhat it is good atHow Cohesive.Relations differs
AutoMapper / MapsterFast object-to-object mapping, convention mapping, DTO projection.Models mappings as semantic IR with sources, joins, filters, lineage, materialization, query semantics, and adapter hooks. It can still express DTO projection, but DTO projection is only one interpretation.
GraphQLClient-facing graph selection, resolver composition, schema-based API transport.Starts from backend semantic relations rather than client request syntax. A relation can inform APIs, UI, storage, and generated contracts without being tied to GraphQL as the surface.
ORMsPersistence-centric entity tracking, relational database access, object queries.Starts from semantic shapes and relationships, then attaches storage interpretation. It does not assume that the domain model, persistence model, and projection model are the same thing.
Raw SQLPrecise control, database-native performance, full access to vendor features.Keeps relation intent explicit, typed, inspectable, and portable. SQL-like lowering can be an adapter interpretation rather than the source of truth.
SQL buildersSafer dynamic SQL construction and composable query syntax.Still usually model the database surface. Cohesive.Relations models domain relations and capability requirements above a specific SQL dialect or schema.

Cohesive.Relations is not intended to replace every use of these systems. It is most useful when the relationship itself is important enough to become part of the system's semantic model: when mappings need lineage, queries need capabilities, projections need to feed APIs and UI, or infrastructure choices should remain attached to meaning rather than define it.

Why It Matters

Most systems accumulate mapping code, query code, API response shaping, UI filter models, search documents, reporting SQL, and storage-specific adapters as separate artifacts. Each artifact embeds part of the same semantic relationship, usually as duplicated strings and one-off logic.

Cohesive.Relations makes those relationships first-class.

That gives higher-level Cohesive components a single source of truth for:

  • Which shapes participate in a projection.
  • Which fields are selected, renamed, aggregated, or derived.
  • Which joins and predicates are semantically required.
  • Which capabilities an execution backend must support.
  • Which generated API, frontend, or materialized artifacts should stay aligned.

The result is a foundation for semantic system definition: relations can be authored once, interpreted many ways, tested as explicit models, and evolved without losing the meaning they encode.

References