Osy#betaRun it on Osyrin: osyrin.com

Reference / Function

[Event] — what happens to a record, declared once

[Event] / [Event("Label")] public sealed class <Name> { public <Record> <Member>; … public override string Sentence() { … } } · Raise(new <Name> { … }) · [On] void <Handler>(<Name> e) · [On] void <Handler>(Event e) · Events.Catalog() · Events.Name(e) · Events.Label(e) · Events.Type(e) · Events.About(e) · Events.Write(e) · Events.Read(text) · Events.Delivery()

An event is a class marked `[Event]`: something that happens to a record, declared once by the code that owns the record. It is raised where it happens with `Raise(new QuoteAccepted { Quote = q })`, and any package that sees the class — or the app — hears it with an `[On]` function, after the commit that raised it. Neither side names the other. Every event is an `Event`: it says itself in a sentence, read in the reader's language when it is read, and a kit that works on ANY event — a timeline, a rule engine, webhooks — hears them all and asks `Events.*` what each one is.

preview4 examples compiled by CIfunctioneventscomposition

Summary#

A kit — or an app — says what HAPPENS to its records as classes:

/// The customer accepted a quote.
[Event("Quote accepted")]
public sealed class QuoteAccepted { public Quote Quote; }

It raises one where it happens, and anybody who cares hears it:

Raise(new QuoteAccepted { Quote = q });            // in the code that accepts the quote

[On] void WinOnAccept(QuoteAccepted e) { … }       // in any package that sees QuoteAccepted, or in the app

The raiser does not know who listens, and a listener does not call the raiser. That is what lets two kits written apart work together: a kit that depends on another may hear its events, and an app hears every kit's.

Signature#

[Event]                          // its label is its name in words — "Quote accepted"
[Event("Quote accepted")]        // …or the words you give it
public sealed class <Name> {     // an `Event` — the base is implicit
  public <Record> <Member>;      // the record it is about — its first record-typed member
  public <T> <Member>;           // anything else a handler needs to know — values, more records
  public override string Sentence() { … }   // optional — what it says; its label when not overridden
}

// What a kit that works on ANY event asks — server-side:
List<EventInfo> Events.Catalog()     // every PUBLIC event in the app and its packages: Type, Label, Name, About
string Events.Name(Event e)          // its name on the wire — "order-shipped", "knowledge-base.article-published"
string Events.Label(Event e)         // its label, in the reader's language
string Events.Type(Event e)          // its class's full name
Entity? Events.About(Event e)        // the record it is about
string Events.Write(Event e)         // the event as text, to keep
Event? Events.Read(string text)      // …and back — e.Sentence() then says it as the reader reads it
EventDelivery? Events.Delivery()     // inside a handler: this event's Id, Root, Depth, RaisedAt, Cause

Description#

What makes a class an event?#

The [Event] mark. Without it, Raise(new X { … }) is refused and names the mark to add; with it, X is an event everywhere the class can be seen. An event is a class, never an entity — it is what happened, not a row you keep — and it carries whatever its handlers need: the record it is about, who did it, the reason, a value from before.

Its label is what people read it by — in a rule's trigger, on a timeline — and defaults to the class name in words (QuoteAccepted → "Quote accepted").

Which record is it about?#

The first record-typed member. The platform keeps it beside the event, so what happened to a record can be found from the record.

What happens when nobody listens?#

Nothing. The compiler knows every handler in the app and its packages, so an event no [On] hears has no delivery: raising it writes no row and sends no statement. A kit can raise every event a person might care about, and an app pays only for the ones it hears.

What does it say?#

Every [Event] class is an Event, and Sentence() is what it says — on a timeline, in a bell, in a mail. Not overridden, it says its label; overridden, it says more, from its own members:

public override string Sentence() { return $"{Quote.Customer.Name} accepted the quote."; }

It runs when it is READ, as whoever reads it, in their language — so a kit keeps the event (Events.Write) and says it later (Events.Read(kept).Sentence()): a name changed since is said as it is now, an erased customer's name is never in a stored sentence, and a record the reader may not read is never put in front of them — the label is said instead. What Events.Write gives is JSON, so it may be kept in a Json column as well as in a string, and Events.Read takes either.

A kit that hears every event#

[On] void H(Event e) hears every event the app and its packages raise — its own, other kits', the app's — and asks Events.* what each one is: its name, its label, its type, the record it is about. That is how a timeline, a rule engine or webhooks work on events they have never heard of ([On] — hear an event, after the commit that raised it).

Which events are offered to people?#

Events.Catalog() lists every public [Event] class — what a rule may run on, what a webhook may be subscribed to. An internal event is its declarer's own business (a notice it tells somebody, say): handlers hear it, nobody is offered it. A PACKAGE's internal event is heard by that package's own handlers only — a catch-all in another kit or in the app cannot name it and never hears it. An app's own internal event is heard by every handler, catch-alls included.

Its name on the wire#

Events.Name(e) is what a subscription stores and a receiver reads: the app's own event by its class's name in kebab case (OrderShipped → order-shipped), a package's prefixed with the package's area (Osysharp.KnowledgeBase.ArticlePublished → knowledge-base.article-published), so two kits' events never share a name.

Where is it written down?#

An event some handler hears is written in the same commit as the work that raised it — so an event exists exactly when that work does, and one raised by work that was rolled back was never raised. After the commit each handler runs, in order, each on its own; what ran, and any handler that failed or was cut from a loop, is recorded with the event. Raise(e) — say that something happened and [On] — hear an event, after the commit that raised it say how. The record is kept in the platform's Osysharp.EventOutbox tables, which an app carries only once some handler is in it — its own or a package's. An app that raises events nobody hears has no outbox, and its raises cost nothing.

Examples#

A record that says what happened to it#

The shop raises OrderShipped where it ships; a handler elsewhere in the app hears it after the commit.

entity Order {
  [Required, MaxLength(40)] string Code;
  bool Shipped;
  security { allow read, create, update when IsAnonymous || IsAuthenticated; }
}

entity ShippingNote {
  [Required, MaxLength(100)] string Says;
  security { allow read, create when IsAnonymous || IsAuthenticated; }
}

/// An order left the warehouse.
[Event("Order shipped")]
public sealed class OrderShipped { public Order Order; }

void Ship(Order o) {
  o.Shipped = true;
  Raise(new OrderShipped { Order = o });
}

[On]
void NoteTheShipment(OrderShipped e) { new ShippingNote { Says = "shipped " + e.Order.Code }; }
[Test]
void A_shipped_order_is_heard_once_its_commit_lands() {
  var o = new Order { Code = "A-1" };
  Ship(o);
  Background.Settle();                           // the work commits — and the handler runs after it
  Assert.Equal("shipped A-1", ShippingNote.Single().Says);
}

Every event, heard by one handler that names none#

LogEverything hears both events and says each — OrderShipped in its own words, OrderReturned by its label.

entity Parcel {
  [Required, MaxLength(40)] string Code;
  security { allow read, create, update when IsAnonymous || IsAuthenticated; }
}

entity LogLine {
  [Required, MaxLength(200)] string Says;
  security { allow read, create when IsAnonymous || IsAuthenticated; }
}

[Event("Parcel shipped")]
public sealed class ParcelShipped {
  public Parcel Parcel;
  public override string Sentence() { return $"Parcel {Parcel.Code} left the warehouse."; }
}

[Event]
public sealed class ParcelReturned { public Parcel Parcel; }

[On]
void LogEverything(Event e) { new LogLine { Says = Events.Name(e) + ": " + e.Sentence() }; }

void Ship(Parcel p) { Raise(new ParcelShipped { Parcel = p }); }
void TakeBack(Parcel p) { Raise(new ParcelReturned { Parcel = p }); }
[Test]
void Every_event_is_heard_and_says_itself() {
  var p = new Parcel { Code = "A-1" };
  Ship(p);
  TakeBack(p);
  Background.Settle();
  Assert.Equal(["parcel-returned: Parcel returned", "parcel-shipped: Parcel A-1 left the warehouse."],
               LogLine.OrderBy(l => l.Says).Select(l => l.Says).ToList());
}

See also#

Related

Raise(e) — say that something happened

`Raise(e)` says that the event `e` happened — an instance of a class marked `[Event]`. It is written in the same commit…

[On] — hear an event, after the commit that raised it

`[On]` on a function makes it a HANDLER of the event its one parameter is. Every handler of an event — in the app and…

[Action] — what an app offers to be done, by name

Mark a function `[Action]` and the app offers it to be done BY NAME — by a rule, a macro, a tool — with everything a…

Records — what any record is called, where it opens, who may read it

`Records.*` answers facts about a row of ANY type from what the app declared — what it is called (`[Title]`), where it…

Entity

`Entity` is a reference to a row of ANY type the app keeps — what a notification, a comment or a question "about some…

try / catch / finally

Handles a fault. C#'s syntax, including typed catches, catch filters and finally. The rows written inside a try block…

The RecordTimeline kit — what happened to a record, on its own timeline

`Osysharp.RecordTimeline` puts what happened to a record on its timeline: every event the app and its kits raise about…

The Automations kit — rules that do an app's routine work when something happens

`Osysharp.Automations` runs rules — WHEN something happens to a record (any event about it, or every hour), IF the…

The Webhooks kit — telling other systems, signed, when something happens

`Osysharp.Webhooks` sends other systems a signed JSON document each time an event happens to a record — any public…

The Notifications kit — telling a person that something needs them

`Osysharp.Notifications` tells a person that something needs them — in the app, a bell with a live count and a list…