DTO mappers
- What it contributes
- Fast object-to-object assignment and convention mapping.
- Where Cohesive.Relations starts
- The mapping becomes a relation when target fields depend on joins, filters, hydration, lineage, materialization, or reuse.
Building Blocks
Mapping, joins, hydration, and queries as one model.
Application code is full of hidden relations. A DTO is derived from an entity. A search document is derived from several entities. A dashboard row is derived from filtered and aggregated facts. An integration message is derived from a domain model and reshaped into an external schema.
These relationships are usually implemented with separate tools: mapper profiles, repository calls, LINQ fragments, SQL strings, search index builders, GraphQL resolvers, and hand-written projection code.
Cohesive.Relations gives these relationships a first-class model.
var loadSearchDocument = Relation<LoadSearchDocument>
.From<Load>()
.Join<Carrier>(
static (load, carrier) => load.CarrierId == carrier.Id)
.Where(
static (load, carrier) => load.Status != LoadStatus.Cancelled)
.Select(static (load, carrier) => new LoadSearchDocument
{
LoadId = load.Id,
ReferenceNumber = load.ReferenceNumber,
CarrierName = carrier.LegalName,
CarrierMcNumber = carrier.McNumber,
PickupCity = load.Pickup.City,
DeliveryCity = load.Delivery.City,
TotalAmount = load.TotalAmount
})
.Materialize(SearchIndexes.Loads);The code looks like a projection, but Cohesive treats it as a relation: an inspectable definition of the sources, joins, filters, fields, target shape, and materialization intent.
A mapping framework usually answers one question:
Given this source object, how do I produce this target object?
Cohesive.Relations answers a larger set of questions:
DTO mapping is still part of the model. It is the simplest case. Cohesive.Relations starts where DTO mapping becomes relational: when a target shape depends on multiple sources, joins, filters, derived fields, backend capabilities, and materialization choices.
Assignment from a source field to a target field is one node in a larger relation graph. A search document may combine a load, customer, carrier, stops, status history, and financial summary. An API response may need authorization-sensitive fields from related entities. A dashboard row may need filtering, grouping, aggregation, and derived metrics.
At that point, the problem is no longer just object-to-object mapping. It is a relation.
LINQ showed that query belongs in application code.
It gave developers a familiar way to filter, join, group, sort, and project data without dropping into strings for every query. That idea remains valuable. Cohesive.Relations can support LINQ-style syntax as one authoring layer for relation definitions.
But Cohesive does not treat provider translation as the whole abstraction. In LINQ, the same expression may run in memory, translate to SQL, fail at runtime, partially evaluate locally, or lose access to backend-specific capabilities such as full-text search, scoring, nested documents, vector search, graph traversal, or materialized projections.
Cohesive.Relations separates the authoring surface from the semantic model. A relation may be written with C# expressions, fluent builders, generated definitions, visual tools, or Ari-inferred mappings. Those authoring surfaces compile into a relation IR that explicitly represents sources, joins, filters, projections, derived fields, aggregations, materialization targets, required fields, and backend capabilities.
LINQ
language-integrated query syntax over provider-specific execution
Cohesive.Relations
relation IR with optional LINQ authoring, explicit capabilities,
hydration planning, and multiple execution targetsLINQ made query language-integrated. Cohesive.Relations makes relations system-integrated.
DTO mappers usually assume the source object already exists.
That is often not enough. If a target shape depends on several entities, related collections, external references, search metadata, or authorization-sensitive fields, the runtime needs to know what must be loaded before the projection can run.
Hydration is therefore part of the relation. A relation should be able to expose:
That means a relation definition can become a hydration plan. The plan can decide whether to read full entities, select only the fields used by the projection, batch related lookups, call repositories, use a search backend, or materialize the result ahead of time.
A relation should preserve intent before choosing execution.
The same relation definition may be interpreted as:
This is the Cohesive pattern: separate what the system means from how it is realized, while keeping the two systematically connected.
Meaning
Systematic Connection
Analyze capabilities, choose execution, generate artifacts, and keep realization traceable to the same semantic relation.
Realizations
A relation should not assume every backend can do everything.
SQL databases, document databases, search engines, graph databases, in-memory collections, repositories, and stream processors all support different operations. Some support joins. Some support nested fields. Some support full-text search. Some support scoring. Some support aggregations. Some support graph traversal. Some support only a subset of filtering and sorting.
Cohesive.Relations makes those capabilities explicit.
var plan = relation.AnalyzeAgainst(SearchBackends.Elastic);
if (!plan.IsSupported)
{
// Unsupported: relational join must be hydrated before indexing.
// Supported alternative: repository hydration + generated projection.
}The question is not only:
Can this expression be translated?
The better question is:
Which parts of this relation can run on this backend, which parts require hydration, and which parts must be materialized or executed elsewhere?
That gives teams earlier diagnostics, safer portability, and more honest execution plans.
Cohesive.Relations is useful when a projection spans more than one source, when mapping intent needs to be inspectable or testable, or when the same relation feeds API, UI, search, reporting, or integration surfaces.
Immediate use cases include:
Use a simple mapper when the mapping is local, one-source, stable, and has no need for hydration, query lowering, materialization, or reuse. Use Cohesive.Relations when the relationship itself is important enough to become part of the system model.
Inputs
Stable source and target shapes describe the facts a relation can observe and produce.
Meaning
The semantic relation records sources, joins, filters, mappings, aggregations, and materialization intent.
Planning
The relation is checked against capabilities before choosing where each part can run.
Realization
Runtimes lower the supported plan into repositories, SQL, search, graph, generated code, or materialized views.
The relation lifecycle keeps semantic intent intact while allowing different runtimes to make different execution choices. A backend-specific compiler can lower the supported parts. A repository runtime can hydrate the required fields. A code generator can produce a fast mapper. A projection engine can materialize a read model. Ari can infer candidate relations and hand them to developers for review.
The relation IR is the lower-level model that makes the page-level promise practical. It should appear after the reader understands the problem, not before.
At the core is RelationDefinition, the canonical model for:
Around that IR are typed authoring APIs, query APIs, execution services, observation mappers, lineage records, serialization support, and adapter-specific interpreters.
Most systems accumulate mapping code, query code, API response shaping, UI filter models, search documents, reporting SQL, integration transforms, 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.
A mapper can execute. A relation can be inspected, hydrated, optimized, lowered, materialized, tested, and reused.
That gives higher-level Cohesive components one relation definition for:
The result is a foundation for semantic system definition: define the relation once, execute it in memory, hydrate it from repositories, lower it into backend queries, materialize it as a read model, or use it as the basis for schema matching.