Skip to content

Commit 0093388

Browse files
authored
feat: implement channel adapter (#19)
* feat: implement channel adapter * chore: code review change and add agent file * fix: build error * chore: code review change * chore: code review change * chore: code style update * chore: code style update
1 parent a52a12e commit 0093388

26 files changed

Lines changed: 1981 additions & 184 deletions

AGENTS.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Repository Guidelines
2+
3+
## Project Structure & Module Organization
4+
5+
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.
6+
7+
## Build, Test, and Development Commands
8+
9+
Use the .NET 10 SDK. Common commands:
10+
11+
```text
12+
dotnet restore Eventa.slnx --locked-mode
13+
dotnet build Eventa.slnx --configuration Release --no-restore
14+
dotnet test --project tests/Eventa.Tests/Eventa.Tests.csproj --configuration Release --no-build
15+
dotnet test --project tests/Eventa.Adapters.Tests/Eventa.Adapters.Tests.csproj --configuration Release --no-build
16+
dotnet run --project examples/Eventa.Example/Eventa.Example.csproj --configuration Release --no-restore
17+
```
18+
19+
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.
20+
21+
## Coding Style & Naming Conventions
22+
23+
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.
24+
25+
## Testing Guidelines
26+
27+
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.
28+
29+
```text
30+
dotnet test --project tests/Eventa.Tests/Eventa.Tests.csproj --configuration Release --no-build --filter-class Eventa.Tests.AsyncSignalQueueTests
31+
```
32+
33+
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.
34+
35+
## Commit & Pull Request Guidelines
36+
37+
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.
38+
39+
## Agent-Specific Notes
40+
41+
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.

Eventa.slnx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
<Project Path="examples/Eventa.Example/Eventa.Example.csproj" />
44
</Folder>
55
<Folder Name="/src/">
6+
<Project Path="src/Eventa.Adapters/Eventa.Adapters.csproj" />
67
<Project Path="src/Eventa/Eventa.csproj" />
78
</Folder>
89
<Folder Name="/tests/">
10+
<Project Path="tests/Eventa.Adapters.Tests/Eventa.Adapters.Tests.csproj" />
911
<Project Path="tests/Eventa.Tests/Eventa.Tests.csproj" />
1012
</Folder>
1113
</Solution>

README.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ The C# implementation currently focuses on the core protocol layer:
2929
- request-stream to unary-response invokes
3030
- server-streaming and bidirectional streaming invokes
3131
- adapter observation hooks for send/receive activity
32+
- in-process channel adapter for connecting two contexts
3233
- fatal event and fatal match-expression hooks that abort pending invokes
3334

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

4750
## Requirements
4851

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

5659
```sh
5760
dotnet add package Eventa --prerelease
61+
dotnet add package Eventa.Adapters --prerelease
5862
```
5963

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

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

179+
## Channel Adapter Example
180+
181+
```csharp
182+
using Eventa;
183+
using Eventa.Adapters.Channels;
184+
185+
using var pipe = new ChannelPipe();
186+
187+
var echo = new InvokeEventDefinition<EchoResponse, EchoRequest>("demo:channel:echo");
188+
189+
using var handler = pipe.Right.RegisterInvokeHandler(
190+
echo,
191+
static (EchoRequest request, CancellationToken _) =>
192+
Task.FromResult(new EchoResponse(request.Input.ToUpperInvariant())));
193+
194+
var client = pipe.Left.CreateInvokeClient(echo);
195+
var response = await client.InvokeAsync(new EchoRequest("eventa"));
196+
197+
Console.WriteLine(response.Output); // EVENTA
198+
199+
public sealed record EchoRequest(string Input);
200+
201+
public sealed record EchoResponse(string Output);
202+
```
203+
204+
`ChannelPipe.Left` and `ChannelPipe.Right` are symmetric endpoints and each is
205+
usable as an `IEventContext`. The v1 channel adapter is in-process and
206+
object-only; it forwards existing typed envelopes through
207+
`System.Threading.Channels` and does not serialize payloads. Default
208+
`ChannelPipe` channels are unbounded and intended for local composition, tests,
209+
and same-process boundaries, not as a throughput or backpressure policy.
210+
174211
## Project Layout
175212

176213
```text
177214
.
178215
+-- Eventa.slnx
179216
+-- src/Eventa/ # Core library
217+
+-- src/Eventa.Adapters/ # Channel adapter library
180218
+-- tests/Eventa.Tests/ # xUnit v3 tests on Microsoft.Testing.Platform
219+
+-- tests/Eventa.Adapters.Tests/ # Adapter integration tests
181220
+-- examples/Eventa.Example/ # Console examples
182221
+-- docs/ # Design and compatibility notes
183222
```
@@ -190,6 +229,7 @@ Useful commands:
190229
dotnet restore Eventa.slnx --locked-mode
191230
dotnet build Eventa.slnx --configuration Release --no-restore
192231
dotnet test --project tests/Eventa.Tests/Eventa.Tests.csproj --configuration Release --no-build
232+
dotnet test --project tests/Eventa.Adapters.Tests/Eventa.Adapters.Tests.csproj --configuration Release --no-build
193233
dotnet run --project examples/Eventa.Example/Eventa.Example.csproj --configuration Release --no-restore
194234
```
195235

examples/Eventa.Example/Program.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -514,7 +514,7 @@ private sealed record FatalPayload(string Reason);
514514

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

517-
private sealed record AdapterReceivedCall(string EventId, object? Envelope);
517+
private sealed record AdapterReceivedCall(string EventId, object? Envelope, object? Options);
518518

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

530-
public void OnReceived(string eventId, object? envelope)
530+
public void OnReceived(string eventId, object? envelope, object? options = null)
531531
{
532-
ReceivedCalls.Add(new AdapterReceivedCall(eventId, envelope));
532+
ReceivedCalls.Add(new AdapterReceivedCall(eventId, envelope, options));
533533
}
534534

535535
public void Dispose() { }
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
using System.Threading.Channels;
2+
3+
using Eventa;
4+
5+
namespace Eventa.Adapters.Channels;
6+
7+
/// <summary>
8+
/// Mirrors locally sent Eventa envelopes to an outbound channel writer.
9+
/// </summary>
10+
internal sealed class ChannelAdapter(ChannelWriter<ChannelMessage> outbound) : IEventaAdapter
11+
{
12+
private int _disposed;
13+
14+
/// <inheritdoc />
15+
public void OnSent(string eventId, object? envelope, object? options = null)
16+
{
17+
if (Volatile.Read(ref _disposed) != 0)
18+
{
19+
throw new ChannelClosedException("Channel endpoint closed.");
20+
}
21+
22+
if (envelope is not IEventEnvelope eventEnvelope)
23+
{
24+
throw new InvalidOperationException(
25+
$"Event '{eventId}' was sent with an envelope that does not implement {nameof(IEventEnvelope)}.");
26+
}
27+
28+
if (!outbound.TryWrite(new ChannelMessage(eventEnvelope, options)))
29+
{
30+
throw new ChannelClosedException("Channel endpoint closed.");
31+
}
32+
}
33+
34+
/// <inheritdoc />
35+
public void OnReceived(string eventId, object? envelope, object? options = null) { }
36+
37+
/// <inheritdoc />
38+
public void Dispose()
39+
{
40+
Interlocked.Exchange(ref _disposed, 1);
41+
}
42+
}

0 commit comments

Comments
 (0)