diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..3f49f5c --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,67 @@ +# Copilot instructions — EventSourcing (JKang.EventSourcing) + +Purpose: help an AI coding agent quickly be productive in this repo — what matters, where to look, and how to run/verify work. + +## Quick summary +- This is a .NET Core event-sourcing framework (see `README.md`). +- Core projects: `JKang.EventSourcing.*` (abstractions, options, snapshotting, persistence implementations). +- Persistence providers are separate projects under `src/` (e.g., `Persistence.FileSystem`, `Persistence.DynamoDB`, `Persistence.CosmosDB`, `Persistence.EfCore`, `Persistence.S3`). +- `JKang.EventSourcing.TestingWebApp` is an interactive sample app to exercise different persistence backends. + +## Useful commands (local dev) +- Restore/build all: `dotnet restore` then `dotnet build src\EventSourcing.sln` +- Run tests: `dotnet test src` (or `dotnet test` inside a specific `*.Tests` project) +- Run the sample web app: `dotnet run --project src\JKang.EventSourcing.TestingWebApp\JKang.EventSourcing.TestingWebApp.csproj` + - Toggle storage backend inside `Startup.cs` (see `PersistenceMode` and `ConfigureServicesFor*` methods) or set `appsettings.json` values for AWS/Cosmos. +- CI: Azure Pipelines YAML lives under `build/` (e.g., `azure-pipeline.yml`, `pack-publish.yml`). + +## Architecture & important files to read first +- High-level: core abstractions and patterns are in `JKang.EventSourcing.Abstractions` and `JKang.EventSourcing`. +- Examples & canonical patterns: `src\JKang.EventSourcing.TestingFixtures\` (e.g., `GiftCard.cs`, `GiftCardSnapshot.cs`, `GiftCardRepository.cs`) and `samples/`. +- Persistence pattern: each store provides + - Event store (`IEventStore` implementation) — e.g., `TextFileEventStore`, `DynamoDBEventStore`, `EfCoreEventStore`. + - Snapshot store (`ISnapshotStore`) when supported. + - Builder extension to wire into DI: `*PersistenceEventSourcingBuilderExtensions.cs` (e.g., `S3SnapshotPersistenceEventSourcingBuilderExtensions.cs`). + - `Defaults.cs` with Json.NET settings used consistently across providers. +- Initializers: `IEventStoreInitializer<>` / `ISnapshotStoreInitializer<>` — used by `TestingWebApp` to create tables/containers before use. +- Serializers: Cosmos uses `EventSourcingCosmosSerializer` and persistence uses `Defaults.JsonSerializerSettings`. Ensure any new event types are JSON-serializable with these settings. + +## Project-specific conventions & patterns +- Aggregate pattern: aggregates implement `IAggregate` (often via `Aggregate`) and mutate state only by applying `IAggregateEvent` in `ApplyEvent`. + - Required constructors: `(TKey id, IEnumerable> savedEvents)` and `(TKey id, IAggregateSnapshot snapshot, IEnumerable> savedEvents)` for rehydration. + - Snapshot support via `TakeSnapshot()` and `IAggregateSnapshot`. +- Events should be immutable and JSON-serializable. Many stores call `JsonConvert.SerializeObject(@event, Defaults.JsonSerializerSettings)`. +- Naming: options & configuration types end with `Options` (e.g., `S3SnapshotStoreOptions`), and extension classes follow the `*PersistenceEventSourcingBuilderExtensions` pattern. +- Async-first: APIs and store methods use `async`/`Task` and take optional `CancellationToken`. + +## How to add a new persistence provider (pattern to follow) +1. Create `JKang.EventSourcing.Persistence.YourProvider` project under `src/`. +2. Add `Defaults.cs` for serializer settings (reuse pattern from existing `Defaults.cs`). +3. Implement `IEventStore` and (optionally) `ISnapshotStore`. +4. Provide builder extension `YourProviderPersistenceEventSourcingBuilderExtensions.cs` that: + - Adds configuration via `.ConfigureAggregate(...)` and registers the services in DI. +5. Add initializer implementing `IEventStoreInitializer<>` / `ISnapshotStoreInitializer<>` if the backend needs resource creation (tables, containers, buckets). +6. Create tests mirroring existing provider tests (see `*.Tests` projects) and a sample configuration in `TestingWebApp`. + +## Testing & local integration tips +- Unit tests use xUnit (see `*.Tests` projects under `src/`). Use `dotnet test`. +- DynamoDB local: `TestingWebApp` uses `ServiceURL = "http://localhost:8800"` under DEBUG — useful with a local DynamoDB container (e.g., LocalStack or dynamodb-local). +- Cosmos: `ConfigureServicesForCosmosDB` expects a connection string under `ConnectionStrings:CosmosDB` (see `Startup.cs`). +- S3 and DynamoDB integration tests assume AWS credentials or local endpoints configured in environment or `appsettings.json`. + +## What to check when changing behavior +- Verify serialization round-trip: events and snapshots must deserialize correctly with `Defaults.JsonSerializerSettings` (or `EventSourcingCosmosSerializer` for Cosmos). +- Make sure aggregate invariants are enforced in `ApplyEvent` or when emitting events (tests often assert that invalid ops throw `InvalidOperationException`). +- Update `TestingWebApp` to allow manual verification of a new storage configuration. + +## Where to look for examples / canonical code +- Aggregate + events: `src\JKang.EventSourcing.TestingFixtures\GiftCard.cs`, `GiftCardCreated.cs`, `GiftCardDebited.cs`. +- Text file provider: `src\JKang.EventSourcing.Persistence.FileSystem\` (simple, readable reference implementation). +- EF Core provider + snapshots: `src\JKang.EventSourcing.Persistence.EfCore\`. +- DynamoDB provider: `src\JKang.EventSourcing.Persistence.DynamoDB\` (good example for non-relational storage patterns). +- Testing web app: `src\JKang.EventSourcing.TestingWebApp\Startup.cs` (how to wire different providers at runtime). + +--- +If you want, I can merge this into any existing `.github/copilot-instructions.md` (if present) or iterate with more details (e.g., example PR checklist, coding style rules, or common bug patterns). Please tell me which additions you'd like. + +*Generated: concise, practical, and based on discoverable patterns in this repo.* diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..ac68266 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,85 @@ +name: CI Build & Pack + +on: + push: + branches: + - develop + workflow_dispatch: {} + +jobs: + build-and-pack: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Cache NuGet packages + uses: actions/cache@v4 + with: + path: ~/.nuget/packages + key: nuget-packages-${{ runner.os }}-$(sha1sum version.yml | cut -d' ' -f1) + restore-keys: | + nuget-packages-${{ runner.os }}- + + - name: Setup .NET SDK + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0.x' + + - name: Determine package version + id: pkgver + run: | + VERSION_LINE=$(grep '^ version:' version.yml || true) + VERSION=$(echo $VERSION_LINE | awk '{print $2}') + if [ -z "$VERSION" ]; then + echo "version not found in version.yml, defaulting to 0.0.0" + VERSION=0.0.0 + fi + # Append run number so CI packages are unique + PACKAGE_VERSION="${VERSION}-ci.${GITHUB_RUN_NUMBER}" + echo "PACKAGE_VERSION=$PACKAGE_VERSION" >> $GITHUB_ENV + echo "Determined PACKAGE_VERSION=$PACKAGE_VERSION" + + - name: Restore + run: dotnet restore + + - name: Build + run: dotnet build --configuration Release --no-restore + + - name: Run tests + run: dotnet test --configuration Release --no-build --verbosity normal || true + + - name: Pack NuGet packages + run: | + mkdir -p artifacts + dotnet pack "src/**/*.csproj" -c Release -o artifacts /p:PackageVersion=${{ env.PACKAGE_VERSION }} --no-build + + - name: List artifacts + run: ls -la artifacts || true + + - name: Upload NuGet packages + uses: actions/upload-artifact@v4 + with: + name: nuget-packages + path: artifacts/*.nupkg + + - name: Publish to NuGet.org + if: ${{ github.ref == 'refs/heads/develop' }} + env: + NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} + run: | + set -euo pipefail + echo "Publishing packages to NuGet.org" + shopt -s nullglob || true + files=(artifacts/*.nupkg) + if [ ${#files[@]} -eq 0 ]; then + echo "No packages found, skipping publish" + exit 0 + fi + for pkg in "${files[@]}"; do + echo "Pushing $pkg" + dotnet nuget push "$pkg" -s https://api.nuget.org/v3/index.json -k "$NUGET_API_KEY" --skip-duplicate + done diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml new file mode 100644 index 0000000..683ce4f --- /dev/null +++ b/.github/workflows/pr-validation.yml @@ -0,0 +1,66 @@ +name: PR Validation (Minimal) + +on: + pull_request: + branches: + - '**' + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Require version.yml change + if: ${{ github.event_name == 'pull_request' }} + run: | + set -euo pipefail + echo "Checking that version.yml was modified in this PR..." + # Fetch base branch to compare + git fetch origin ${{ github.event.pull_request.base.ref }} --depth=1 + changed_files=$(git diff --name-only origin/${{ github.event.pull_request.base.ref }}...HEAD || true) + echo "$changed_files" + if ! echo "$changed_files" | grep -xq 'version.yml'; then + echo "::error::Pull request must include an updated version.yml" + exit 1 + fi + + - name: Setup .NET + uses: actions/setup-dotnet@v3 + with: + dotnet-version: '10.0.x' + + - name: Cache NuGet packages + uses: actions/cache@v4 + with: + path: ~/.nuget/packages + key: nuget-${{ runner.os }}-${{ hashFiles('**/*.csproj') }} + restore-keys: | + nuget-${{ runner.os }}- + + - name: Restore + run: dotnet restore src/EventSourcing.sln + + - name: Build + run: dotnet build src/EventSourcing.sln --no-restore + + - name: Test - all + run: | + set -euo pipefail + mkdir -p TestResults + for proj in src/*Tests/*.csproj; do + echo "Running tests for $proj" + name=$(basename "$proj" .csproj) + dotnet test "$proj" --no-build --logger "trx;LogFileName=${name}.trx" --results-directory TestResults + done + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results + path: TestResults + diff --git a/build/azure-pipeline-ci.yml b/build/azure-pipeline-ci.yml deleted file mode 100644 index 353d3c8..0000000 --- a/build/azure-pipeline-ci.yml +++ /dev/null @@ -1,24 +0,0 @@ -variables: -- template: version.yml - -name: $(version)-ci-$(Date:yyyyMMdd)$(Rev:.r) - -trigger: - branches: - include: - - develop - -pr: none - -pool: - vmImage: 'ubuntu-16.04' - -steps: -- template: templates/build-test.yml - parameters: - buildConfiguration: Release - -- template: templates/pack-publish.yml - parameters: - buildConfiguration: Release - versionEnvVar: Build.BuildNumber diff --git a/build/azure-pipeline-pr.yml b/build/azure-pipeline-pr.yml deleted file mode 100644 index e4f5323..0000000 --- a/build/azure-pipeline-pr.yml +++ /dev/null @@ -1,24 +0,0 @@ -variables: -- template: version.yml - - -name: $(version)-pr-$(Date:yyyyMMdd)$(Rev:.r) - - -trigger: none - - -pr: - branches: - include: - - '*' - - -pool: - vmImage: 'ubuntu-16.04' - - -steps: -- template: templates/build-test.yml - parameters: - buildConfiguration: Debug diff --git a/build/azure-pipeline.yml b/build/azure-pipeline.yml deleted file mode 100644 index aff0bd0..0000000 --- a/build/azure-pipeline.yml +++ /dev/null @@ -1,54 +0,0 @@ -parameters: -- name: quality - displayName: Build quality - type: string - default: preview - values: - - preview - - stable - - -variables: - - template: version.yml - - -name: $(version)-${{ parameters.quality }}$(Rev:.r) - - -trigger: - branches: - include: - - master - - -pr: none - - -pool: - vmImage: 'ubuntu-16.04' - - -steps: -- bash: | - echo "##vso[task.setvariable variable=version]$(Build.BuildNumber)" - condition: ${{ not(eq(parameters.quality, 'stable')) }} - displayName: 'Override package version' - -- checkout: self - persistCredentials: true - -- template: templates/build-test.yml - parameters: - buildConfiguration: Release - -- template: templates/pack-publish.yml - parameters: - buildConfiguration: Release - versionEnvVar: 'version' - -- script: | - git config --global user.name "Azure DevOps" - git config --global user.email "fake@dev.azure.com" - git tag -a "v$(version)" -m "v$(version)" - git push origin "v$(version)" - displayName: Tag source \ No newline at end of file diff --git a/build/templates/build-test.yml b/build/templates/build-test.yml deleted file mode 100644 index cc00a03..0000000 --- a/build/templates/build-test.yml +++ /dev/null @@ -1,19 +0,0 @@ -parameters: -- name: buildConfiguration - type: string - - -steps: -- task: DotNetCoreCLI@2 - displayName: Build - inputs: - command: 'build' - projects: 'src/*.sln' - arguments: '--configuration ${{parameters.buildConfiguration}}' - -- task: DotNetCoreCLI@2 - displayName: Test - inputs: - command: 'test' - projects: 'src/*Tests/*.csproj' - arguments: '--configuration ${{parameters.buildConfiguration}}' diff --git a/build/templates/pack-publish.yml b/build/templates/pack-publish.yml deleted file mode 100644 index 0c27358..0000000 --- a/build/templates/pack-publish.yml +++ /dev/null @@ -1,26 +0,0 @@ -parameters: -- name: buildConfiguration - type: string - -- name: versionEnvVar - type: string - - -steps: -- task: DotNetCoreCLI@2 - displayName: Pack - inputs: - command: 'pack' - packagesToPack: 'src/**/*.csproj' - configuration: ${{parameters.buildConfiguration}} - nobuild: true - versioningScheme: 'byEnvVar' - versionEnvVar: ${{parameters.versionEnvVar}} - -- task: PublishBuildArtifacts@1 - inputs: - PathtoPublish: '$(Build.ArtifactStagingDirectory)' - ArtifactName: 'drop' - publishLocation: 'Container' - displayName: Publish artifacts - diff --git a/build/version.yml b/build/version.yml deleted file mode 100644 index f9ab330..0000000 --- a/build/version.yml +++ /dev/null @@ -1,2 +0,0 @@ -variables: - version: 1.1.2 \ No newline at end of file diff --git a/src/JKang.EventSourcing.Abstractions.Tests/JKang.EventSourcing.Abstractions.Tests.csproj b/src/JKang.EventSourcing.Abstractions.Tests/JKang.EventSourcing.Abstractions.Tests.csproj index 39da758..737bc53 100644 --- a/src/JKang.EventSourcing.Abstractions.Tests/JKang.EventSourcing.Abstractions.Tests.csproj +++ b/src/JKang.EventSourcing.Abstractions.Tests/JKang.EventSourcing.Abstractions.Tests.csproj @@ -1,21 +1,21 @@ - netcoreapp3.1 + net10.0 false - - - - - + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/JKang.EventSourcing.Abstractions/JKang.EventSourcing.Abstractions.csproj b/src/JKang.EventSourcing.Abstractions/JKang.EventSourcing.Abstractions.csproj index 7c1ac3d..e3cfe44 100644 --- a/src/JKang.EventSourcing.Abstractions/JKang.EventSourcing.Abstractions.csproj +++ b/src/JKang.EventSourcing.Abstractions/JKang.EventSourcing.Abstractions.csproj @@ -6,11 +6,7 @@ - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - + diff --git a/src/JKang.EventSourcing.Options/JKang.EventSourcing.Options.csproj b/src/JKang.EventSourcing.Options/JKang.EventSourcing.Options.csproj index cde82f9..4ce6197 100644 --- a/src/JKang.EventSourcing.Options/JKang.EventSourcing.Options.csproj +++ b/src/JKang.EventSourcing.Options/JKang.EventSourcing.Options.csproj @@ -5,8 +5,8 @@ - - + + diff --git a/src/JKang.EventSourcing.Persistence.Caching/JKang.EventSourcing.Persistence.Caching.csproj b/src/JKang.EventSourcing.Persistence.Caching/JKang.EventSourcing.Persistence.Caching.csproj index 1365f5f..88bc574 100644 --- a/src/JKang.EventSourcing.Persistence.Caching/JKang.EventSourcing.Persistence.Caching.csproj +++ b/src/JKang.EventSourcing.Persistence.Caching/JKang.EventSourcing.Persistence.Caching.csproj @@ -6,8 +6,8 @@ - - + + diff --git a/src/JKang.EventSourcing.Persistence.CosmosDB/JKang.EventSourcing.Persistence.CosmosDB.csproj b/src/JKang.EventSourcing.Persistence.CosmosDB/JKang.EventSourcing.Persistence.CosmosDB.csproj index a376752..daab49c 100644 --- a/src/JKang.EventSourcing.Persistence.CosmosDB/JKang.EventSourcing.Persistence.CosmosDB.csproj +++ b/src/JKang.EventSourcing.Persistence.CosmosDB/JKang.EventSourcing.Persistence.CosmosDB.csproj @@ -5,11 +5,8 @@ - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + + diff --git a/src/JKang.EventSourcing.Persistence.DynamoDB.Tests/JKang.EventSourcing.Persistence.DynamoDB.Tests.csproj b/src/JKang.EventSourcing.Persistence.DynamoDB.Tests/JKang.EventSourcing.Persistence.DynamoDB.Tests.csproj index 6a3d5ca..823c548 100644 --- a/src/JKang.EventSourcing.Persistence.DynamoDB.Tests/JKang.EventSourcing.Persistence.DynamoDB.Tests.csproj +++ b/src/JKang.EventSourcing.Persistence.DynamoDB.Tests/JKang.EventSourcing.Persistence.DynamoDB.Tests.csproj @@ -1,21 +1,21 @@ - netcoreapp3.1 + net10.0 false - - - - - + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/JKang.EventSourcing.Persistence.DynamoDB/JKang.EventSourcing.Persistence.DynamoDB.csproj b/src/JKang.EventSourcing.Persistence.DynamoDB/JKang.EventSourcing.Persistence.DynamoDB.csproj index 48e40db..a0fb5a2 100644 --- a/src/JKang.EventSourcing.Persistence.DynamoDB/JKang.EventSourcing.Persistence.DynamoDB.csproj +++ b/src/JKang.EventSourcing.Persistence.DynamoDB/JKang.EventSourcing.Persistence.DynamoDB.csproj @@ -7,12 +7,8 @@ - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - + diff --git a/src/JKang.EventSourcing.Persistence.EfCore/JKang.EventSourcing.Persistence.EfCore.csproj b/src/JKang.EventSourcing.Persistence.EfCore/JKang.EventSourcing.Persistence.EfCore.csproj index 8416f9e..9e2bbc2 100644 --- a/src/JKang.EventSourcing.Persistence.EfCore/JKang.EventSourcing.Persistence.EfCore.csproj +++ b/src/JKang.EventSourcing.Persistence.EfCore/JKang.EventSourcing.Persistence.EfCore.csproj @@ -5,12 +5,8 @@ - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - + diff --git a/src/JKang.EventSourcing.Persistence.FileSystem/JKang.EventSourcing.Persistence.FileSystem.csproj b/src/JKang.EventSourcing.Persistence.FileSystem/JKang.EventSourcing.Persistence.FileSystem.csproj index 575a81b..5ea492b 100644 --- a/src/JKang.EventSourcing.Persistence.FileSystem/JKang.EventSourcing.Persistence.FileSystem.csproj +++ b/src/JKang.EventSourcing.Persistence.FileSystem/JKang.EventSourcing.Persistence.FileSystem.csproj @@ -5,12 +5,8 @@ - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + diff --git a/src/JKang.EventSourcing.Persistence.S3/JKang.EventSourcing.Persistence.S3.csproj b/src/JKang.EventSourcing.Persistence.S3/JKang.EventSourcing.Persistence.S3.csproj index d46327e..2da7c61 100644 --- a/src/JKang.EventSourcing.Persistence.S3/JKang.EventSourcing.Persistence.S3.csproj +++ b/src/JKang.EventSourcing.Persistence.S3/JKang.EventSourcing.Persistence.S3.csproj @@ -6,7 +6,7 @@ - + diff --git a/src/JKang.EventSourcing.TestingWebApp/JKang.EventSourcing.TestingWebApp.csproj b/src/JKang.EventSourcing.TestingWebApp/JKang.EventSourcing.TestingWebApp.csproj index c0061ef..013c22a 100644 --- a/src/JKang.EventSourcing.TestingWebApp/JKang.EventSourcing.TestingWebApp.csproj +++ b/src/JKang.EventSourcing.TestingWebApp/JKang.EventSourcing.TestingWebApp.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1 + net10.0 cb5278a6-703d-43fa-abf8-17397caac03b @@ -10,8 +10,8 @@ - - + + diff --git a/src/JKang.EventSourcing/JKang.EventSourcing.csproj b/src/JKang.EventSourcing/JKang.EventSourcing.csproj index 19e4144..7ac3ac2 100644 --- a/src/JKang.EventSourcing/JKang.EventSourcing.csproj +++ b/src/JKang.EventSourcing/JKang.EventSourcing.csproj @@ -5,10 +5,6 @@ - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - diff --git a/version.yml b/version.yml new file mode 100644 index 0000000..dccb359 --- /dev/null +++ b/version.yml @@ -0,0 +1,2 @@ +variables: + version: 1.2.0-preview.1