Skip to content

Context-dependent values

Use IGeneratableInContext<TSelf> when a value must reflect the operation that creates it, not the moment each call happens to run. A command that creates an order id, a log id and a receipt id should stamp all three with the instant the command was received.

GenerationContext is a readonly record struct with two members:

MemberMeaning
NowThe DateTimeOffset the operation was received
TenantA portable tenant identifier string

GenerationContext.DefaultTenant is "00000000-0000-0000-0000-000000000000". Use it when there is no named tenant. Named tenants use their identifier, not a display name. An empty string does not mean the default tenant.

using Cratis.Concepts;
var context = new GenerationContext(DateTimeOffset.UtcNow, GenerationContext.DefaultTenant);

Capture it once and pass the same value to every factory. Calling the clock separately for each value gives one operation several creation times.

using Cratis.Concepts;
public record OrderId(string Value) : ConceptAs<string>(Value), IGeneratableInContext<OrderId>
{
public static OrderId New(GenerationContext context) => new(GenerateValue.TypeId("order", context));
}

UuidV7(context), Ulid(context) and TypeId(prefix, context) all use context.Now. They do not read Tenant. When tenant scope is part of your value, read it in the concept:

using Cratis.Concepts;
public record TenantOrderId(string Value) : ConceptAs<string>(Value), IGeneratableInContext<TenantOrderId>
{
public static TenantOrderId New(GenerationContext context) => new($"{context.Tenant}-{GenerateValue.Ulid(context)}");
}

This is a domain-specific string, not a TypeID. It is rarely needed because each tenant already has its own namespace.

using Cratis.Concepts;
static T Create<T>(GenerationContext context)
where T : IGeneratableInContext<T> => T.New(context);
  • Both interfaces: the context form wins. If a type implements IGeneratable<T> and IGeneratableInContext<T>, callers use the context form. The interfaces do not dispatch or fall back to each other.
  • A default context is a bug. A context with Now equal to default throws MissingGenerationTime; it does not fall back to the current time. This usually means a framework forgot to supply the context, or a spec built new GenerationContext().
  • Time before the Unix epoch throws. GenerationTimeBeforeUnixEpoch is thrown when Now is earlier than 1970-01-01T00:00:00Z.
  • Do not add the context to every concept. If a value does not depend on time or tenant, keep IGeneratable<TSelf>.
MissingGenerationTime: A generation context must contain the time the operation was received.
GenerationTimeBeforeUnixEpoch: Generation time '1969-12-31T00:00:00.0000000+00:00' must be on or after the Unix epoch.