C#

Designing C# event contracts: payloads, notifications and decisions

A C# event only fixes a delegate signature; it says nothing about what happened, when subscribers run, or what an empty subscriber list means. Nick Cosentino separates notification events (a past-tense fact) from decision events (a future-tense question) and argues for stable payloads, a single-owner callback and honest failure boundaries.

Published
Reading time
13 min read
agent run #24
agent run #24
Go to original devleader.ca

Report this story

Is something wrong with the summary, or do you own this content? Your message comes straight to my inbox.

Type of message

Key points

  • Event names describe intent but cannot enforce it: Saved alone does not reveal whether state is committed or still proposed.
  • Keep payloads to stable facts such as id and version instead of handing out the whole entity behind get-only properties.
  • A field-like event with no subscribers has a null delegate, so the raise is a silent no-op that the contract should state.
  • Decision events need one owner: the sample requires a Func<int,bool> and rejects a combined invocation list with several entries.
  • A synchronous handler exception stops later handlers and occurs after the state change; the publisher does not roll back.
  • An async void handler cannot answer a decision, since no returned task lets the raiser observe completion or exceptions.

An event's meaning comes from the method that raises it, not from its name. Cosentino separates past-tense notifications like Stored from future-tense decisions that gate an operation, and argues a payload should carry stable facts only: the sample's DocumentStoredArgs exposes a document id and version behind get-only properties instead of handing out the whole mutable document.

Where handlers can influence the outcome, one owner must answer. The second sample requires a Func<int,bool> callback in the constructor and rejects a delegate combining several entries. With no subscribers the null delegate is skipped and the operation still succeeds. A synchronous handler exception stops the remaining handlers and arrives after the state change, and an async void handler cannot decide anything.

Real-world use

  • A file-upload service that raises a Stored notification after a blob is committed, letting audit and telemetry handlers react without touching the write path.
  • A pricing engine that runs one approval callback (Func<decimal,bool>) before applying a plan change and writes nothing when approval fails.
  • A desktop app listening to document changes where one failing UI handler must not silently cancel unrelated reactions such as audit writes.
  • A multi-tenant API where observers are optional subscribers while authorization stays a required injected dependency.

Why it matters

Deciding up front whether an event notifies or decides makes the empty-subscriber behaviour and the effect of a throwing handler predictable. Otherwise correctness quietly depends on wiring that the public contract never mentions.

This summary was generated by the agent from the original post on Nick Cosentino (Dev Leader). The full text and all rights belong to its author and publisher. Go to original

Related stories

Feedback

Found a bug or have an idea? Your message lands straight in my inbox.

Type of message