Skip to content

Process Flow of SetGroupCommand

Overview

This document describes the end-to-end flow for handling a SetGroupCommand against a sporting event aggregate, including validation, change detection, event persistence, and projection behavior.


Command parameter

SetGroupCommand (a DomainCommand<SportingEventId>) carries a single payload field:

  • Group: required (required string); the new group value to set.

The command is gated with [RequiresRoles(RoleDefinition.ContentManager)].


Aggregate receive and validation

SportingEventAggregate.Execute(SetGroupCommand) runs the following steps in order:

  • Validation first: Validate(SetGroupCommand) is invoked before any event construction. It throws InvalidCommandException when:
  • the event has not been created (!isSportingEventCreated) -- "SportingEvent not created";
  • the incoming Group is null, empty, or whitespace -- "SportingEvent Group is null, empty, or whitespace";
  • the event is archived (isArchived) -- "SportingEvent is archived".
  • Trim: the incoming Group is trimmed (command.Group.Trim()) before comparison and before it becomes the event payload.
  • Change detection: an event is raised only when sportingEventDetails is non-null and the trimmed Group differs from the current sportingEventDetails.Group using a case-insensitive comparison (StringComparison.OrdinalIgnoreCase).
  • State update: Apply(GroupSetEvent) rebuilds sportingEventDetails with the new Group and sets groupManuallyOverridden = true, recording that the group was set explicitly rather than derived.

Change detection and event creation

  • Compare the trimmed incoming Group against the existing sportingEventDetails.Group using StringComparison.OrdinalIgnoreCase.
  • If sportingEventDetails is non-null and the values differ, construct and raise GroupSetEvent. The event (a DomainEvent<SportingEventId>) carries a single payload field: Group (the trimmed value).
  • If the values are identical (case-insensitive), or sportingEventDetails is null, no event is raised.

Note that the archive guard is enforced on the write side only: Validate rejects the command outright when isArchived is true, so an archived event never produces a GroupSetEvent.


Event persistence and publication

  • A raised GroupSetEvent is appended to the event stream via the command executor/store.
  • The event is then published to the event bus so projections and subscribers receive it.

Projection behavior and read model update

  • SportingEventContractBuilder is a SingleStreamProjection<SportingEventContract, Guid> that includes GroupSetEvent (alongside the create, update, start-time, archive, and restore events) and updates the SportingEventContract read model.
  • Upon receiving GroupSetEvent, Apply(GroupSetEvent, SportingEventContract) produces an updated contract unconditionally:
  • sets contract.Group to domainEvent.Group;
  • sets contract.Version to domainEvent.Version.
  • There is no data-source gate on this apply: every GroupSetEvent updates the read model's group.

Archive state on the read model is managed by separate events: SportingEventArchivedEvent sets IsArchived = true and SportingEventRestoredEvent sets IsArchived = false. The GroupSetEvent apply does not touch IsArchived; the archive check that prevents a group change on an archived event lives in the aggregate's Validate, upstream of the projection.


Read side visibility

  • Consumers of SportingEventContract observe the updated Group for every GroupSetEvent, regardless of originating data source.
  • Because archived events are rejected at command validation, a GroupSetEvent is only ever emitted (and therefore only ever applied to the read model) for non-archived events.