Contracts
Cratis.Showrunner.Marquee.Contracts holds the public events a product receives announcements through. The package references only Cratis.Chronicle, so a publisher and a product can both depend on it without depending on the rest of Showrunner.
Rules every contract follows
Section titled “Rules every contract follows”- An announcement belongs to a product, not to a tenant. Announcements of a product share one product-wide stream, so
AnnouncementIdis a payload field, not the event source id. This is the one exception to carrying aTenantIdin every payload: tenant ids appear only inside anAudiencethat names tenants. - Every event is
[Public], and none names an origin store. There is no[EventStore]attribute on any contract or on the assembly, because store names vary by deployment. Each side subscribes to the other explicitly. - Events are named for what happened, and no name is shared with a private event.
- Nothing is nullable. A value that does not apply is empty:
ModuleKey.NotSet,PlanId.NotSet, no tenants. An announcement with no end usesActiveUntil.Indefinitely. - The window is exact UTC times.
ActiveFromandActiveUntilareDateTimeOffsetconcepts, not whole days, so a maintenance window can be announced to the minute.
Namespaces are relative to Cratis.Showrunner.Marquee.Contracts. TenantId sits in the root namespace.
Events
Section titled “Events”| Event | Namespace | Payload |
|---|---|---|
AnnouncementPublished | Announcements | AnnouncementId; Product; Audience; Title: AnnouncementTitle; Body: AnnouncementBody; Display; Importance; ActiveFrom; ActiveUntil |
AnnouncementRevised | Announcements | AnnouncementId; Product; Title; Body; Importance; ActiveFrom; ActiveUntil |
AnnouncementAudienceChanged | Announcements | AnnouncementId; Product; Audience |
AnnouncementRetracted | Announcements | AnnouncementId; Product |
AnnouncementRevised states the full new text and window, so a reader never combines it with earlier events. It does not change the audience or the display; AnnouncementAudienceChanged changes the audience.
Shared types
Section titled “Shared types”| Type | Namespace | Shape |
|---|---|---|
AnnouncementId | Announcements | ConceptAs<Guid> |
Product | Announcements | Text; the product an announcement is for |
AnnouncementTitle, AnnouncementBody | Announcements | Text; the body is Markdown |
ActiveFrom, ActiveUntil | Announcements | ConceptAs<DateTimeOffset> |
Display | Announcements | Banner = 0, DialogOnce = 1, DialogEverySession = 2 |
Importance | Announcements | Information = 0, Warning = 1, Critical = 2 |
Audience | Audiences | Kind: AudienceKind; TenantIds: IEnumerable<TenantId>; Module: ModuleKey; Plan: PlanId |
AudienceKind | Audiences | Everyone = 0, Tenants = 1, HoldersOfModule = 2, OnPlan = 3 |
ModuleKey, PlanId | Audiences | Text |
TenantId | root | The tenant’s namespace name |
Audiences
Section titled “Audiences”Build an audience with its factory:
Audience.Everyone();Audience.Tenants("acme", "globex");Audience.HoldersOfModule("reports");Audience.OnPlan("pro");Kind says which of the other values applies. The rest are empty, so the payload has no nulls.
Plugging in who holds a module or is on a plan
Section titled “Plugging in who holds a module or is on a plan”Everyone and Tenants are decided from the audience itself. HoldersOfModule and OnPlan are not: whether a tenant holds a module or is on a plan belongs to whatever sells the product. Marquee therefore depends on one abstraction, IAudienceEntitlements, and on nothing that provides it:
public interface IAudienceEntitlements{ ValueTask<bool> HoldsModule(TenantId tenantId, ModuleKey module); ValueTask<bool> IsOnPlan(TenantId tenantId, PlanId plan);}Ask an audience whether it reaches a tenant with Includes:
var reaches = await announcement.Audience.Includes(tenantId, entitlements);A host that has an entitlement source, such as Box Office or its own tables, implements the interface and registers it. A host without one passes NoAudienceEntitlements.Instance: it still gets Everyone and Tenants audiences, and module and plan audiences reach nobody.
Released generations
Section titled “Released generations”EventSchemas.snapshot.json records the schema of every event. A specification compares the code with it, so a changed schema fails the build until the snapshot is regenerated on purpose:
MARQUEE_UPDATE_EVENT_SCHEMA_SNAPSHOT=1 dotnet test Source/DotNET/Marquee.Contracts/Marquee.Contracts.csprojReleasedEventSchemas.json lists the generations that have shipped. A listed generation must stay in the snapshot and in code, and its schema can never change: a released event needs a new generation and a migration instead.