Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
4468ee2
Preserve core options when adding worker
george2006 Jul 24, 2026
eaa992c
Validate TinyEvents option values
george2006 Jul 24, 2026
d3d5c2d
Stop processing when canceled after claim
george2006 Jul 24, 2026
3359323
Add generated event dispatcher contract
george2006 Jul 24, 2026
87f5b03
Emit generated event dispatchers
george2006 Jul 24, 2026
2c88144
Dispatch events without reflection
george2006 Jul 24, 2026
7bafa2b
Remove event type descriptor registrations
george2006 Jul 24, 2026
5be5279
Cover loaded consumer assembly contributions
george2006 Jul 24, 2026
f4fddb0
Document generated dispatcher bootstrap behavior
george2006 Jul 24, 2026
8fdb37b
Handle lost outbox leases explicitly
george2006 Jul 24, 2026
184a7d9
Keep worker running after iteration failures
george2006 Jul 24, 2026
f35be65
Document worker lease loss behavior
george2006 Jul 24, 2026
72b8f1e
Document retry attempt semantics
george2006 Jul 25, 2026
c36bb0e
Document unknown event type handling
george2006 Jul 25, 2026
4e0c393
Align SQL Server EF outbox string lengths
george2006 Jul 25, 2026
2313b8f
Document EF Core worker transaction behavior
george2006 Jul 25, 2026
d541b53
Document provider schema parity
george2006 Jul 25, 2026
158f2d8
Log outbox processing outcomes
george2006 Jul 25, 2026
51d6408
Hide provider implementation details
george2006 Jul 25, 2026
5af2324
Use one package release train
george2006 Jul 25, 2026
8d0ab20
Add local package smoke validation
george2006 Jul 25, 2026
28e3958
Align PostgreSQL lease-loss runtime tests
george2006 Jul 25, 2026
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
110 changes: 0 additions & 110 deletions .github/workflows/release-package.yml

This file was deleted.

11 changes: 10 additions & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,14 @@
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<PropertyGroup>
<Version>0.1.0-alpha.2</Version>
<Authors>Jorge Durban Antunano</Authors>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageProjectUrl>https://github.com/george2006/TinyEvents</PackageProjectUrl>
<RepositoryUrl>https://github.com/george2006/TinyEvents</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageReadmeFile>README.md</PackageReadmeFile>
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
</PropertyGroup>
</Project>

6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ TinyEvents is deliberately different:

- `PublishAsync` stores an outbox message.
- Consumers run later through the processor or hosted worker.
- Incremental source generation registers consumers and event type descriptors automatically.
- Incremental source generation registers consumers and event dispatchers automatically.
- Runtime dispatch resolves consumers from Microsoft dependency injection.
- Worker claiming is database-backed and lease-based.
- Delivery is at-least-once, not exactly-once.
Expand Down Expand Up @@ -139,7 +139,7 @@ public sealed class SendWelcomeEmail : IEventConsumer<UserCreated>
The source generator discovers concrete closed consumers and emits:

- `IEventConsumer<TEvent>` DI registrations
- event type descriptors for deserialization
- event dispatchers for deserialization and consumer invocation
- a module-initialized contribution to TinyEvents bootstrap

No runtime assembly scanning is required, and normal consumers do not need manual DI registration.
Expand Down Expand Up @@ -265,7 +265,7 @@ TinyEvents is intentionally small.
- Domain/application event handling with outbox reliability.
- Plain event objects, no marker interface.
- No runtime scanning.
- Source generation for consumer registration and event type descriptors.
- Source generation for consumer registration and event dispatchers.
- Contribution-based bootstrap for generated registrations.
- Provider isolation.
- Database-backed lease claiming.
Expand Down
22 changes: 17 additions & 5 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ TinyEvents is designed to stay small enough to reason about.
- Providers are isolated in separate class libraries.
- No runtime assembly scanning.
- Source generation removes registration and event-map boilerplate.
- Generated contributions register consumers with dependency injection.
- Generated contributions register consumers and event dispatchers with dependency injection.
- Delivery is at-least-once, not exactly-once.
- Database-backed claim leases support multiple workers.
- Consumers should be idempotent.
Expand Down Expand Up @@ -115,19 +115,27 @@ EF Core publishing adds the message to the current `DbContext`.

ADO.NET publishing inserts the message through the current application transaction.

EF Core worker claim and mark commands use the scoped `DbContext` relational connection and attach to `DbContext.Database.CurrentTransaction` when one exists. TinyEvents does not create or complete EF Core transactions for worker operations.

## Processing Flow

1. Resolve the current worker id.
2. Claim pending or expired processing messages.
3. Resolve event type through generated event descriptors.
3. Resolve the event dispatcher for the stored event type.
4. Deserialize the payload.
5. Resolve all `IEventConsumer<TEvent>` instances from DI.
6. Invoke consumers.
6. Invoke consumers through the generated dispatcher.
7. Mark processed if all consumers succeed.
8. Mark failed or scheduled for retry when a consumer fails.

One outbox message represents one event, not one message per consumer. If one consumer fails, the event message is retried.

If marking a message as processed or failed no longer updates a row because the worker lost ownership of the processing lease, the processor leaves the message alone and continues. Lease loss is not recorded as a consumer failure.

Failures that are recorded through `MarkFailedAsync` advance the message attempt count. Before `MaxAttempts` is reached, the message is made pending again and delayed until `NextAttemptAtUtc`. When `MaxAttempts` is reached, the message is marked failed and no next attempt is scheduled.

Unknown event types are processing failures because the processor cannot resolve a generated dispatcher for the stored event type.

## Claiming Contract

```csharp
Expand Down Expand Up @@ -165,6 +173,8 @@ public interface ITinyOutboxStore

Provider claiming must be atomic. Query-then-update claiming is not acceptable for DB providers.

Provider completion and failure updates must validate affected row counts. A mark operation that updates no rows means the worker no longer owns a processing lease for that message.

New database providers must implement atomic claiming safely for their database engine. Query-then-update is not acceptable for multi-worker processing.

## Bootstrap
Expand All @@ -178,6 +188,8 @@ The contribution system is the bridge between compile-time discovery and runtime
1. The generator emits an `ITinyEventsContribution`.
2. A module initializer adds it to `TinyEventsBootstrap`.
3. `UseTinyEvents` or a provider registration method applies contributions.
4. Consumers and event type descriptors become normal DI services.
4. Consumers and event dispatchers become normal DI services.

TinyEvents does not scan assemblies or load consumer assemblies at runtime. Contributions are available only after the assembly that contains them has been loaded and its module initializer has run. Call TinyEvents registration after consumer assemblies are loaded.

Runtime processing does not use a custom consumer registry. It resolves consumers directly from `IServiceProvider`.
Runtime processing does not use a custom consumer registry. It resolves consumers directly from `IServiceProvider` through generated dispatchers.
6 changes: 6 additions & 0 deletions docs/ef-core.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,9 @@ Worker claiming is database-specific:

- SQL Server uses SQL Server locking hints and atomic update/output SQL.
- PostgreSQL uses `FOR UPDATE SKIP LOCKED` inside an atomic update/returning SQL statement.

Worker claim and mark operations use the relational connection from the scoped `DbContext`.

If `DbContext.Database.CurrentTransaction` exists, TinyEvents attaches the claim or mark command to that transaction. TinyEvents does not begin, commit, or roll back an EF Core transaction for worker operations.

The hosted worker creates a fresh dependency-injection scope for each processing iteration. In normal hosted-worker usage, claim and mark commands run without an application transaction unless your application explicitly creates one in that worker scope.
4 changes: 2 additions & 2 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ services.UseSqlServerEntityFrameworkCoreOutbox<AppDbContext>();

`UseSqlServerEntityFrameworkCoreOutbox<TDbContext>` registers TinyEvents core services, the EF Core outbox writer, and the EF Core outbox store.

It also applies generated TinyEvents contributions for the assemblies loaded in the process. Those contributions contain the consumer registrations and event type descriptors emitted by the source generator.
It also applies generated TinyEvents contributions for assemblies already loaded in the process. Those contributions contain the consumer and event dispatcher registrations emitted by the source generator.

For PostgreSQL EF Core, use the PostgreSQL provider package and registration method:

Expand Down Expand Up @@ -137,7 +137,7 @@ var processor = provider.GetRequiredService<ITinyOutboxProcessor>();
await processor.ProcessPendingAsync(ct);
```

Processing resolves generated `TinyEventTypeDescriptor` services to deserialize the payload, then resolves `IEnumerable<IEventConsumer<TEvent>>` from dependency injection.
Processing resolves a generated `ITinyEventDispatcher` for the stored event type, deserializes the payload, and invokes matching `IEventConsumer<TEvent>` services through dependency injection.

For hosted processing:

Expand Down
2 changes: 2 additions & 0 deletions docs/postgresql/ado-net.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,5 @@ The package also includes the default PostgreSQL script:
```text
schema/postgresql/001_CreateTinyOutbox.sql
```

The default PostgreSQL schema uses `text` for `EventType`, `Payload`, `ClaimedBy`, and `LastError`.
6 changes: 6 additions & 0 deletions docs/postgresql/ef-core.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,18 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)

The provider option controls SQL claiming and marking. The model builder extension controls EF mapping and migrations.

The PostgreSQL mapping uses `text` for `EventType`, `Payload`, `ClaimedBy`, and `LastError`.

## Worker Claiming

The PostgreSQL EF Core store opens the underlying relational connection when needed and executes PostgreSQL claim/mark statements.

Claiming is atomic and lease-based. PostgreSQL uses `FOR UPDATE SKIP LOCKED` inside an update/returning statement.

If the scoped `DbContext` has a current EF Core transaction, TinyEvents attaches claim and mark commands to that transaction. TinyEvents does not start, commit, or roll back that transaction.

The hosted worker creates a fresh scope for each processing iteration, so claim and mark commands normally run outside an application-owned transaction.

## Migrations

Use normal EF Core migrations:
Expand Down
41 changes: 4 additions & 37 deletions docs/releasing.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Releasing

TinyEvents has two release paths.
TinyEvents uses one release train.

Use the full release workflow when every package should move together. Use the package release workflow when one package needs its own version bump.
All published TinyEvents packages move together with the same version. The shared development version lives in `Directory.Build.props`. Release workflows override that version from the git tag when packages are packed.

## Full Release Train

Expand All @@ -23,7 +23,7 @@ This workflow:
- verifies every expected package exists
- publishes every package to NuGet

Use this when the suite should share one version:
The release train publishes:

- `TinyEvents`
- `TinyEvents.Worker`
Expand All @@ -32,40 +32,7 @@ Use this when the suite should share one version:
- `TinyEvents.PostgreSql.AdoNet`
- `TinyEvents.PostgreSql.EntityFrameworkCore`

## Single Package Release

The `Release Package` workflow is manual.

Use it when one package needs a dedicated version without forcing every package to publish the same version.

Inputs:

- `package`: the package id to publish
- `version`: the exact NuGet version to publish

Example:

```text
package: TinyEvents.PostgreSql.AdoNet
version: 0.1.0-alpha.3
```

This workflow still restores, builds, and tests the whole solution before publishing. It only packs and pushes the selected package.

Use this for:

- provider-specific alpha fixes
- package metadata fixes
- documentation or schema-content fixes that affect one package
- small package-specific corrections that do not require a release train

## Before A Single Package Release

Check the selected package dependency story before publishing.

Provider packages reference `TinyEvents` in the repository through project references. During packing, NuGet dependencies are generated from those project references. Make sure the dependency version is the one you intend consumers to use.

If the provider package requires a newer core package, publish the core package first or use the full release train.
Do not publish a provider package independently during the alpha hardening phase. Provider packages reference `TinyEvents`, and keeping one version across the train avoids accidental dependency mismatches.

## Local Checks

Expand Down
5 changes: 5 additions & 0 deletions docs/schema-and-migrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ The v1 statuses are:
- `Processed`
- `Failed`

Provider schemas keep the same logical columns and indexes, but database types follow each provider:

- SQL Server maps `EventType` to `NVARCHAR(512)`, `Payload` to `NVARCHAR(MAX)`, `ClaimedBy` to `NVARCHAR(256)`, and `LastError` to `NVARCHAR(MAX)`.
- PostgreSQL maps `EventType`, `Payload`, `ClaimedBy`, and `LastError` to `text`.

## EF Core

EF Core applications create the schema through normal EF Core migrations.
Expand Down
21 changes: 12 additions & 9 deletions docs/source-generator.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,16 @@ public sealed class SendWelcomeEmail : IEventConsumer<UserCreated>
For each discovered consumer, the generator emits:

- DI registration for `IEventConsumer<TEvent>`
- event type descriptor registration for deserialization
- event dispatcher registration for deserialization and consumer invocation
- a generated contribution
- a module initializer that adds the contribution to TinyEvents bootstrap

The generated registration is equivalent to:

```csharp
services.AddScoped<IEventConsumer<UserCreated>, SendWelcomeEmail>();
services.AddSingleton(new TinyEventTypeDescriptor(
"MyApp.UserCreated",
typeof(UserCreated)));
services.AddSingleton<ITinyEventDispatcher>(
new TinyEventDispatcher<UserCreated>("MyApp.UserCreated"));
```

The generated code is packaged as an `ITinyEventsContribution`. A module initializer calls:
Expand All @@ -50,17 +49,21 @@ TinyEvents does not scan assemblies at runtime to find consumers.

Runtime processing uses:

1. Generated `TinyEventTypeDescriptor` services to resolve the stored event type name.
1. Generated `ITinyEventDispatcher` services to resolve the stored event type name.
2. `ITinyEventSerializer` to deserialize the payload.
3. Dependency injection to resolve `IEnumerable<IEventConsumer<TEvent>>`.
3. Dependency injection to resolve and invoke `IEventConsumer<TEvent>` services.

Dependency injection is the consumer registry. `TinyEventTypeDescriptor` is only the event-name-to-CLR-type map needed for deserialization.
Dependency injection is the consumer registry. `ITinyEventDispatcher` is the event-name-to-typed-dispatch map needed for deserialization and consumer invocation.

## Contribution Bootstrap

Generated contributions make multi-assembly projects work without runtime scanning.

Each assembly that contains consumers can contribute registrations. When the host calls a TinyEvents registration method, bootstrap applies the collected contributions once per service collection.
Each assembly that contains consumers generates its own contribution. When that assembly is loaded, its module initializer adds the contribution to TinyEvents bootstrap. When the host calls a TinyEvents registration method, bootstrap applies the collected contributions once per service collection.

TinyEvents does not scan application assemblies or force-load referenced assemblies. If a consumer assembly has not been loaded before TinyEvents registration runs, that assembly's consumers and event dispatchers will not be registered in that service collection.

An outbox message whose stored event type has no registered dispatcher is treated as a processing failure by the worker. It follows the normal retry and max-attempt rules. Load consumer assemblies before TinyEvents registration so their generated contributions are available.

For example:

Expand Down Expand Up @@ -88,7 +91,7 @@ Analysis reads Roslyn syntax and symbols.

Model stores TinyEvents-owned facts.

Planning creates consumer registration and event descriptor plans.
Planning creates consumer registration and event dispatcher plans.

Emission writes generated C#.

Expand Down
Loading
Loading