CloudEvents Standard
Hermodr uses the CloudEvents specification as its event model. Every event published through the framework is represented as a CloudEvent object from the CloudNative.CloudEvents SDK.
Why CloudEvents?
- Interoperability — CloudEvents is a CNCF specification adopted by Azure, Google Cloud, AWS, and many other platforms. Events can be consumed by any system that understands the standard.
- Schema-agnostic — The envelope is fixed; the
datapayload can be any content type. - Tooling ecosystem — Schema registries, event bridges, and observability tools often natively support CloudEvents.
The CloudEvent Envelope
A CloudEvent consists of a set of required and optional attributes:
| Attribute | Type | Description |
|---|---|---|
id | string | Unique identifier for this specific event occurrence |
source | Uri | Identifies the context in which the event happened (e.g. your service URL) |
specversion | string | Always "1.0" |
type | string | Describes the kind of event (e.g. "order.placed") |
datacontenttype | string? | MIME type of the data payload (e.g. "application/json") |
dataschema | Uri? | URI of the schema that the data conforms to |
subject | string? | Additional context about the subject of the event |
time | DateTimeOffset? | Timestamp when the event occurred |
data | object? | The event payload |
Event time semantics
In Hermodr, CloudEvent.time is the occurrence timestamp of the business fact, not the transport delivery time.
- Use
timeto answer when the event happened in domain terms. - Use transport/outbox/dead-letter metadata (
NextRetryAt,NextReplayAt, delivery headers) to answer when delivery was attempted. - Keep these concepts separate to preserve an accurate event record and make replay/audit timelines reliable.
When the event has no time, EventPublisher fills it through IEventSystemTime.UtcNow during enrichment.
This allows deterministic tests by replacing the clock via UseSystemTime<TClock>().
How Hermodr Populates the Envelope
When you call publisher.PublishAsync(data) with an annotated data object, the IEventFactory service reads the [Event] attribute and populates the envelope as follows:
| CloudEvent attribute | Source |
|---|---|
type | [Event] attribute's EventType property |
id | IEventIdGenerator (default: new GUID) |
source | EventPublisherOptions.Source |
time | IEventSystemTime.UtcNow |
dataschema | [Event] attribute's DataSchema, or EventPublisherOptions.DataSchemaBaseUri + event type |
datacontenttype | [Event] attribute's ContentType |
data | The annotated object itself |
Any additional CloudEvent attributes declared via [EventAttributes] (or AMQP-specific attributes) are merged in.
Required-attribute enforcement
After enrichment the publisher checks that the four required attributes (id, source, type, specversion) are non-null and non-empty. If any are missing, an InvalidCloudEventException is thrown before the event reaches any channel. This means:
typemust always be provided — it is never auto-filled.sourcemust be set on the event itself or viaEventPublisherOptions.Source.idis auto-generated byIEventIdGenerator, so it effectively never fails the check.specversionis always"1.0"in the CloudNative SDK.
Full payload validation (checking the
datafield against its declared schema) is deferred to a later milestone. See Schema Validation for the roadmap item.
Working with CloudEvent Directly
You can bypass the annotation system and construct a CloudEvent manually when you need full control:
using CloudNative.CloudEvents;
var systemTime = serviceProvider.GetRequiredService<IEventSystemTime>();
var @event = new CloudEvent
{
Id = Guid.NewGuid().ToString(),
Type = "com.acme.order.shipped",
Source = new Uri("https://orders.acme.com"),
Time = systemTime.UtcNow,
DataContentType = "application/json",
Data = new { OrderId = 42, TrackingNumber = "1Z999AA1" }
};
await publisher.PublishEventAsync(@event);