Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -397,3 +397,5 @@ FodyWeavers.xsd
# JetBrains Rider
*.sln.iml
.idea

samples/**/libs
30 changes: 30 additions & 0 deletions Deveel.Events.sln
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Deveel.Events.Publisher.Out
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Deveel.Events.Publisher.Outbox.EntityFramework.XUnit", "test\Deveel.Events.Publisher.Outbox.EntityFramework.XUnit\Deveel.Events.Publisher.Outbox.EntityFramework.XUnit.csproj", "{CB9A9A81-9768-4485-B28D-EC994D57F274}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Deveel.Events.Generators", "src\Deveel.Events.Generators\Deveel.Events.Generators.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Deveel.Events.Generators.XUnit", "test\Deveel.Events.Generators.XUnit\Deveel.Events.Generators.XUnit.csproj", "{B2C3D4E5-F6A7-8901-BCDE-F12345678901}"
EndProject

Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down Expand Up @@ -394,6 +398,30 @@ Global
{CB9A9A81-9768-4485-B28D-EC994D57F274}.Release|x64.Build.0 = Release|Any CPU
{CB9A9A81-9768-4485-B28D-EC994D57F274}.Release|x86.ActiveCfg = Release|Any CPU
{CB9A9A81-9768-4485-B28D-EC994D57F274}.Release|x86.Build.0 = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x64.ActiveCfg = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x64.Build.0 = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x86.ActiveCfg = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x86.Build.0 = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x64.ActiveCfg = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x64.Build.0 = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x86.ActiveCfg = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x86.Build.0 = Release|Any CPU
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|x64.ActiveCfg = Debug|Any CPU
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|x64.Build.0 = Debug|Any CPU
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|x86.ActiveCfg = Debug|Any CPU
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|x86.Build.0 = Debug|Any CPU
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.Build.0 = Release|Any CPU
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|x64.ActiveCfg = Release|Any CPU
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|x64.Build.0 = Release|Any CPU
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|x86.ActiveCfg = Release|Any CPU
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|x86.Build.0 = Release|Any CPU

EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
Expand Down Expand Up @@ -428,6 +456,8 @@ Global
{0141FD69-B2F4-4A57-9114-7EECEE378796} = {EDED427E-1408-4971-9FA9-EBFCACCEAC22}
{6592809F-328D-412C-839E-0A9783B4ABF2} = {537B0F0D-AC12-49A9-B54B-21456E9139CB}
{CB9A9A81-9768-4485-B28D-EC994D57F274} = {EDED427E-1408-4971-9FA9-EBFCACCEAC22}
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890} = {B34ECEB3-D2F9-459B-A3DE-99E04123E593}
{B2C3D4E5-F6A7-8901-BCDE-F12345678901} = {EDED427E-1408-4971-9FA9-EBFCACCEAC22}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {2F6B1794-D5DB-4788-A4E3-2786DA1F4359}
Expand Down
1 change: 1 addition & 0 deletions docs/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
* [OrderService — Minimal API + RabbitMQ](samples/aspnet-publisher-rabbitmq.md)
* [OrderService — In-Process Outbox + RabbitMQ](samples/outbox-inapp-rabbitmq.md)
* [OrderService — Split Outbox + MassTransit RabbitMQ](samples/outbox-relay-masstransit.md)
* [Event generation + MassTransit (console)](samples/event-genration-masstransit.md)

## Contributing

Expand Down
61 changes: 61 additions & 0 deletions docs/concepts/event-annotations.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,4 +127,65 @@ public class OrderPlacedData
- [RabbitMQ Channel — AMQP Annotations](../publishers/rabbitmq.md#amqp-annotations)
- [Schema from Annotations](../schema/from-annotations.md)

---

## Assembly-level generator attributes

When `Deveel.Events.Generators` is referenced as an analyzer, two optional
**assembly-level** attributes let you supply defaults that are baked directly
into the generated code at build time, eliminating runtime lookups entirely.

### `[EventDataSchemaUri]`

Tells the generator the base URI to use when building the `dataschema` CloudEvents
attribute for events that declare a `DataVersion` (rather than a full absolute URI):

```csharp
[assembly: EventDataSchemaUri("https://schemas.example.com/events")]
```

The generator appends `/{eventType}/{dataVersion}` and emits the result as a
`const string` inside the generated `partial class`. For example, given:

```csharp
[Event("order.placed", "2.0")]
public partial class OrderPlaced { ... }
```

the generator emits:

```csharp
private const string __dataSchema = "https://schemas.example.com/events/order.placed/2.0";
```

No runtime lookup via `EventGeneratorContext` is needed for the schema URI.

If the attribute is absent, the generator falls back to reading
`EventGeneratorContext.DataSchemaBaseUri` at call time (seeded from the active
`EventPublisher` instance's `EventPublisherOptions.DataSchemaBaseUri`).

### `[EventJsonSerializationOptions]`

Designates a type whose static `GetOptions()` method provides the
`JsonSerializerOptions` used when serialising event data inside `ToCloudEvent()`:

```csharp
[assembly: EventJsonSerializationOptions(typeof(MyApp.MyJsonOptions))]
```

The referenced type must expose:

```csharp
public static JsonSerializerOptions GetOptions();
```

The generator emits a direct static call to that method:

```csharp
Data = JsonSerializer.Serialize(this, global::MyApp.MyJsonOptions.GetOptions()),
```

If the attribute is absent, the generator reads `EventGeneratorContext.JsonSerializerOptions`
at runtime instead.

Both attributes are **independent** — you can use one, both, or neither.
61 changes: 61 additions & 0 deletions docs/concepts/event-creation.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,67 @@ public class OrderPlaced : IEventConvertible
When `PublishAsync<TData>` detects that `TData` implements `IEventConvertible`, it calls
`ToCloudEvent()` directly and bypasses `IEventFactory` entirely.

---

## Source-generated `IEventConvertible`

When `Deveel.Events.Generators` is referenced as a Roslyn analyzer, annotating a `partial`
class with `[Event]` is enough — the generator writes `ToCloudEvent()` for you:

```csharp
[Event("order.placed", "2.0", ContentType = "application/json")]
public partial class OrderPlaced
{
public string OrderId { get; init; } = "";
public decimal Total { get; init; }
}
```

The generator emits a second `partial class` file that implements
`IEventConvertible.ToCloudEvent()` with zero runtime reflection.

### How the schema URI is resolved

The generated code builds the `dataschema` URI through one of three paths, in priority order:

| Priority | Source | When used |
|----------|--------|-----------|
| **1. Compile-time const** | `[assembly: EventDataSchemaUri("https://…")]` + `DataVersion` | Assembly attribute present |
| **2. Compile-time const** | Absolute URI in `[Event("type", "https://…")]` | Full URI given directly on the event |
| **3. Runtime lookup** | `EventGeneratorContext.DataSchemaBaseUri` | No assembly attribute or full URI |

For path 3, `EventPublisher` pushes the value from `EventPublisherOptions.DataSchemaBaseUri`
into `EventGeneratorContext` for the duration of each `ToCloudEvent()` call, so every
publisher instance uses its own options without interfering with other instances.

### How JSON options are resolved

Similarly, the `Data` serialisation follows one of two paths:

| Priority | Source | When used |
|----------|--------|-----------|
| **1. Compile-time static call** | `[assembly: EventJsonSerializationOptions(typeof(MyOptions))]` | Assembly attribute present |
| **2. Runtime lookup** | `EventGeneratorContext.JsonSerializerOptions` | No assembly attribute |

The provider type must expose `public static JsonSerializerOptions GetOptions()`.

### Setting assembly defaults

Place either or both attributes at assembly level (any `.cs` file):

```csharp
// Bakes the full schema URI into a const string for every event with DataVersion.
[assembly: EventDataSchemaUri("https://schemas.example.com/events")]

// Emits a direct static call to MyJsonOptions.GetOptions() for every serialisation.
[assembly: EventJsonSerializationOptions(typeof(MyApp.MyJsonOptions))]
```

Both are optional and independent. You can use one, both, or neither.

See [Event Annotations](event-annotations.md#assembly-level-generator-attributes) for the full reference.


---

## `CreateEventFromData` — protected virtual hook
Expand Down
1 change: 1 addition & 0 deletions docs/samples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Each sample is self-contained and ships with its own infrastructure (via `docker
| [OrderService — Minimal API + RabbitMQ](aspnet-publisher-rabbitmq.md) | RabbitMQ | Annotated event classes, typed channels, `IEventPublisher` in a service |
| [OrderService — In-Process Outbox + RabbitMQ](outbox-inapp-rabbitmq.md) | RabbitMQ | Transactional Outbox with in-process relay; EF Core SQLite; `[AmqpExchange]` / `[AmqpRoutingKey]` annotations |
| [OrderService — Split Outbox + MassTransit RabbitMQ](outbox-relay-masstransit.md) | MassTransit / RabbitMQ | Split outbox across two processes; API has no transport dependency; external relay worker; MassTransit publish channel |
| [Event generation + MassTransit (console)](event-genration-masstransit.md) | MassTransit | Console sample showing source-generated `IEventConvertible` from `[Event]` and publish via `.AddMassTransit()` |

---

Expand Down
49 changes: 49 additions & 0 deletions docs/samples/event-genration-masstransit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Event generation + MassTransit (console)

This sample demonstrates a compile-time and runtime flow in one place:

- A `partial` event class is annotated with `[Event]`.
- `Deveel.Events.Generators` emits `IEventConvertible` during build.
- Two assembly-level attributes bake the schema URI and JSON options into the generated code at **compile time**.
- `IEventPublisher.PublishAsync(...)` sends through the MassTransit publish channel.

## Location

- `samples/event-genration/EventGeneration.Console`

## Key files

| File | Purpose |
|------|---------|
| `Program.cs` | Host setup, assembly-level generator attributes, publisher configuration |
| `SampleJsonOptions.cs` | Static `GetOptions()` provider referenced by `[EventJsonSerializationOptions]` |
| `Events/PersonRegistered.cs` | Annotated `partial` event class |

## Assembly-level attributes used

```csharp
// Bakes "https://schemas.example.com/events/{eventType}/{dataVersion}" as a const string.
[assembly: EventDataSchemaUri("https://schemas.example.com/events")]

// Generated ToCloudEvent() calls SampleJsonOptions.GetOptions() for serialisation.
[assembly: EventJsonSerializationOptions(typeof(SampleJsonOptions))]
```

## Run

```bash
cd samples/event-genration/EventGeneration.Console
dotnet run
```

You should see output that confirms:

1. the generated `IEventConvertible` path produced a `CloudEvent` with a fully resolved `dataschema`
2. the MassTransit channel published an `ICloudEventMessage`

## Notes

- The sample uses mocked `IPublishEndpoint` / `ISendEndpointProvider`, so no external broker is required.
- Both assembly attributes are optional and independent — remove either to fall back to the runtime `EventGeneratorContext` approach.
- The folder name intentionally follows the requested spelling: `event-genration`.

14 changes: 14 additions & 0 deletions samples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,20 @@ Each sample is self-contained: it has its own project file, configuration, and (
| Sample | Transport | Description |
|--------|-----------|-------------|
| [aspnet-publisher/OrderService](aspnet-publisher/OrderService/README.md) | RabbitMQ | ASP.NET Core Minimal API microservice that publishes Order lifecycle events to a RabbitMQ exchange |
| [event-genration](event-genration/README.md) | MassTransit (mocked endpoints) | Console app showing `[Event]` source-generation to `IEventConvertible` and publish through a MassTransit channel |

---

## Shared build scripts

Sample-level dependency scripts (`build-libs.sh`, `build-libs.ps1`, `build-libs.bat`) are thin wrappers.
They forward the project list and output folder to shared core scripts in this folder:

- `build-libs-core.sh`
- `build-libs-core.ps1`
- `build-libs-core.bat`

This keeps build/copy logic centralized and avoids duplication across samples.

---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,24 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="CloudNative.CloudEvents" Version="2.8.0" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.5" />
</ItemGroup>

<ItemGroup>
<!-- Deveel.Events: core publisher + RabbitMQ channel + AMQP annotations -->
<ProjectReference Include="..\..\..\src\Deveel.Events.Publisher.RabbitMq\Deveel.Events.Publisher.RabbitMq.csproj" />
<Reference Include="Deveel.Events.Annotations">
<HintPath>..\libs\Deveel.Events.Annotations.dll</HintPath>
</Reference>
<Reference Include="Deveel.Events.Publisher">
<HintPath>..\libs\Deveel.Events.Publisher.dll</HintPath>
</Reference>
<Reference Include="Deveel.Events.Amqp.Annotations">
<HintPath>..\libs\Deveel.Events.Amqp.Annotations.dll</HintPath>
</Reference>
<Reference Include="Deveel.Events.Publisher.RabbitMq">
<HintPath>..\libs\Deveel.Events.Publisher.RabbitMq.dll</HintPath>
</Reference>
</ItemGroup>

</Project>
Expand Down
Loading
Loading