Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
41 changes: 41 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Repository Guidelines

## Project Structure & Module Organization

This is a .NET 10 solution centered on `Eventa.slnx`. Core library code lives in `src/Eventa`, and transport/adapter code lives in `src/Eventa.Adapters` with channel adapter types under `Channels`. Tests mirror that split in `tests/Eventa.Tests` and `tests/Eventa.Adapters.Tests`. `examples/Eventa.Example` is the console sample used to validate public API ergonomics. Design notes and RFCs belong in `docs`; generated build output belongs in `artifacts` and should not be edited by hand.

## Build, Test, and Development Commands

Use the .NET 10 SDK. Common commands:

```text
dotnet restore Eventa.slnx --locked-mode
dotnet build Eventa.slnx --configuration Release --no-restore
dotnet test --project tests/Eventa.Tests/Eventa.Tests.csproj --configuration Release --no-build
dotnet test --project tests/Eventa.Adapters.Tests/Eventa.Adapters.Tests.csproj --configuration Release --no-build
dotnet run --project examples/Eventa.Example/Eventa.Example.csproj --configuration Release --no-restore
```

Restore first, build the full solution, then run the two xUnit test projects as separate `dotnet test --project ...` commands. This repository uses Microsoft.Testing.Platform, so agents should not rely on repo-wide `dotnet test` discovery when narrowing scope. Run the example after API or behavior changes that affect user-facing flows.

## Coding Style & Naming Conventions

Follow `.editorconfig`: spaces only, 4-space indentation for C#, 2-space indentation for project/XML/JSON files. C# uses nullable reference types and implicit usings. Prefer explicit types over `var`, file-scoped namespaces, braces for blocks, sorted `System` usings first, and static local functions where practical. Public types, methods, and properties use PascalCase; interfaces use `I` + PascalCase; type parameters use `T` + PascalCase.

## Testing Guidelines

Tests use xUnit v3 with Microsoft.Testing.Platform, configured by `global.json`. Always pass `--project` when invoking tests from the CLI in this repo. For targeted validation, use Microsoft.Testing.Platform/xUnit v3 switches such as `--filter-class Eventa.Tests.AsyncSignalQueueTests`, and prefer fully qualified class names even when the simple class name appears unique.

```text
dotnet test --project tests/Eventa.Tests/Eventa.Tests.csproj --configuration Release --no-build --filter-class Eventa.Tests.AsyncSignalQueueTests
```

Do not use VSTest-style `--filter "..."` expressions here; they are the wrong syntax for this setup and will commonly return zero tests. Filtering by class is the most reliable narrow validation path in this repo. Keep tests close to the behavior they cover and name methods in the existing `MethodOrScenario_Condition_ExpectedResult` style, for example `InvokeAsync_WithPreCanceledToken_EmitsAbortOnlyOnce`. Add regression coverage for cancellation, streaming, adapter faulting, and concurrency changes.

## Commit & Pull Request Guidelines

History uses Conventional Commit prefixes such as `feat:`, `fix:`, `docs:`, `chore:`, and scoped forms like `feat(example):`. Keep commits focused and imperative. Pull requests should describe the behavior change, list validation commands run, link related issues or RFCs, and call out public API or protocol compatibility impact.

## Agent-Specific Notes

Keep plans concise and list unresolved questions at the end. When sharing runnable PowerShell commands, prefer fenced `text` blocks rather than inline command formatting. For targeted validation, prefer explicit `dotnet test --project ...` commands and `--filter-class <FullyQualifiedClassName>` over generic single-test tooling or VSTest filter expressions.
2 changes: 2 additions & 0 deletions Eventa.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
<Project Path="examples/Eventa.Example/Eventa.Example.csproj" />
</Folder>
<Folder Name="/src/">
<Project Path="src/Eventa.Adapters/Eventa.Adapters.csproj" />
<Project Path="src/Eventa/Eventa.csproj" />
</Folder>
<Folder Name="/tests/">
<Project Path="tests/Eventa.Adapters.Tests/Eventa.Adapters.Tests.csproj" />
<Project Path="tests/Eventa.Tests/Eventa.Tests.csproj" />
</Folder>
</Solution>
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ The C# implementation currently focuses on the core protocol layer:
- request-stream to unary-response invokes
- server-streaming and bidirectional streaming invokes
- adapter observation hooks for send/receive activity
- in-process channel adapter for connecting two contexts
- fatal event and fatal match-expression hooks that abort pending invokes

## Features
Expand All @@ -43,6 +44,8 @@ The C# implementation currently focuses on the core protocol layer:
- Cancellation through `CancellationToken`.
- AOT-oriented API shape with explicit generic payload types.
- Minimal adapter surface through `IEventaAdapter`.
- Constructor-first `Eventa.Adapters.Channels` pair transport for same-process
object transport.

## Requirements

Expand All @@ -55,6 +58,7 @@ Eventa is available on NuGet:

```sh
dotnet add package Eventa --prerelease
dotnet add package Eventa.Adapters --prerelease
```

Package page: <https://www.nuget.org/packages/Eventa/>
Expand All @@ -68,6 +72,7 @@ To run the repository examples locally:
dotnet restore Eventa.slnx --locked-mode
dotnet build Eventa.slnx --configuration Release --no-restore
dotnet test --project tests/Eventa.Tests/Eventa.Tests.csproj --configuration Release --no-build
dotnet test --project tests/Eventa.Adapters.Tests/Eventa.Adapters.Tests.csproj --configuration Release --no-build
dotnet run --project examples/Eventa.Example/Eventa.Example.csproj --configuration Release --no-restore
```

Expand Down Expand Up @@ -171,13 +176,47 @@ Streaming invokes return `IAsyncEnumerable<TResponse>`. Handlers can be written
as async iterators or adapted from callback-style code with
`EventStream.ToStreamHandler`.

## Channel Adapter Example

```csharp
using Eventa;
using Eventa.Adapters.Channels;

using var pipe = new ChannelPipe();

var echo = new InvokeEventDefinition<EchoResponse, EchoRequest>("demo:channel:echo");

using var handler = pipe.Right.RegisterInvokeHandler(
echo,
static (EchoRequest request, CancellationToken _) =>
Task.FromResult(new EchoResponse(request.Input.ToUpperInvariant())));

var client = pipe.Left.CreateInvokeClient(echo);
var response = await client.InvokeAsync(new EchoRequest("eventa"));

Console.WriteLine(response.Output); // EVENTA

public sealed record EchoRequest(string Input);

public sealed record EchoResponse(string Output);
```

`ChannelPipe.Left` and `ChannelPipe.Right` are symmetric endpoints and each is
usable as an `IEventContext`. The v1 channel adapter is in-process and
object-only; it forwards existing typed envelopes through
`System.Threading.Channels` and does not serialize payloads. Default
`ChannelPipe` channels are unbounded and intended for local composition, tests,
and same-process boundaries, not as a throughput or backpressure policy.

## Project Layout

```text
.
+-- Eventa.slnx
+-- src/Eventa/ # Core library
+-- src/Eventa.Adapters/ # Channel adapter library
+-- tests/Eventa.Tests/ # xUnit v3 tests on Microsoft.Testing.Platform
+-- tests/Eventa.Adapters.Tests/ # Adapter integration tests
+-- examples/Eventa.Example/ # Console examples
+-- docs/ # Design and compatibility notes
```
Expand All @@ -190,6 +229,7 @@ Useful commands:
dotnet restore Eventa.slnx --locked-mode
dotnet build Eventa.slnx --configuration Release --no-restore
dotnet test --project tests/Eventa.Tests/Eventa.Tests.csproj --configuration Release --no-build
dotnet test --project tests/Eventa.Adapters.Tests/Eventa.Adapters.Tests.csproj --configuration Release --no-build
dotnet run --project examples/Eventa.Example/Eventa.Example.csproj --configuration Release --no-restore
```

Expand Down
6 changes: 3 additions & 3 deletions examples/Eventa.Example/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -514,7 +514,7 @@ private sealed record FatalPayload(string Reason);

private sealed record AdapterSentCall(string EventId, object? Envelope, object? Options);

private sealed record AdapterReceivedCall(string EventId, object? Envelope);
private sealed record AdapterReceivedCall(string EventId, object? Envelope, object? Options);

private sealed class RecordingAdapter : IEventaAdapter
{
Expand All @@ -527,9 +527,9 @@ public void OnSent(string eventId, object? envelope, object? options = null)
SentCalls.Add(new AdapterSentCall(eventId, envelope, options));
}

public void OnReceived(string eventId, object? envelope)
public void OnReceived(string eventId, object? envelope, object? options = null)
{
ReceivedCalls.Add(new AdapterReceivedCall(eventId, envelope));
ReceivedCalls.Add(new AdapterReceivedCall(eventId, envelope, options));
}

public void Dispose() { }
Expand Down
42 changes: 42 additions & 0 deletions src/Eventa.Adapters/Channels/ChannelAdapter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using System.Threading.Channels;

using Eventa;

namespace Eventa.Adapters.Channels;

/// <summary>
/// Mirrors locally sent Eventa envelopes to an outbound channel writer.
/// </summary>
internal sealed class ChannelAdapter(ChannelWriter<ChannelMessage> outbound) : IEventaAdapter
{
private int _disposed;

/// <inheritdoc />
public void OnSent(string eventId, object? envelope, object? options = null)
{
if (Volatile.Read(ref _disposed) != 0)
{
throw new ChannelClosedException("Channel endpoint closed.");
}

if (envelope is not IEventEnvelope eventEnvelope)
{
throw new InvalidOperationException(
$"Event '{eventId}' was sent with an envelope that does not implement {nameof(IEventEnvelope)}.");
}

if (!outbound.TryWrite(new ChannelMessage(eventEnvelope, options)))
{
throw new ChannelClosedException("Channel endpoint closed.");
}
Comment thread
Garfield550 marked this conversation as resolved.
}

/// <inheritdoc />
public void OnReceived(string eventId, object? envelope, object? options = null) { }

/// <inheritdoc />
public void Dispose()
{
Interlocked.Exchange(ref _disposed, 1);
}
}
Loading