--- name: cratis-fundamentals-concept description: Create strongly typed Cratis domain values with ConceptAs and Chronicle event-source identities with EventSourceId. Use when a C# domain value has meaning beyond its primitive or when an identity is actually used as a Chronicle event-source/stream ID. Do not use for enums, DTO-only transport values, arbitrary non-stream entity IDs, or event schema migration. license: MIT --- # Cratis domain concepts and event-source identities Replace a primitive only when the domain gives it distinct meaning. Keep value concepts and Chronicle stream identities separate. ## Verified product sources This skill is verified against these exact public releases: | Package | Version | Purpose | | --- | --- | --- | | `Cratis.Fundamentals` | `7.18.1` | `Cratis.Concepts.ConceptAs` | | `Cratis.Chronicle` | `16.38.1` | `Cratis.Chronicle.Events.EventSourceId` and `EventSourceId` | Reverify product sources before claiming support for another version. ## Choose the type - Derive a name, amount, code, number, or non-stream entity ID from `ConceptAs`. - Derive an identity from `EventSourceId` only when that value is actually passed to Chronicle as the event-source/stream ID. - Do not use `ConceptAs` for a Chronicle stream identity. - Do not use `EventSourceId` merely because a value is called an ID. - Do not wrap an enum. An enum already expresses a closed domain concept. - Keep DTO-only transport values primitive unless the domain type belongs in the public contract. Both generic bases require an underlying type that implements `IComparable`. ## Create a value concept A value concept contains exactly one wrapped value. Do not add extra properties; Fundamentals converters assume the concept is a single-value type and additional state can be lost during serialization. ```csharp // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using Cratis.Concepts; namespace .; /// /// Represents the . /// /// The underlying value. public record ( Value) : ConceptAs<>(Value); ``` `ConceptAs` supplies implicit conversion from the concept to `T`. Add the reverse conversion only when it improves the domain API: ```csharp public static implicit operator ( value) => new(value); ``` Primitive-to-concept conversion is optional; it is not a Fundamentals requirement. ### Absence and sentinels `ConceptAs` rejects a null wrapped value. Represent absence with a nullable concept reference such as `?` when absence is valid. A `NotSet` or `Empty` value is optional domain policy. Add one only when the chosen primitive value is impossible or explicitly reserved in that domain. Do not assume `string.Empty`, `0`, or `Guid.Empty` is universally invalid. ## Create a Guid-backed Chronicle stream identity Use this shape only for an identity actually supplied to Chronicle append/read operations as the event-source ID. ```csharp // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using Cratis.Chronicle.Events; namespace .; /// /// Represents the event-source identity of a . /// /// The underlying Guid value. public record (Guid Value) : EventSourceId(Value) { /// /// Creates a new . /// /// A new . public static New() => new(Guid.NewGuid()); /// /// Converts a Guid to a . /// public static implicit operator (Guid value) => new(value); } ``` `New()` and the primitive-to-derived conversion are conveniences on this domain type. `EventSourceId` does not construct an arbitrary derived identity for you. ## Create a non-Guid Chronicle stream identity Use a factory only when the domain has an authoritative way to create the underlying value. ```csharp // Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using Cratis.Chronicle.Events; namespace .; /// /// Represents the event-source identity of a . /// /// The underlying value. public record ( Value) : EventSourceId<>(Value) { /// /// Converts the underlying value to a . /// public static implicit operator ( value) => new(value); } ``` The exact `EventSourceId` base supports conversions among `T`, string, untyped `EventSourceId`, and `EventSourceId`. Those operators do not create your derived `` from `T`, string, or untyped `EventSourceId`. Declare only the derived-type conversions your domain API needs. String and Guid are the safest round-trip primitives. Chronicle also supports constructible `ConceptAs` and `ConceptAs` values. Other comparable values rely on `Convert.ChangeType`; verify round-trip behavior before using them as stream IDs. ### Unspecified and sensitive identities `EventSourceId.Unspecified` belongs to the untyped string-backed ID. `Guid.Empty`, `0`, `0L`, and similar typed values become real, specified stream IDs after conversion; they are not Chronicle's unspecified value. Treat any sentinel on a typed identity as explicit domain policy, not framework behavior. Never use a sensitive natural identifier directly as an event-source ID. Chronicle cannot encrypt event-source IDs. Use a random surrogate stream ID and store the sensitive value separately under the approved compliance model. ## Use the identity with Chronicle Pass the typed identity as the append/read event-source ID. Merely declaring an `EventSourceId` property does not select the event stream. Do not add `[Key]` or `[Subject]` to an `EventSourceId`-derived member; Chronicle analyzer `CHR0026` reports that misuse. Do not add `[PII]` to an event-source ID; analyzer `CHR0034` rejects it. ## Placement is an application convention In a Cratis application, place the concept with the feature that owns its meaning rather than in a generic `Concepts/` folder. Put genuinely cross-feature concepts in `Common/`. Do not introduce a top-level `Features/` wrapper. This placement is a Cratis application convention, not a Fundamentals or Chronicle API requirement. Framework and client repositories follow their own repository structure. ## Verify - `ConceptAs` and `EventSourceId` use an `IComparable` underlying type. - A concept contains exactly one wrapped value and no extra properties. - Enums remain enums. - Null absence uses a nullable concept reference rather than a null wrapped value. - Primitive-to-derived conversions and sentinels exist only when justified by the domain. - An `EventSourceId` type represents a real Chronicle stream identity. - The typed identity is passed explicitly to Chronicle operations. - No `[Key]`, `[Subject]`, or `[PII]` attribute is placed on the stream identity. - Sensitive natural identifiers use a surrogate stream ID. - The file carries the repository license header. - The project builds and its relevant specifications pass against the verified package versions.