Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 45 additions & 2 deletions docs/event-modeling/declaring.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ Every role takes either a type or a name:
| `HandledBy<T>()` | The handler or endpoint type |
| `Against<T>()` (or `UsesAggregate<T>()`) | An aggregate the slice decides against |
| `StartsStream<T>()` | The aggregate whose stream the slice *starts* -- see below |
| `NoAggregate()` | Deliberately no aggregate -- see [A default aggregate](#a-default-aggregate) |
| `DeciderModel<T>()` | The DCB decider model the slice decides through, rather than single-stream aggregates |
| `Emits<T>()` | An event the slice writes |
| `Publishes<T>()` | A non-event message the slice sends out |
| `On<T>()` (or `From<T>()`) | An event the slice reacts to or folds |
Expand Down Expand Up @@ -144,6 +146,47 @@ A typo in a name is just a second, mysterious box on the diagram. A typo in a ty

The aggregate is also recorded as one of the slice's aggregates, so any viewer that doesn't know about this role still draws it.

## A Default Aggregate

Most commands in a chapter decide against the same aggregate. Say so once with `ForAggregate<T>()`, the companion to `InChapter()`:

```csharp
public override void Configure(EventModelBuilder model)
{
model.InChapter("BookingAppointments");
model.ForAggregate<Appointment>(); // or ForAggregate("Appointment")

model.Command<ConfirmAppointment>() // decides against Appointment
.Emits<AppointmentConfirmed>();

model.Command<RescheduleAppointment>()
.Against<Calendar>() // an explicit Against replaces the default
.Emits<AppointmentRescheduled>();

model.Command<ProposeAppointment>()
.StartsStream<Appointment>() // a slice that starts a stream ignores the default
.Emits<AppointmentProposed>();

model.Command<RecordWalkIn>()
.NoAggregate() // deliberately none
.Emits<WalkInRecorded>();

model.Command<BookSlot>()
.DeciderModel<SlotBooking>() // a DCB decider model instead
.Emits<SlotBooked>();
}
```

The rules:

- The last `ForAggregate` wins, and applies to every **command** slice opened after it that declares none of `Against`, `StartsStream`, `NoAggregate` or `DeciderModel`. Earlier slices, views, automations and translations are untouched.
- Like `InDomain` and `InChapter`, it never carries into another definition: each definition gets a fresh builder.
- `ForAggregate` also declares the aggregate on the model. `Aggregate<T>()` is unchanged: it declares an aggregate and sets no default.
- `NoAggregate()` is for a slice that genuinely decides against nothing. Tooling such as Wolverine's scaffold stops warning about the missing aggregate. Combining it with `Against` on one slice throws.
- `DeciderModel<T>()` only records the decider type for now; how its events are selected waits on the Dynamic Consistency Boundary design.

Each slice says *why* it has its aggregate on `AggregateDeclaration` -- `Default`, `Explicit`, `None` or `DeciderModel` -- so tooling can explain it. A default-applied aggregate is still a declared claim: if the handler decides against something else, the code wins and the difference becomes a hotspot.

## Aggregates

You can declare the aggregates in the model itself. The events each aggregate applies get filled in from the code later:
Expand Down Expand Up @@ -247,15 +290,15 @@ That's the workflow in a nutshell:
3. Write the specifications -- they'll be red
4. Build the handlers until the specifications pass and the disagreements go away

Some things never come from code. Slice names, trigger labels like "Agent clicks Escalate", domains, chapters and specification links only ever come from declarations, so those stay yours.
Some things never come from code. Slice names, trigger labels like "Agent clicks Escalate", domains and chapters only ever come from declarations, so those stay yours. Specification links can come from the specifications themselves -- see [Links from the specifications](/event-modeling/descriptors#links-from-the-specifications).

::: tip
For a while, this builder could only name, group and annotate slices -- the [overlay](/event-modeling/overlay). The provenance ladder is what made it safe to declare roles again: a declaration can no longer overwrite what the code actually does.
:::

## Registering the Model

Register each definition with the container, the same way as an overlay:
Register each definition with the container, the same way as an overlay -- or all of them at once with `AddDiscoveredEventModels(assembly)`. A definition that doesn't override `Name` contributes to the application's model, so one definition per chapter merges with the code without any further wiring:

<!-- snippet: sample_registering_declared_models -->
<a id='snippet-sample_registering_declared_models'></a>
Expand Down
31 changes: 28 additions & 3 deletions docs/event-modeling/descriptors.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ One slice. The positional constructor is the original 2.x shape and is kept sour
| `Domain` | **Declared** | Bounded context -- in a modular monolith, the module |
| `Chapter` | **Declared** | A named span of slices — the navigation unit above `Domain` |
| `StartsStream` | Either | The aggregate whose stream the slice *starts*, as against appends to |
| `AggregateDeclaration` | **Declared** | *Why* the slice has its aggregate: `Default` (from `ForAggregate`), `Explicit`, `None` or `DeciderModel` |
| `DeciderModel` | **Declared** | The Dynamic Consistency Boundary decider model the slice decides through |
| `Origin` | Any source | *Which* source contributed the slice — a file path, a suite or assembly name, a store URI. Stamp it and a [source disagreement](/event-modeling/hotspots#a-rung-is-not-an-identity) names a party rather than a rung |

### What a slice reads
Expand Down Expand Up @@ -177,13 +179,18 @@ foreach (var hotspot in helpdesk.Hotspots)

### Provenance decides the merge

Four producers feed one descriptor — Gherkin specs, the C# overlay and code-first specs, Wolverine's chains, and runtime observation from CritterWatch — so something has to arbitrate when two of them describe the same slice. That something is a three-rung ladder of authority, `EventModelProvenance`:
Four producers feed one descriptor — Gherkin specs, the C# overlay and code-first specs, Wolverine's chains, and runtime observation from CritterWatch — so something has to arbitrate when two of them describe the same slice. That something is a ladder of authority, `EventModelProvenance`:

| Rung | Who | Beats |
| --- | --- | --- |
| `Declared` | A Gherkin spec, a code-first spec, the `EventModelDefinition` overlay | — |
| `Derived` | Wolverine's handler / HTTP / gRPC chains, the source generator | `Declared` |
| `Observed` | CritterWatch watching a running system | `Derived`, `Declared` |
| `Specified` | Links read off the specifications by [`EventModelSpecifications.Link`](#links-from-the-specifications) | `Declared` |
| `Derived` | Wolverine's handler / HTTP / gRPC chains, the source generator | `Specified`, `Declared` |
| `Observed` | CritterWatch watching a running system | `Derived`, `Specified`, `Declared` |

::: warning Compare rungs by rank
`Specified` was added after the others, so its wire value is `3` although it ranks second. Compare rungs with `Rank()` / `Outranks()`, never with `>` on the enum.
:::

Production beats what the code implies, and the code beats what somebody wrote down. A source declares its rung once, on `IEventModelDefinitionSource.Provenance`; `EventModelDiscovery.DiscoverAsync` stamps it onto every slice the source returns.

Expand Down Expand Up @@ -214,6 +221,24 @@ Every rendered `EventModelElement` carries the same answer on its own `Provenanc

Merging two slices with different names throws — slices merge by name, and a mismatch means a bug in whoever assembled the list.

### Links from the specifications

A specification already says which command it exercises, so it can say which slice it specifies without anyone typing a link. A spec manifest is a list of `SpecificationBindingDescriptor`s: the spec's `{Feature}/{Scenario}` identity, the `CommandType` it sends, and optionally a `Domain`, a `Namespace` or an explicit `SliceName`. Bobcat emits it. The contract lives in JasperFx, so neither Bobcat nor Wolverine depends on the other.

`EventModelSpecifications.Link(model, manifest)` joins it onto an assembled model:

```cs
var linked = EventModelSpecifications.Link(model, manifest);
```

- **The join key is the command type.** A spec links to the one slice whose `CommandType` is its command.
- **Several slices handling one command** -- one per module -- are narrowed by the spec's `Domain`, then by its `Namespace` (a prefix of the slice's handler namespace). If that leaves exactly one, the spec links there. Otherwise it links nowhere, and an `UnresolvedSpecification` hotspot on the model names the candidates rather than guessing.
- **An explicit `SliceName`** replaces the inference. One naming a slice the model doesn't have is reported the same way.
- **A spec whose command no slice handles** is not linked and not reported: it specifies code that doesn't exist yet.
- **Derived links sit on the `Specified` rung.** A hand-typed `LinksToSpecification` that agrees records nothing. One that the specifications don't include loses, and is recorded as a `SourceDisagreement`.

The join runs over an *assembled* model, not as one more source, because it has to see every source's slices to find the one handling a command. The specifications also live in a test assembly the application host never loads, so where the join runs -- a monitor that receives the runner's manifest, or a model command handed the spec assembly -- is up to the caller.

## Serialization

The descriptors are plain records and serialize with `System.Text.Json` as-is. CritterWatch's wire shape is camelCase with camelCase string enums:
Expand Down
21 changes: 18 additions & 3 deletions docs/event-modeling/overlay.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@ public class IncidentServiceModel : EventModelDefinition
<sup><a href='https://github.com/JasperFx/jasperfx/blob/master/src/DocSamples/EventModeling/IncidentServiceEventModel.cs#L5-L35' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_incident_service_overlay' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

`Name` is the model this overlay contributes to. Several definitions can return the same name and they are all folded into one model — which is how IncidentService keeps its slice names in one class and its open questions in another. Leave `Name` alone and it defaults to the class name.
`Name` is the model this overlay contributes to. Leave it alone and the definition contributes to **the application's model**: the service name (`JasperFxOptions.ServiceName`, which Wolverine keeps in step with `WolverineOptions.ServiceName`), or the entry assembly's name when there is none. That is the name Wolverine and the stores give the model they derive from your code, so any number of definitions — one per chapter, one per module — fold onto the code without restating a name. Override `Name` only for an app that genuinely hosts several models; IncidentService does, which is why its samples say `"Helpdesk"`.

Each slice still records which definition declared it, on `Origin` (`event-model://{ClassName}`), so a disagreement with the code names the definition that lost.

## Slice names are the merge key

Expand All @@ -70,12 +72,23 @@ builder.Slice("ArchiveIncident").InDomain("Retention"); // Retention

A human-readable label for what starts the slice — "Agent clicks Close", "Customer submits the incident form". This is the one thing about a trigger that code genuinely cannot express. The trigger's *kind* (`Http`, `Grpc`, `MessageHandler`, `JobScheduler`, `Human`, `External`) and its CLR type are derived; only the sentence is yours.

### `LinksToSpecification(string)`
### `LinksToSpecification(string)` and `LinksToSpecification<TSpec>(scenario)`

Binds a specification to the slice by its `{Feature}/{Scenario}` identity.

Use this **only** for a specification the binding source cannot see for itself — a manual test plan, a partner's acceptance suite, something outside the compilation. Specs the Bobcat generator or a code-first runner can see are bound by them, with their step types resolved, and re-typing them here would just be a second copy waiting to go stale.

When the definition can reference the spec class, prefer the typed overload — the IDE navigates it and a rename keeps it in sync:

```csharp
model.Command<ApplyToVolunteer>()
.LinksToSpecification<apply_to_volunteer>(nameof(apply_to_volunteer.volunteer_application_submitted));
```

The identity is derived the way Bobcat derives it (`SpecificationIdentity.For`): the class's `[BobcatFeature]` title, or its name with one `Spec`/`Specs`/`Specification`/`Fixture` suffix removed, read as a sentence; then the method name read as a sentence. The example above links `apply to volunteer/volunteer application submitted`, exactly what the string overload would take.

Most definitions live in the app and their specs in a test project, so neither overload is reachable. There, let the specifications supply the links: see [Links from the specifications](/event-modeling/descriptors#links-from-the-specifications).

### `Hotspot(string)`

Records an open question. See **[Hotspots](/event-modeling/hotspots)**.
Expand All @@ -94,11 +107,13 @@ services.AddEventModel<IncidentServiceHotspots>();
// ...or every EventModelDefinition in an assembly
services.AddEventModelsFromAssembly(typeof(IncidentServiceModel).Assembly);
```

`AddDiscoveredEventModels(assembly)` does the same from a compile-time manifest: `JasperFx.SourceGenerator` emits `JasperFx.Generated.DiscoveredEventModels` into every assembly that references JasperFx.Events, listing each public, concrete, non-generic `EventModelDefinition` subclass with a public constructor and rooting those constructors for the trimmer. No type is enumerated at runtime.
<sup><a href='https://github.com/JasperFx/jasperfx/blob/master/src/DocSamples/EventModeling/EventModelUsageSamples.cs#L13-L22' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_registering_an_event_model' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

::: warning AOT
`AddEventModelsFromAssembly` walks `Assembly.ExportedTypes` and is marked `[RequiresUnreferencedCode]`. Applications publishing native AOT should register each definition explicitly with `AddEventModel<T>()`.
`AddEventModelsFromAssembly` walks `Assembly.ExportedTypes` when the assembly has no generated manifest, and is marked `[RequiresUnreferencedCode]`. Applications publishing native AOT should reference `JasperFx.SourceGenerator` in each assembly that declares definitions and call `AddDiscoveredEventModels(assembly)`, or register each definition explicitly with `AddEventModel<T>()`. An assembly built without the generator still falls back to the scan.
:::

For something small, skip the class entirely:
Expand Down
Loading
Loading