Cohesive Systems logoCOHESIVE SYSTEMS

Search Cohesive Systems

Ready

Search Cohesive Systems

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

Cohesive.Storage Guide

Getting Started

Persist one entity, read only the fields an operation needs, protect an update with optimistic concurrency, and expose the repository as a Relations source.

Start with One Entity

This guide persists a Load in the in-memory reference repository. It writes the initial state, reads the fields needed to assign a carrier, and commits a new state using the concurrency token returned by that read.

The same repository is then registered as a bounded source for Cohesive.Relations. The repository remains responsible for physical acquisition, while Relations remains responsible for filters, joins, projections, and results.

Install

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

dotnet add package Cohesive.Storage --prerelease

Import the namespaces used by this walkthrough:

using Cohesive.Prelude;
using Cohesive.Storage;
using Cohesive.Transitions.Authoring;

Keep Storage and the semantic packages it references on compatible prerelease versions.

Define the Entity

Start with the same style of entity used by Cohesive.Entities:

public enum LoadStatus { Draft, Assigned }
 
public sealed record LoadState(
    string TenantId,
    LoadStatus Status,
    string? CarrierId);
 
public sealed class Load : Entity<Load>
{
    public Load()
    {
        TenantId = WriteOnceField<string>(nameof(TenantId));
        Status = Field(nameof(Status), LoadStatus.Draft);
        CarrierId = Field<string?>(
            nameof(CarrierId),
            initialValue: null,
            configure: field => field.Optional());
 
        Invariant(
            name: "AssignedLoadsHaveACarrier",
            predicate: load => load.Status != LoadStatus.Assigned ||
                               load.CarrierId != null);
    }
 
    public Field<string> TenantId { get; }
    public Field<LoadStatus> Status { get; }
    public Field<string?> CarrierId { get; }
}

Load.Define() supplies the semantic entity definition that the repository persists. The physical provider does not infer or redefine these fields and invariants.

Create the Repository

Create the in-memory entity and outbox repository. TenantId supplies its partition policy:

var repository = new InMemoryEntityOutboxRepository(
    entityDefinition: Load.Define(),
    partitionKeyFieldName: nameof(Load.TenantId));
 
var context = OperationContext.Create();

Application hosts normally resolve repositories through RegisterEntityRepository and the dependency-injection helpers. Constructing one directly keeps the first example focused on its behavior.

Write Initial State

Create a valid entity state and persist its underlying observation:

var initial = Load.Instance.CreateState(
    entityId: "load-42",
    stateObject: new LoadState(
        TenantId: "tenant-7",
        Status: LoadStatus.Draft,
        CarrierId: null));
 
var created = await repository.Upsert(
    context: context,
    write: new EntityWriteRequest(
        Entity: initial.Observation));

The returned EntitySnapshot includes the committed observation, resolved partition key, and an opaque provider concurrency token.

Read Only the Required Fields

An assignment decision needs the current status and carrier. Ask for those fields and provide the known partition:

var current = await repository.TryGet(
    context: context,
    id: "load-42",
    options: EntityReadOptions
        .ForFields(nameof(Load.Status), nameof(Load.CarrierId))
        .WithPartitionKey("tenant-7"));
 
if (current is null)
    throw new InvalidOperationException("Load not found.");

The snapshot still retains entity identity, version, partition, and concurrency evidence. Its observation contains only the requested fields, and LoadedFields records the projection.

In a Transition execution environment, compilation supplies the possible field requirements and the evaluated decision records the fields its selected path actually read. An adapter can use those facts to choose a selective read safely.

Commit with Optimistic Concurrency

For a compact storage-only example, construct the accepted candidate state directly. A normal application obtains it by applying an accepted Transition decision:

var assigned = Load.Instance.CreateState(
    entityId: "load-42",
    stateObject: new LoadState(
        TenantId: "tenant-7",
        Status: LoadStatus.Assigned,
        CarrierId: "carrier-9"),
    version: current.Entity.Version + 1);
 
var committed = await repository.Upsert(
    context: context,
    write: new EntityWriteRequest(
        Entity: assigned.Observation,
        ExpectedConcurrencyToken: current.ConcurrencyToken));

If another writer changed the entity after the read, the repository throws ObservationConcurrencyConflictException. Storage does not silently overwrite state from a stale decision.

Production Transition integrations can also validate actual semantic reads and request stronger atomic boundaries. IEntityOutboxRepository commits state with outgoing envelopes, while IEntityTransitionOperationRepository commits state with a replayable Process operation receipt.

Register a Relations Source

The repository can provide physical facts to a canonical Relation or Query. Register its graph-qualified shape, reader, and hard limits:

var source = EntityRelationQuerySourceRegistration.InMemory(
    shape: loadShape,
    repository: repository,
    limits: new(
        maximumBatchSize: 100,
        maximumBufferedRows: 10_000,
        maximumFanOut: 100,
        maximumConcurrency: 4));
 
var catalog = new EntityRelationQuerySourceCatalog([source]);
var evaluator = catalog.CreateEvaluator(physicalPlanningPolicy);

loadShape is the graph-qualified shape used by the Relation definition. The source reader supplies bounded enumeration, identity batches, relationship-reference batches, selected fields, and completeness evidence. The evaluator continues to use the canonical Relations model for query meaning.

Understand the Boundaries

Entity fields and invariant

Owner in this walkthrough
The Load definition in Cohesive.Entities.

Legal assignment decision

Owner in this walkthrough
An AssignCarrier Transition; constructed state is used only to keep this Storage guide compact.

Point read, partition resolution, field projection, and optimistic write

Owner in this walkthrough
The entity repository and its provider implementation.

Filters, joins, projection, aggregation, and paging

Owner in this walkthrough
Cohesive.Relations, using Storage only for physical fact acquisition.

Production database guarantees

Owner in this walkthrough
A concrete adapter such as Cohesive.Adapters.Cosmos, with capability evidence.

Continue

Return to the Cohesive.Storage overview →

Read Storage Internals →

Define the AssignCarrier Transition →