# Kanject.Core.Events

Say "this happened" without choosing a transport. Declare an event with `[EventTopic]`, publish it through `IEventPublisher`, and let the host decide where it goes: handlers in the same process by default, SNS through ServerlessEventHub, or your own broker. Free under the Community licence.

## Install

```bash
dotnet add package Kanject.Core.Events
dotnet add package Kanject.Core.Events.Annotations   # the generator
```

A library that only declares events needs `Kanject.Core.Events.Abstractions`, which depends on nothing but the BCL and `Microsoft.Extensions.DependencyInjection.Abstractions` — so an event contract never drags a transport into the code that declares it. Targets .NET 8, 9 and 10.

## Declare an event

```csharp
using System.Text.Json.Serialization;
using Kanject.Core.Events.Abstractions.Attributes;

[assembly: EventTopicOwner("orders-svc")]

[EventTopic("order_placed", Description = "An order was placed")]
public sealed record OrderPlaced(string OrderId, int Lines);

// One per assembly: the source-generated serializer every event travels through.
[EventJsonContext]
[JsonSerializable(typeof(OrderPlaced))]
internal partial class OrderEventsJsonContext : JsonSerializerContext;
```

- The topic is 1–200 lowercase letters, digits, `_` and `-` — the same name on every transport (SNS refuses a `.` in a topic name).
- `Version` (default 1) is the payload contract's version. Two versions of one topic can be declared side by side, each with its own payload type.
- `Owner` names the generated class (`orders-svc` → `OrdersSvcEvents`); `[assembly: EventTopicOwner]` sets it for every event that names none.
- Exactly one `[EventJsonContext]` per assembly lists every event with `[JsonSerializable]`. Its options decide the JSON on the wire.

## Publish

The generator writes a typed `Publish{Name}Async` for each event and one `Add{Owner}Events()` per owner. `EventMetadata` carries what a caller may set: the event id (minted if absent), the tenant, when it occurred, and up to five extra attributes.

```csharp
// Generated from the owner: the declarations, into the container's event catalog.
services.AddOrdersSvcEvents();

public class CheckoutService(IEventPublisher publisher)
{
    public Task PlaceAsync(Order order) =>
        publisher.PublishOrderPlacedAsync(
            new OrderPlaced(order.Id, order.Lines.Count),
            new EventMetadata { TenantId = order.TenantId });
}
```

## Handle it in-process

`AddEvents()` registers the in-process publisher and its handlers. It only registers the publisher when no other is registered, so a library can call it safely: a host that registers a real transport keeps it, in either order.

```csharp
services.AddEvents(events => events
    .On(OrdersSvcEvents.OrderPlaced).Handle<SendConfirmation>()   // typed
    .OnTopic("order_placed").Handle<AuditTrail>()                 // raw: the envelope and its JSON
    .OnAnyTopic().Handle<EventLog>());                            // every topic

public sealed class SendConfirmation(IMailer mailer) : IEventHandler<OrderPlaced>
{
    public Task HandleAsync(OrderPlaced @event, EventEnvelope envelope,
        CancellationToken cancellationToken) =>
        mailer.SendConfirmationAsync(@event.OrderId, cancellationToken);
}
```

- Handlers run **before** `PublishAsync` completes, in registration order, one at a time, each in its own DI scope.
- A failing handler is logged (`KANEVT062`) and the next one runs. With `HandlerFailureMode.Throw`, every handler still runs and the failures are thrown together.
- A handler that publishes runs inline, up to `MaxDispatchDepth` (8 by default).
- A topic with no handler is dropped and logged once (`KANEVT060`).

> **In-process delivery is not durable:** Delivery is at most once: a process that stops part-way through a publish loses the remaining deliveries. When an event must go out exactly when a write commits, queue it through the [ChangeFeed outbox](https://www.kanject.com/docs/core-changefeed/) instead.

## Choose the transport

`AddEventPublisher<T>()` registers the host's publisher and replaces any other, so exactly one remains whatever the registration order. The in-process handlers then stop running — the transport delivers instead.

```csharp
// SNS, through BaaS ServerlessEventHub
services.AddServerlessEventHubEventPublisher("orders-svc");

// …or your own broker: write envelope.Payload as the body and
// envelope.GetHeaders() as the headers, then prove it with the TestKit.
services.AddEventPublisher<KafkaEventPublisher>();
```

Every publisher keeps one contract, and `Kanject.Core.Events.TestKit` checks it: completion means the transport accepted the event; failure throws, never returns; the envelope goes out unchanged; a retried event keeps its id, so consumers can deduplicate on it. Kanject BaaS modules — Identity, Wallet, Workflow and others — publish their product events through this contract, so the one publisher a host registers carries theirs too.

## On the wire

- `EventTopic` — the topic
- `EventId` — the event id (`evt_` and 32 hex digits unless you set one)
- `EventVersion` — the payload contract's version
- `OccurredAt` — ISO 8601, UTC
- `kanject-tenant-id` — only when a tenant is set
- The body is the payload's JSON, byte for byte.

## What ships with it

- `Kanject.Core.Events.Abstractions` — `[EventTopic]`, `IEventPublisher`, the envelope, handlers and the per-container `IEventCatalog`
- `Kanject.Core.Events` — the in-process default publisher and handler registration
- `Kanject.Core.Events.Annotations` — the generator: typed publishers, definitions, `Add{Owner}Events()`, and an `<Assembly>.eventtopicschema.md` catalogue of every event
- `Kanject.Core.Events.TestKit` — the contract suite every publisher passes, and a recording test double
- Reflection-free and `IsAotCompatible`: payloads serialize only through your source-generated context

> **Free under the Community licence:** `Kanject.Core.Events`, its abstractions, generator and TestKit are published on public NuGet and free under the Community licence — individuals, open source, education, and companies under $250K.

---
_Source: https://www.kanject.com/docs/core-events/ · Kanject Docs_
