diff --git a/AGENTS.md b/AGENTS.md index 96daa64..0740320 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,7 +22,7 @@ Do not treat FeatureFusion as a framework. Install a BuildingBlock only when it | `BuildingBlocks.Mediator` | CQRS Send + ordered pipeline | [Mediator](src/BuildingBlocks/Mediator/AGENTS.md) | | `BuildingBlocks.Pagination.EntityFrameworkCore` | Keyset (cursor) paging for EF Core (**1.1.0**: any-width Npgsql row comparison, NULLS, `HasKeysetIndex` + `NullOrder`) | [Pagination.EF](src/BuildingBlocks/Pagination.EntityFrameworkCore/AGENTS.md) | | `BuildingBlocks.Mcp` | Same logic as HTTP as deny-by-default MCP tools | [Mcp](src/BuildingBlocks/Mcp/AGENTS.md) | -| `BuildingBlocks.Idempotency` | HTTP Idempotency-Key + optional Redis lock | [Idempotency](src/BuildingBlocks/Idempotency/AGENTS.md) | +| `BuildingBlocks.Idempotency` | HTTP Idempotency-Key + optional Redis lock (**1.0.1**: System.Text.Json, package icon) | [Idempotency](src/BuildingBlocks/Idempotency/AGENTS.md) | | `BuildingBlocks.Telemetry` | Config-driven OpenTelemetry (not a SigNoz SDK) | [Telemetry](src/BuildingBlocks/Telemetry/AGENTS.md) | | `BuildingBlocks.Aspire.Hosting.SigNoz` | Local AppHost SigNoz only (net10) | [SigNoz](src/BuildingBlocks/Aspire.Hosting.SigNoz/AGENTS.md) | diff --git a/CHANGELOG.md b/CHANGELOG.md index 1362956..77b4fba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - No hard PackageReference on Npgsql in the nupkg; row comparison requires the host already using Npgsql.EntityFrameworkCore.PostgreSQL. - Lab: `NameThenPrice` product sort (3-column keyset) + `AddBuildingBlocksPagination` / `UseBuildingBlocksPagination` on FeatureFusion. +## BuildingBlocks.Idempotency [1.0.1] - 2026-09-04 + +### Changed + +- **System.Text.Json** for cache envelope and MVC `ObjectResult` body capture (dropped `Newtonsoft.Json`). Minimal API path already used STJ. Default property names remain PascalCase. +- **Package icon** (`idempotency-icon.png`) for NuGet listing parity with other BuildingBlocks. + +### Notes + +- Cache JSON written by 1.0.0 (Newtonsoft) and 1.0.1 (STJ) are compatible for `IdempotencyCacheEntry` under default options; clear cache if you see odd replay after upgrading mid-flight. +- Package still depends on `Ulid` and `StackExchange.Redis` (Redis lock); hosts that use the lock must register `IConnectionMultiplexer`. + ## BuildingBlocks.Idempotency [1.0.0] - 2026-09-04 ### Added @@ -45,7 +57,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Notes - Distinct from MCP write idempotency (`UseMemoryIdempotency` / `IMcpIdempotencyStore` in BuildingBlocks.Mcp) -- Package depends on `Newtonsoft.Json` (MVC ObjectResult capture), `Ulid`, and `StackExchange.Redis` (Redis lock); hosts that use the lock must register `IConnectionMultiplexer` +- Package depended on `Newtonsoft.Json` (MVC ObjectResult capture), `Ulid`, and `StackExchange.Redis` (Redis lock); hosts that use the lock must register `IConnectionMultiplexer` ## BuildingBlocks.Pagination.EntityFrameworkCore [1.0.0] - 2026-08-29 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3ab853b..257fa1e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -46,7 +46,9 @@ Release tags (must match the project ``): `mediator-v*`, `mcp-v*`, `ide - HTTP `Idempotency-Key` only — do not fold MCP `IMcpIdempotencyStore` into this package. - Host owns `IDistributedCache` (and `IConnectionMultiplexer` when using the Redis lock). +- Prefer **System.Text.Json** for cache / MVC body capture (no Newtonsoft.Json). - Lab provenance: Experiments 3 / 4 / 12 under `tests/Lab/IntegrationTests/Experiments/`. +- Release tag: `idempotency-v*` must match `` (current **1.0.1**). ### BuildingBlocks.Telemetry / SigNoz hosting diff --git a/README.md b/README.md index 104d8bf..8d602d2 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ NuGet packages you can install in **your** hosts. The FeatureFusion API is a sho |---------|---------|------|------| | **[BuildingBlocks.Mediator](https://www.nuget.org/packages/BuildingBlocks.Mediator)** | **1.1.0** | CQRS **Send** + ordered pipeline (`ICommand` / `IQuery`, typed behaviors, opt-in traces + metrics) | net8 / net9 / net10 | | **[BuildingBlocks.Mcp](https://www.nuget.org/packages/BuildingBlocks.Mcp)** | **1.0.0** | Message types → MCP tools on the official SDK (deny-by-default, `McpResult`, HTTP + opt-in stdio) | net8 / net9 / net10 | -| **[BuildingBlocks.Idempotency](https://www.nuget.org/packages/BuildingBlocks.Idempotency)** | **1.0.0** | HTTP **Idempotency-Key** — MVC + Minimal API, 2xx envelope replay, ProblemDetails, optional Redis lock, fingerprint, ActivitySource | net8 / net9 / net10 | +| **[BuildingBlocks.Idempotency](https://www.nuget.org/packages/BuildingBlocks.Idempotency)** | **1.0.1** | HTTP **Idempotency-Key** — MVC + Minimal API, 2xx envelope replay, ProblemDetails, optional Redis lock, fingerprint, ActivitySource | net8 / net9 / net10 | | **[BuildingBlocks.Pagination.EntityFrameworkCore](https://www.nuget.org/packages/BuildingBlocks.Pagination.EntityFrameworkCore)** | **1.1.0** | Typed keyset (cursor) pagination for EF Core (IR bundled): any-width Npgsql row comparison, `NULLS FIRST/LAST`, `HasKeysetIndex` + `NullOrder` | net8 / net9 / net10 | | **[BuildingBlocks.Telemetry](https://www.nuget.org/packages/BuildingBlocks.Telemetry)** | **1.0.2** | Config-driven OpenTelemetry (traces, metrics, logs) + `IntegrateMediator` / opt-in `IntegrateMcp` | net8 / net9 / net10 | | **[BuildingBlocks.Aspire.Hosting.SigNoz](https://www.nuget.org/packages/BuildingBlocks.Aspire.Hosting.SigNoz)** | **1.0.0** | Local-dev Aspire `AddSigNoz()` + `WithSigNozOtlpExporter` | net10 (AppHost) | @@ -362,7 +362,7 @@ Cursor HTTP: ASP.NET Core HTTP **Idempotency-Key** for MVC and Minimal API. Host-owned `IDistributedCache`, **2xx** envelope replay, ProblemDetails on conflicts, optional Redis SET NX lock, opt-in method/path/body fingerprint, per-endpoint TTL, optional ActivitySource. Distinct from MCP write idempotency (`UseMemoryIdempotency` / `IMcpIdempotencyStore` above). -**What's new in 1.0.0:** shared `IdempotencyGate` for MVC + Minimal API, 2xx envelope replay, ProblemDetails, optional Redis lock / fingerprint / telemetry. NuGet and release badges resolve after `idempotency-v1.0.0` is tagged and published. +**What's new in 1.0.1:** NuGet package icon; **System.Text.Json** for cache envelope and MVC `ObjectResult` capture (dropped Newtonsoft.Json). No API surface change from 1.0.0. ```bash dotnet add package BuildingBlocks.Idempotency @@ -718,7 +718,7 @@ Install the packages above in your own hosts, **or** clone this repo and run **F | Telemetry | **`BuildingBlocks.Telemetry`** in ServiceDefaults; **`BuildingBlocks.Aspire.Hosting.SigNoz`** on AppHost | | Event bus | RabbitMQ + transactional outbox/inbox, DLQ, dedup hooks | | Aspire lab | AppHost orchestration for Postgres, Redis, RabbitMQ, Memcached, SigNoz | -| HTTP idempotency | **`BuildingBlocks.Idempotency`** — MVC + Minimal API, 2xx envelope replay, optional Redis lock (`POST /api/v2/Order/order`) | +| HTTP idempotency | **`BuildingBlocks.Idempotency` 1.0.1** — MVC + Minimal API, 2xx envelope replay, System.Text.Json, optional Redis lock (`POST /api/v2/Order/order`) | | Feature flags (demo) | ASP.NET Core Feature Management + custom filters (claims / VIP) | | API surface | Versioned controllers + Minimal APIs, FluentValidation patterns | | Gateway | YARP reverse proxy + Memcached distributed rate limiting | @@ -943,7 +943,7 @@ Conditional features via Microsoft.FeatureManagement and custom filters (e.g. VI ### HTTP idempotency (BuildingBlocks.Idempotency) -REST idempotency with `IDistributedCache` status tracking, MVC `[Idempotent]` / Minimal API `WithIdempotency`, and optional Redis lock (`POST /api/v2/Order/order`, smoke `POST /api/v2/idempotency-smoke`). See [BuildingBlocks.Idempotency](#buildingblocksidempotency). +REST idempotency with `IDistributedCache` status tracking, MVC `[Idempotent]` / Minimal API `WithIdempotency`, optional Redis lock, and **System.Text.Json** cache/body serialization (`POST /api/v2/Order/order`, smoke `POST /api/v2/idempotency-smoke`). Package **1.0.1**. See [BuildingBlocks.Idempotency](#buildingblocksidempotency). - [Idempotency with CQRS](https://www.linkedin.com/feed/update/urn:li:activity:7303686809891356676/) - [IdempotentFusion project](https://www.linkedin.com/feed/update/urn:li:activity:7309149985307029504/) (historical Lab name) @@ -986,7 +986,7 @@ See [Pagination showcase](#pagination-showcase) for the FeatureFusion catalog (` | **Polling publisher** | `OutBoxWorker` background poll → publish | | **Dead letter queue** | EventBus DLX / DLQ topology | | **Message deduplication** | Inbox + `MessageDeduplicationService` | -| **Idempotency** | `BuildingBlocks.Idempotency` — `[Idempotent]` / `WithIdempotency`, cache envelope, optional Redis lock | +| **Idempotency** | `BuildingBlocks.Idempotency` **1.0.1** — `[Idempotent]` / `WithIdempotency`, System.Text.Json cache envelope, optional Redis lock | | **Feature toggle** | ASP.NET Core Feature Management + custom filters | | **Rate limiting** | ApiGateway Memcached fixed-window limiter | | **Circuit breaker / resilience** | Polly `ResiliencePipelineFactory` | diff --git a/docs/building-blocks/idempotency.md b/docs/building-blocks/idempotency.md index 8f4dfe1..751d555 100644 --- a/docs/building-blocks/idempotency.md +++ b/docs/building-blocks/idempotency.md @@ -2,7 +2,15 @@ ASP.NET Core HTTP **Idempotency-Key** for MVC and Minimal API: host-owned `IDistributedCache`, optional Redis lock, opt-in fingerprint, ProblemDetails on conflicts, optional ActivitySource. -Canonical consumer docs: [package README](../../src/BuildingBlocks/Idempotency/PACKAGE_README.md). Agent notes: [AGENTS.md](../../src/BuildingBlocks/Idempotency/AGENTS.md). +[![NuGet](https://img.shields.io/nuget/v/BuildingBlocks.Idempotency.svg)](https://www.nuget.org/packages/BuildingBlocks.Idempotency) +[![GitHub Release](https://img.shields.io/github/v/release/Maxofpower/FeatureFusion?filter=idempotency-v*&logo=github&label=GitHub%20Release)](https://github.com/Maxofpower/FeatureFusion/releases?q=idempotency-v) + +**Current package version: 1.0.1.** Canonical consumer docs: [package README](../../src/BuildingBlocks/Idempotency/PACKAGE_README.md). Agent notes: [AGENTS.md](../../src/BuildingBlocks/Idempotency/AGENTS.md). + +## What's new in 1.0.1 + +- NuGet package icon +- **System.Text.Json** for cache envelope and MVC `ObjectResult` capture (no Newtonsoft.Json). No public API change from 1.0.0. ## Install diff --git a/docs/building-blocks/pagination.md b/docs/building-blocks/pagination.md index a10a805..5d4b558 100644 --- a/docs/building-blocks/pagination.md +++ b/docs/building-blocks/pagination.md @@ -2,6 +2,9 @@ Typed keyset (cursor) pagination. **One NuGet:** `BuildingBlocks.Pagination.EntityFrameworkCore` **1.1.0** (IR + EF Core adapter). Dapper is an in-repo project, not a package. +[![NuGet](https://img.shields.io/nuget/v/BuildingBlocks.Pagination.EntityFrameworkCore.svg)](https://www.nuget.org/packages/BuildingBlocks.Pagination.EntityFrameworkCore) +[![GitHub Release](https://img.shields.io/github/v/release/Maxofpower/FeatureFusion?filter=pagination-v*&logo=github&label=GitHub%20Release)](https://github.com/Maxofpower/FeatureFusion/releases?q=pagination-v) + See ADR [0003](../adr/0003-pagination-keyset.md), the [test matrix](PAGINATION_TEST_MATRIX.md), and [`PACKAGE_README.md`](../../src/BuildingBlocks/Pagination.EntityFrameworkCore/PACKAGE_README.md). ## Install diff --git a/docs/lab/README.md b/docs/lab/README.md index 272a112..3a8d7c1 100644 --- a/docs/lab/README.md +++ b/docs/lab/README.md @@ -53,7 +53,7 @@ Aspire: Postgres │ Redis │ RabbitMQ │ Memcached │ SigNoz | Family | Experiments | Outcome | |--------|-------------|---------| | Keyset pagination abuse | 1–2 | Careless HTTP/MCP cursor clients characterized | -| HTTP Redis idempotency | 3, 4, 12, 15 | Miss/hit, concurrency, fingerprint, ProcessingTtl lease overlap → **BuildingBlocks.Idempotency 1.0.0** | +| HTTP Redis idempotency | 3, 4, 12, 15 | Miss/hit, concurrency, fingerprint, ProcessingTtl lease overlap → **BuildingBlocks.Idempotency 1.0.1** | | MCP write / agent-client semantics | 6, 13, 14, 16 | Confirm+replay, regenerated keys amplify, concurrent same-key safe, **IMcpRateLimiter** bounds distinct-key storms | | Outbox → bus → inbox happy path | 5, 8, 10 | HTTP + MCP parity; outbox row lifecycle | | Consumer dedup / failure | 7, 9, 11, 17 | Inbox dedup; retry/DLQ; `EnableDeduplication` + `processed_messages` | @@ -74,7 +74,7 @@ Do **not** extract: Scenario DSL, one-off test gates, fixed-permit rate limiters ## Package / versioning (current) -- Each packable project owns `` in its `.csproj` (e.g. `BuildingBlocks.Pagination.EntityFrameworkCore` `1.1.0`, `BuildingBlocks.Mcp` `1.0.0`). +- Each packable project owns `` in its `.csproj` (e.g. `BuildingBlocks.Pagination.EntityFrameworkCore` `1.1.0`, `BuildingBlocks.Idempotency` `1.0.1`, `BuildingBlocks.Mcp` `1.0.0`). - Release tags are package-prefixed (`mcp-v*`, `mediator-v*`, `telemetry-v*`, …) and must match ``. - Per-package GitHub Actions: `*-yml` CI + `*-release.yml` pack / Trusted Publishing to nuget.org (`idempotency.yml` / `idempotency-release.yml` for this package). - IntegrationTests / Exp 1–18 are **not** in CI (local Aspire/Docker). diff --git a/docs/linkedin-posts.md b/docs/linkedin-posts.md index 63e72f9..979d753 100644 --- a/docs/linkedin-posts.md +++ b/docs/linkedin-posts.md @@ -15,7 +15,7 @@ Umbrella narrative (lab → packages): [Medium](https://medium.com/@m2hweb86/fea | `cursor-pagination` | Reusable generic bidirectional keyset (cursor) pagination | published | https://www.linkedin.com/feed/update/urn:li:activity:7325068550614708225/ | `src/BuildingBlocks/Pagination.EntityFrameworkCore` + lab `ProductService` | `building-blocks-overview` | Typed `SortKey`, opaque cursors, one EF Core nupkg (**1.1.0**: Npgsql row comparison, NULLS interceptor). Optional `QueryHint` (default none). Lab: MVC `POST /api/v2/Product/products`, Minimal API `GET /api/v2/products-page` (POST kept), Dapper `products-dapper`, MCP `products.list` — one `GetProductsQuery`. | | `mcp-message-tools` | Message types as MCP tools | published | https://lnkd.in/p/e4-FrM44 | `src/BuildingBlocks/Mcp` + `docs/building-blocks/mcp.md` | `building-blocks-overview` | Deny-by-default `[McpTool]` / `WithMcp` / `MapTool` on the official SDK. Same logic as HTTP. Interim permalink is the umbrella post until a dedicated MCP post ships. Not OpenAPI, not a SOLID linter. | | `idempotency-cqrs` | Idempotency with CQRS commands | published | https://www.linkedin.com/feed/update/urn:li:activity:7303686809891356676/ | (pattern lab) | `idempotentfusion` | **Lab only, not NuGet.** Command-level idempotency via reusable `IdentifiedCommand` + ULID `Idempotency-Key`, intercepting before handlers. Complementary to Mediator — not core v1. | -| `idempotentfusion` | IdempotentFusion (REST API) | published | https://www.linkedin.com/feed/update/urn:li:activity:7309149985307029504/ | `src/BuildingBlocks/Idempotency` + Lab `OrderController` | `idempotency-cqrs` | HTTP Idempotency-Key via `BuildingBlocks.Idempotency` (Redis cache host-owned, optional SET NX lock). Lab sets `UserIdFallback = "123"`. | +| `idempotentfusion` | IdempotentFusion (REST API) | published | https://www.linkedin.com/feed/update/urn:li:activity:7309149985307029504/ | `src/BuildingBlocks/Idempotency` + Lab `OrderController` | `idempotency-cqrs` | HTTP Idempotency-Key via `BuildingBlocks.Idempotency` **1.0.1** (Redis cache host-owned, optional SET NX lock, System.Text.Json). Lab sets `UserIdFallback = "123"`. | | `eventbus-outbox-inbox` | Lightweight modular EventBus (outbox, inbox, DLQ) | published | https://www.linkedin.com/posts/mhhoseini_a-lightweight-modular-eventbus-implementation-activity-7316381126535663631-6FiI | `src/Lab/EventBus` | `aspire-integration-testing` | **Lab only, not NuGet.** Sibling of Mediator Send — not `INotification`. | | `aspire-integration-testing` | Integration testing microservices with Aspire | published | https://www.linkedin.com/posts/mhhoseini_integration-testing-microservices-with-aspire-activity-7321088554565029888-Z0a8 | `tests/Lab/IntegrationTests` + `src/Lab/FeatureFusion.AppHost` | `building-blocks-overview` | **Lab only, not NuGet.** Aspire-hosted fixture against real Postgres / Redis / RabbitMQ / Memcached. | | `ddd-architecture` | Diving into DDD | published | https://www.linkedin.com/posts/mhhoseini_diving-into-ddd-activity-7427983383126896640-Eaaw | lab vertical slices under `src/Lab/FeatureFusion/Features` | `medium-engineering-lab` | **Lab only, not NuGet.** Architecture notes; FeatureFusion is the showcase host, not a DDD framework. | diff --git a/llms.txt b/llms.txt index 5f0a9ba..23ae73c 100644 --- a/llms.txt +++ b/llms.txt @@ -4,11 +4,26 @@ ## Canonical -- [README](https://github.com/Maxofpower/FeatureFusion/blob/main/README.md): lab + package overview +- [README](https://github.com/Maxofpower/FeatureFusion/blob/main/README.md): lab + package overview (versions below) - [AGENTS.md](https://github.com/Maxofpower/FeatureFusion/blob/main/AGENTS.md): repo map for coding agents +- [CHANGELOG](https://github.com/Maxofpower/FeatureFusion/blob/main/CHANGELOG.md): BuildingBlocks release notes - [Medium](https://medium.com/@m2hweb86/featurefusion-an-engineering-lab-that-extracts-reusable-net-building-blocks-28152cafafb9): lab-to-package narrative - [Writing catalog](https://github.com/Maxofpower/FeatureFusion/blob/main/docs/linkedin-posts.md): LinkedIn / Medium post ↔ code map - [CONTRIBUTING](https://github.com/Maxofpower/FeatureFusion/blob/main/CONTRIBUTING.md) +- [web/README](https://github.com/Maxofpower/FeatureFusion/blob/main/web/README.md): reserved Next.js frontend root (not in the .NET solution) + +## Packages (current versions) + +| Package | Version | NuGet | GitHub releases | +|---------|---------|-------|-----------------| +| BuildingBlocks.Mediator | 1.1.0 | [nuget.org](https://www.nuget.org/packages/BuildingBlocks.Mediator/1.1.0) | [mediator-v*](https://github.com/Maxofpower/FeatureFusion/releases?q=mediator-v) | +| BuildingBlocks.Mcp | 1.0.0 | [nuget.org](https://www.nuget.org/packages/BuildingBlocks.Mcp/1.0.0) | [mcp-v*](https://github.com/Maxofpower/FeatureFusion/releases?q=mcp-v) | +| BuildingBlocks.Idempotency | 1.0.1 | [nuget.org](https://www.nuget.org/packages/BuildingBlocks.Idempotency/1.0.1) | [idempotency-v*](https://github.com/Maxofpower/FeatureFusion/releases?q=idempotency-v) | +| BuildingBlocks.Pagination.EntityFrameworkCore | 1.1.0 | [nuget.org](https://www.nuget.org/packages/BuildingBlocks.Pagination.EntityFrameworkCore/1.1.0) | [pagination-v*](https://github.com/Maxofpower/FeatureFusion/releases?q=pagination-v) | +| BuildingBlocks.Telemetry | 1.0.2 | [nuget.org](https://www.nuget.org/packages/BuildingBlocks.Telemetry/1.0.2) | [telemetry-v*](https://github.com/Maxofpower/FeatureFusion/releases?q=telemetry-v) | +| BuildingBlocks.Aspire.Hosting.SigNoz | 1.0.0 | [nuget.org](https://www.nuget.org/packages/BuildingBlocks.Aspire.Hosting.SigNoz/1.0.0) | [signoz-v*](https://github.com/Maxofpower/FeatureFusion/releases?q=signoz-v) | + +**Highlights:** Pagination **1.1.0** — any-width Npgsql row comparison, `NULLS FIRST/LAST` interceptor, `HasKeysetIndex` + `NullOrder`. Idempotency **1.0.1** — package icon, System.Text.Json only (no Newtonsoft). ## BuildingBlocks — NuGet READMEs @@ -33,13 +48,17 @@ ## Guides - [Getting started](https://github.com/Maxofpower/FeatureFusion/blob/main/docs/building-blocks/getting-started.md) +- [Cookbook](https://github.com/Maxofpower/FeatureFusion/blob/main/docs/building-blocks/cookbook.md) - [Mediator](https://github.com/Maxofpower/FeatureFusion/blob/main/docs/building-blocks/mediator.md) -- [Idempotency](https://github.com/Maxofpower/FeatureFusion/blob/main/docs/building-blocks/idempotency.md) -- [Pagination](https://github.com/Maxofpower/FeatureFusion/blob/main/docs/building-blocks/pagination.md) +- [Pipeline behaviors](https://github.com/Maxofpower/FeatureFusion/blob/main/docs/building-blocks/pipeline-behaviors.md) +- [Idempotency](https://github.com/Maxofpower/FeatureFusion/blob/main/docs/building-blocks/idempotency.md) (1.0.1) +- [Pagination](https://github.com/Maxofpower/FeatureFusion/blob/main/docs/building-blocks/pagination.md) (1.1.0) +- [Pagination test matrix](https://github.com/Maxofpower/FeatureFusion/blob/main/docs/building-blocks/PAGINATION_TEST_MATRIX.md) - [MCP](https://github.com/Maxofpower/FeatureFusion/blob/main/docs/building-blocks/mcp.md) - [Telemetry](https://github.com/Maxofpower/FeatureFusion/blob/main/docs/building-blocks/telemetry.md) +- [ADR 0003 — pagination keyset](https://github.com/Maxofpower/FeatureFusion/blob/main/docs/adr/0003-pagination-keyset.md) ## Lab integration experiments - [Lab charter](https://github.com/Maxofpower/FeatureFusion/blob/main/docs/lab/README.md): evidence families, extraction rules, roadmap -- [Experiments README](https://github.com/Maxofpower/FeatureFusion/blob/main/tests/Lab/IntegrationTests/Experiments/README.md): behavioral experiment catalog Exp 1–18 + MAF prototype, evidence model, conventions +- [Experiments README](https://github.com/Maxofpower/FeatureFusion/blob/main/tests/Lab/IntegrationTests/Experiments/README.md): behavioral experiment catalog Exp 1–18 + MAF prototype, evidence model, conventions (HTTP Idempotency Exp 3/4/12/15 → package 1.0.1) diff --git a/src/BuildingBlocks/Aspire.Hosting.SigNoz/PACKAGE_README.md b/src/BuildingBlocks/Aspire.Hosting.SigNoz/PACKAGE_README.md index 47c3128..1025437 100644 --- a/src/BuildingBlocks/Aspire.Hosting.SigNoz/PACKAGE_README.md +++ b/src/BuildingBlocks/Aspire.Hosting.SigNoz/PACKAGE_README.md @@ -5,6 +5,7 @@ Local Aspire AppHost integration for SigNoz. Production telemetry belongs in `Bu **TFM:** net10.0 (Aspire 13.4.6 AppHost). Docker required. [![NuGet](https://img.shields.io/nuget/v/BuildingBlocks.Aspire.Hosting.SigNoz.svg)](https://www.nuget.org/packages/BuildingBlocks.Aspire.Hosting.SigNoz) +[![GitHub Release](https://img.shields.io/github/v/release/Maxofpower/FeatureFusion?filter=signoz-v*&logo=github&label=GitHub%20Release)](https://github.com/Maxofpower/FeatureFusion/releases?q=signoz-v) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) | API | Role | diff --git a/src/BuildingBlocks/Idempotency/AGENTS.md b/src/BuildingBlocks/Idempotency/AGENTS.md index f6050bb..efade0b 100644 --- a/src/BuildingBlocks/Idempotency/AGENTS.md +++ b/src/BuildingBlocks/Idempotency/AGENTS.md @@ -1,6 +1,6 @@ # BuildingBlocks.Idempotency — agent notes -**Status: COMPLETE — 1.0.0** +**Status: COMPLETE — 1.0.1** HTTP **Idempotency-Key** for ASP.NET Core. Consumer docs: [PACKAGE_README.md](PACKAGE_README.md). Lab guide: [docs/building-blocks/idempotency.md](../../../docs/building-blocks/idempotency.md). @@ -35,7 +35,7 @@ app.MapPost("/path", handler).WithIdempotency(useLock: true); - Errors: ProblemDetails (`https://buildingblocks.dev/errors/idempotency/...`) - Fingerprint default **off** (Exp 3); on → method+path+body SHA-256 (Exp 12) - Lock only around GetOrCreate when `UseLock` (Exp 4) -- MVC ObjectResult body: Newtonsoft.Json; Minimal API `IResult`: System.Text.Json +- MVC ObjectResult body and cache envelope: System.Text.Json; Minimal API `IResult`: System.Text.Json - Telemetry optional; no cache-key tag by default; no BuildingBlocks.Telemetry package ref ## Lab evidence diff --git a/src/BuildingBlocks/Idempotency/AspNetCore/IdempotentAttributeFilter.cs b/src/BuildingBlocks/Idempotency/AspNetCore/IdempotentAttributeFilter.cs index 1cd12f1..c219850 100644 --- a/src/BuildingBlocks/Idempotency/AspNetCore/IdempotentAttributeFilter.cs +++ b/src/BuildingBlocks/Idempotency/AspNetCore/IdempotentAttributeFilter.cs @@ -1,9 +1,9 @@ +using System.Text.Json; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Logging; -using Newtonsoft.Json; namespace BuildingBlocks.Idempotency.AspNetCore; @@ -121,7 +121,7 @@ await _gate /// /// Captures a successful action result for cache storage. - /// Keeps Newtonsoft serialization for ObjectResult values (same representation as historical Lab/Exp 3). + /// Serializes values with (System.Text.Json). /// Caches all HTTP 2xx results (including null StatusCode meaning 200). /// internal static bool TryCaptureSuccessfulResponse( @@ -138,7 +138,7 @@ internal static bool TryCaptureSuccessfulResponse( { statusCode = obj.StatusCode ?? StatusCodes.Status200OK; contentType = "application/json"; - body = JsonConvert.SerializeObject(obj.Value); + body = JsonSerializer.Serialize(obj.Value); return true; } diff --git a/src/BuildingBlocks/Idempotency/BuildingBlocks.Idempotency.csproj b/src/BuildingBlocks/Idempotency/BuildingBlocks.Idempotency.csproj index c7b1a3b..8c418e4 100644 --- a/src/BuildingBlocks/Idempotency/BuildingBlocks.Idempotency.csproj +++ b/src/BuildingBlocks/Idempotency/BuildingBlocks.Idempotency.csproj @@ -12,11 +12,11 @@ true BuildingBlocks.Idempotency BuildingBlocks.Idempotency - 1.0.0 + 1.0.1 Mohammad Hasan Hosseini Mohammad Hasan Hosseini Copyright (c) 2026 Mohammad Hasan Hosseini - ASP.NET Core HTTP Idempotency-Key filter (MVC + Minimal API): shared IdempotencyGate over IDistributedCache, 2xx response envelope replay, RFC 9457 ProblemDetails errors, optional Redis SET NX lock, ProcessingTtl abandoned recovery, opt-in method+path+body fingerprinting, per-endpoint TTL, and optional ActivitySource instrumentation. Host owns cache registration. No MCP or Telemetry package coupling. + ASP.NET Core HTTP Idempotency-Key filter (MVC + Minimal API): shared IdempotencyGate over IDistributedCache, 2xx response envelope replay, RFC 9457 ProblemDetails errors, optional Redis SET NX lock, ProcessingTtl abandoned recovery, opt-in method+path+body fingerprinting, per-endpoint TTL, and optional ActivitySource instrumentation. Host owns cache registration. Uses System.Text.Json. No MCP or Telemetry package coupling. idempotency;idempotency-key;aspnetcore;minimal-api;redis;distributed-lock;distributed-cache;fingerprint;problemdetails;opentelemetry;net8;net9;net10 MIT https://github.com/Maxofpower/FeatureFusion @@ -24,7 +24,7 @@ git PACKAGE_README.md idempotency-icon.png - 1.0.0: IdempotencyGate; MVC + Minimal API; 2xx envelope replay; ProblemDetails errors; ProcessingTtl / EntryTtl + per-endpoint overrides; optional Redis lock; opt-in method+path+body fingerprint; fluent UseRedisLock/UseTelemetry. Host owns IDistributedCache. https://github.com/Maxofpower/FeatureFusion/blob/main/CHANGELOG.md + 1.0.1: package icon; System.Text.Json for cache + MVC ObjectResult (drop Newtonsoft.Json). https://github.com/Maxofpower/FeatureFusion/blob/main/CHANGELOG.md true true true @@ -43,7 +43,6 @@ - diff --git a/src/BuildingBlocks/Idempotency/Core/IdempotencyGate.cs b/src/BuildingBlocks/Idempotency/Core/IdempotencyGate.cs index 4711028..6259824 100644 --- a/src/BuildingBlocks/Idempotency/Core/IdempotencyGate.cs +++ b/src/BuildingBlocks/Idempotency/Core/IdempotencyGate.cs @@ -1,8 +1,8 @@ using System.Text; +using System.Text.Json; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Logging; -using Newtonsoft.Json; namespace BuildingBlocks.Idempotency; @@ -166,7 +166,7 @@ public async Task CompleteAsync( RequestFingerprint = requestFingerprint, ProcessingExpiresAtUtc = null }; - var cacheEntryData = JsonConvert.SerializeObject(cacheEntry); + var cacheEntryData = JsonSerializer.Serialize(cacheEntry); var entryTtl = endpoint.EntryTtl ?? _options.EntryTtl; await _distributedCache.SetAsync( @@ -289,7 +289,7 @@ await _idempotencyLock.ReleaseAsync(lockKey, lockValue, CancellationToken.None) var cacheData = await _distributedCache.GetAsync(cacheKey, cancellationToken).ConfigureAwait(false); if (cacheData != null) { - var existing = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(cacheData)); + var existing = JsonSerializer.Deserialize(Encoding.UTF8.GetString(cacheData)); if (existing is not null && existing.Status == "Processing" && IsProcessingExpired(existing)) @@ -310,7 +310,7 @@ await _idempotencyLock.ReleaseAsync(lockKey, lockValue, CancellationToken.None) RequestFingerprint = requestFingerprint, ProcessingExpiresAtUtc = expiresAt }; - var cacheEntryData = JsonConvert.SerializeObject(cacheEntry); + var cacheEntryData = JsonSerializer.Serialize(cacheEntry); await _distributedCache.SetAsync( cacheKey, @@ -345,7 +345,7 @@ private async Task MarkRequestAsProcessingAsync( RequestFingerprint = requestFingerprint, ProcessingExpiresAtUtc = DateTimeOffset.UtcNow.Add(processingTtl) }; - var cacheEntryData = JsonConvert.SerializeObject(cacheEntry); + var cacheEntryData = JsonSerializer.Serialize(cacheEntry); await _distributedCache.SetAsync( cacheKey, diff --git a/src/BuildingBlocks/Idempotency/PACKAGE_README.md b/src/BuildingBlocks/Idempotency/PACKAGE_README.md index b10130e..0ac1804 100644 --- a/src/BuildingBlocks/Idempotency/PACKAGE_README.md +++ b/src/BuildingBlocks/Idempotency/PACKAGE_README.md @@ -3,18 +3,24 @@ ASP.NET Core **Idempotency-Key** for MVC and Minimal API. Host-owned `IDistributedCache`, optional Redis lock, opt-in request fingerprint, RFC 9457 ProblemDetails on conflicts, optional ActivitySource. [![NuGet](https://img.shields.io/nuget/v/BuildingBlocks.Idempotency.svg)](https://www.nuget.org/packages/BuildingBlocks.Idempotency) +[![GitHub Release](https://img.shields.io/github/v/release/Maxofpower/FeatureFusion?filter=idempotency-v*&logo=github&label=GitHub%20Release)](https://github.com/Maxofpower/FeatureFusion/releases?q=idempotency-v) [![.NET](https://img.shields.io/badge/.NET-8%20%7C%209%20%7C%2010-512BD4)](https://dotnet.microsoft.com/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) Requires **.NET 8**, **.NET 9**, or **.NET 10**. +## What's new in 1.0.1 + +- **Package icon** for NuGet listing parity with other BuildingBlocks +- **System.Text.Json** for the cache envelope and MVC `ObjectResult` body capture (dropped `Newtonsoft.Json`). Minimal API already used STJ. No public API change from 1.0.0. + ## Install ```bash dotnet add package BuildingBlocks.Idempotency ``` -## Features (1.0.0) +## Features - Shared gate for **MVC** (`[Idempotent]`) and **Minimal API** (`WithIdempotency`) - Cache **HTTP 2xx** envelopes (status, content-type, body) and replay them @@ -26,6 +32,7 @@ dotnet add package BuildingBlocks.Idempotency - `DuplicateCompletedBehavior`: Replay (default) or Conflict - ProblemDetails errors with stable `type` URIs - Optional telemetry (`.UseTelemetry()`); host `AddSource("BuildingBlocks.Idempotency")` +- **System.Text.Json** only (cache + MVC + Minimal API) ## Quick start diff --git a/src/BuildingBlocks/Mcp/PACKAGE_README.md b/src/BuildingBlocks/Mcp/PACKAGE_README.md index ecf800c..40f5c1a 100644 --- a/src/BuildingBlocks/Mcp/PACKAGE_README.md +++ b/src/BuildingBlocks/Mcp/PACKAGE_README.md @@ -3,6 +3,7 @@ Map **application message types** (commands, queries, DTOs) and **public static Minimal API methods** to MCP tools on the official C# SDK. Deny-by-default catalog, typed `McpResult`, HTTP by default (stdio opt-in). Does not require BuildingBlocks.Mediator. [![NuGet](https://img.shields.io/nuget/v/BuildingBlocks.Mcp.svg)](https://www.nuget.org/packages/BuildingBlocks.Mcp) +[![GitHub Release](https://img.shields.io/github/v/release/Maxofpower/FeatureFusion?filter=mcp-v*&logo=github&label=GitHub%20Release)](https://github.com/Maxofpower/FeatureFusion/releases?q=mcp-v) [![.NET](https://img.shields.io/badge/.NET-8%20%7C%209%20%7C%2010-512BD4)](https://dotnet.microsoft.com/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) diff --git a/src/BuildingBlocks/Mediator/PACKAGE_README.md b/src/BuildingBlocks/Mediator/PACKAGE_README.md index e8e8cad..84a9f26 100644 --- a/src/BuildingBlocks/Mediator/PACKAGE_README.md +++ b/src/BuildingBlocks/Mediator/PACKAGE_README.md @@ -3,6 +3,7 @@ CQRS-first **Send** + ordered **pipeline** for .NET 8+. Commands and queries, host-owned behaviors, optional traces and metrics, startup handler checks. [![NuGet](https://img.shields.io/nuget/v/BuildingBlocks.Mediator.svg)](https://www.nuget.org/packages/BuildingBlocks.Mediator) +[![GitHub Release](https://img.shields.io/github/v/release/Maxofpower/FeatureFusion?filter=mediator-v*&logo=github&label=GitHub%20Release)](https://github.com/Maxofpower/FeatureFusion/releases?q=mediator-v) [![.NET](https://img.shields.io/badge/.NET-8%20%7C%209%20%7C%2010-512BD4)](https://dotnet.microsoft.com/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) diff --git a/src/BuildingBlocks/Pagination.EntityFrameworkCore/PACKAGE_README.md b/src/BuildingBlocks/Pagination.EntityFrameworkCore/PACKAGE_README.md index 99148b0..af36f53 100644 --- a/src/BuildingBlocks/Pagination.EntityFrameworkCore/PACKAGE_README.md +++ b/src/BuildingBlocks/Pagination.EntityFrameworkCore/PACKAGE_README.md @@ -3,6 +3,7 @@ Typed **keyset (cursor)** pagination for EF Core: `SortKey`, opaque versioned cursors, and `IQueryable.ToCursorPageAsync`. One NuGet — the IR assembly is bundled; you do not install a second Pagination package. [![NuGet](https://img.shields.io/nuget/v/BuildingBlocks.Pagination.EntityFrameworkCore.svg)](https://www.nuget.org/packages/BuildingBlocks.Pagination.EntityFrameworkCore) +[![GitHub Release](https://img.shields.io/github/v/release/Maxofpower/FeatureFusion?filter=pagination-v*&logo=github&label=GitHub%20Release)](https://github.com/Maxofpower/FeatureFusion/releases?q=pagination-v) [![.NET](https://img.shields.io/badge/.NET-8%20%7C%209%20%7C%2010-512BD4)](https://dotnet.microsoft.com/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) diff --git a/src/BuildingBlocks/Telemetry/PACKAGE_README.md b/src/BuildingBlocks/Telemetry/PACKAGE_README.md index ae034a3..5e312e5 100644 --- a/src/BuildingBlocks/Telemetry/PACKAGE_README.md +++ b/src/BuildingBlocks/Telemetry/PACKAGE_README.md @@ -3,6 +3,7 @@ Config-driven OpenTelemetry traces, metrics, and logs for ASP.NET Core from one `AddTelemetry` call. Export **OTLP to any backend**. Requires `IHostApplicationBuilder`. [![NuGet](https://img.shields.io/nuget/v/BuildingBlocks.Telemetry.svg)](https://www.nuget.org/packages/BuildingBlocks.Telemetry) +[![GitHub Release](https://img.shields.io/github/v/release/Maxofpower/FeatureFusion?filter=telemetry-v*&logo=github&label=GitHub%20Release)](https://github.com/Maxofpower/FeatureFusion/releases?q=telemetry-v) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) ## What's new in 1.0.2 diff --git a/tests/BuildingBlocks/Idempotency.Tests/IdempotentAttributeFilterTests.cs b/tests/BuildingBlocks/Idempotency.Tests/IdempotentAttributeFilterTests.cs index dd27423..7264403 100644 --- a/tests/BuildingBlocks/Idempotency.Tests/IdempotentAttributeFilterTests.cs +++ b/tests/BuildingBlocks/Idempotency.Tests/IdempotentAttributeFilterTests.cs @@ -1,6 +1,7 @@ using System.Diagnostics; using System.Security.Claims; using System.Text; +using System.Text.Json; using BuildingBlocks.Idempotency; using BuildingBlocks.Idempotency.AspNetCore; using BuildingBlocks.Idempotency.DependencyInjection; @@ -15,7 +16,6 @@ using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Moq; -using Newtonsoft.Json; using Xunit; namespace BuildingBlocks.Idempotency.Tests; @@ -179,7 +179,7 @@ public async Task FirstRequest_CachesEnvelope() var cachedData = await _distributedCache.GetAsync(cacheKey); Assert.NotNull(cachedData); - var cachedEntry = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(cachedData)); + var cachedEntry = JsonSerializer.Deserialize(Encoding.UTF8.GetString(cachedData)); Assert.Equal("Completed", cachedEntry?.Status); Assert.Equal(200, cachedEntry?.StatusCode); Assert.Equal("application/json", cachedEntry?.ContentType); @@ -220,7 +220,7 @@ public async Task DuplicateRequest_ReplaysStoredStatusAndContentType() StatusCode = 200, ContentType = "application/json" }; - await _distributedCache.SetAsync(cacheKey, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(cacheEntry))); + await _distributedCache.SetAsync(cacheKey, Encoding.UTF8.GetBytes(JsonSerializer.Serialize(cacheEntry))); var filter = CreateFilter(); var context = CreateActionContext(httpContext); @@ -243,7 +243,7 @@ public async Task DuplicateCompleted_ConflictStrategy_Returns409Problem() }; var idempotencyKey = Ulid.NewUlid().ToString(); var cacheKey = $"Idempotency_1_{idempotencyKey}"; - await _distributedCache.SetAsync(cacheKey, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(new IdempotencyCacheEntry + await _distributedCache.SetAsync(cacheKey, Encoding.UTF8.GetBytes(JsonSerializer.Serialize(new IdempotencyCacheEntry { Status = "Completed", Response = "{}", @@ -268,7 +268,7 @@ public async Task ProcessingRequest_ReturnsConflictProblem() Status = "Processing", ProcessingExpiresAtUtc = DateTimeOffset.UtcNow.AddMinutes(1) }; - await _distributedCache.SetAsync(cacheKey, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(cacheEntry))); + await _distributedCache.SetAsync(cacheKey, Encoding.UTF8.GetBytes(JsonSerializer.Serialize(cacheEntry))); var filter = CreateFilter(); var context = CreateActionContext(httpContext); @@ -289,7 +289,7 @@ public async Task ExpiredProcessing_AllowsNewExecution() Status = "Processing", ProcessingExpiresAtUtc = DateTimeOffset.UtcNow.AddMinutes(-1) }; - await _distributedCache.SetAsync(cacheKey, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(stale))); + await _distributedCache.SetAsync(cacheKey, Encoding.UTF8.GetBytes(JsonSerializer.Serialize(stale))); var filter = CreateFilter(); var context = CreateActionContext(httpContext); @@ -304,7 +304,7 @@ await filter.OnActionExecutionAsync(context, () => Assert.True(ran); var cachedData = await _distributedCache.GetAsync(cacheKey); - var entry = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(cachedData!)); + var entry = JsonSerializer.Deserialize(Encoding.UTF8.GetString(cachedData!)); Assert.Equal("Completed", entry?.Status); } @@ -449,7 +449,7 @@ public async Task PerEndpointProcessingTtl_IsApplied() var cacheKey = $"Idempotency_7_{idempotencyKey}"; var raw = await _distributedCache.GetAsync(cacheKey); Assert.NotNull(raw); - var entry = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(raw)); + var entry = JsonSerializer.Deserialize(Encoding.UTF8.GetString(raw)); Assert.NotNull(entry?.ProcessingExpiresAtUtc); var remaining = entry!.ProcessingExpiresAtUtc!.Value - DateTimeOffset.UtcNow; Assert.True(remaining <= TimeSpan.FromSeconds(30) + TimeSpan.FromSeconds(2)); diff --git a/tests/Lab/IntegrationTests/Experiments/CacheVsProduction/CacheVsProductionExperimentTests.cs b/tests/Lab/IntegrationTests/Experiments/CacheVsProduction/CacheVsProductionExperimentTests.cs index 0d4b4f2..e967e92 100644 --- a/tests/Lab/IntegrationTests/Experiments/CacheVsProduction/CacheVsProductionExperimentTests.cs +++ b/tests/Lab/IntegrationTests/Experiments/CacheVsProduction/CacheVsProductionExperimentTests.cs @@ -152,7 +152,7 @@ public async Task Order_create_redis_idempotency_cache_vs_production_is_observed miss.Body.Should().Contain("\"orderId\"", "production MVC serialization is camelCase System.Text.Json"); hit.Body.Should().Contain("\"OrderId\"", - "the cached replay is Newtonsoft.Json from BuildingBlocks.Idempotency, which defaults to PascalCase"); + "the cached replay is System.Text.Json from BuildingBlocks.Idempotency, which defaults to PascalCase"); mutatedBody.HttpStatus.Should().Be(200); mutatedBody.OrderId.Should().Be(miss.OrderId); diff --git a/tests/Lab/IntegrationTests/Experiments/IdempotencyProcessingLease/IdempotencyProcessingLeaseExperimentTests.cs b/tests/Lab/IntegrationTests/Experiments/IdempotencyProcessingLease/IdempotencyProcessingLeaseExperimentTests.cs index 8db8541..bb06785 100644 --- a/tests/Lab/IntegrationTests/Experiments/IdempotencyProcessingLease/IdempotencyProcessingLeaseExperimentTests.cs +++ b/tests/Lab/IntegrationTests/Experiments/IdempotencyProcessingLease/IdempotencyProcessingLeaseExperimentTests.cs @@ -14,7 +14,6 @@ using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; -using Newtonsoft.Json; using Xunit.Abstractions; using static FeatureFusion.Features.Orders.Commands.CreateOrderCommandHandler; using static IntegrationTests.Infrastructure.Telemetry.LabTrace; @@ -269,7 +268,7 @@ private async Task WaitForProcessingLeaseExpiredAsync( if (bytes is null || bytes.Length == 0) return null; - return JsonConvert.DeserializeObject(Encoding.UTF8.GetString(bytes)); + return JsonSerializer.Deserialize(Encoding.UTF8.GetString(bytes)); } private async Task SendAsync( diff --git a/tests/Lab/IntegrationTests/Experiments/README.md b/tests/Lab/IntegrationTests/Experiments/README.md index 269c600..827a535 100644 --- a/tests/Lab/IntegrationTests/Experiments/README.md +++ b/tests/Lab/IntegrationTests/Experiments/README.md @@ -102,7 +102,7 @@ This is research infrastructure only — not Exp 15 and not a BuildingBlock. ### Workstream status — HTTP Redis idempotency (Exp 3, 4, 12) -**COMPLETE — BuildingBlocks.Idempotency 1.0.0** +**COMPLETE — BuildingBlocks.Idempotency 1.0.1** (extraction evidence from 1.0.0; packaging/STJ polish in 1.0.1) Reusable implementation: [`src/BuildingBlocks/Idempotency`](../../../../src/BuildingBlocks/Idempotency). Lab hosts the package on `POST /api/v2/Order/order`; these three experiments are the **evidence / provenance trail**, not unfinished extraction work. Package unit tests live under `tests/BuildingBlocks/Idempotency.Tests`. MCP `orders.create` idempotency (Exp 6) is a separate in-memory store and is **not** part of this BuildingBlock. @@ -123,16 +123,16 @@ Reusable implementation: [`src/BuildingBlocks/Idempotency`](../../../../src/Buil ### Experiment 3 — HTTP Redis idempotency vs production (CacheVsProduction) - **Role:** Originally characterized Lab HTTP idempotency; now **regression / provenance** that extracted `BuildingBlocks.Idempotency` preserves the intended miss/hit workflow (fingerprint **off**, Lab default). -- **Workstream:** COMPLETE — BuildingBlocks.Idempotency 1.0.0 (extraction proof set). +- **Workstream:** COMPLETE — BuildingBlocks.Idempotency 1.0.1 (extraction proof set). - **Hypothesis / problem:** Package filter (Redis `IDistributedCache`) separates cache replay from production execution on `POST /api/v2/Order/order`. - **Surface:** HTTP order create → `[Idempotent(useLock: true)]` → `CreateOrderCommandHandler` (Mediator + catalog SaveChanges + outbox insert on miss). -- **Primary behavior:** Miss runs production (Mediator + Npgsql); hit replays with `X-Idempotent-Response`; same key + different body keeps original order (body not part of key when fingerprint off); new key runs production again. Replay body casing reflects package Newtonsoft cache serialization. +- **Primary behavior:** Miss runs production (Mediator + Npgsql); hit replays with `X-Idempotent-Response`; same key + different body keeps original order (body not part of key when fingerprint off); new key runs production again. Replay body casing reflects package System.Text.Json cache serialization (PascalCase by default). - **Limitation / non-goal:** Does not cover MCP `orders.create` (Exp 6). Does not assert async handler delivery (Exp 5). Does not prove concurrency lock races (Exp 4) or opt-in fingerprinting (Exp 12). ### Experiment 4 — HTTP idempotency concurrency (ConcurrencyRace) - **Role:** Originally proved the Redis same-key concurrency problem and lock behavior; now **regression / provenance** for `RedisIdempotencyLock` + Processing/Completed coordination under concurrent clients. -- **Workstream:** COMPLETE — BuildingBlocks.Idempotency 1.0.0 (extraction proof set). +- **Workstream:** COMPLETE — BuildingBlocks.Idempotency 1.0.1 (extraction proof set). - **Hypothesis / problem:** Concurrent clients with the **same** `Idempotency-Key` coordinate through Redis lock/status; different keys run independently. - **Surface:** Same HTTP order-create path as Exp 3 (`BuildingBlocks.Idempotency`). - **Primary behavior:** Three concurrent same-key requests → exactly one production winner, one unique `orderId`; losers are cached replay, HTTP 409, or lock-failure HTTP 500 (as observed). Two different keys both run production. @@ -193,7 +193,7 @@ Reusable implementation: [`src/BuildingBlocks/Idempotency`](../../../../src/Buil ### Experiment 12 — HTTP idempotency request fingerprint (opt-in) - **Role:** Proves the **opt-in request fingerprint** capability added in richer BuildingBlocks.Idempotency v1 (not part of the original extraction proof set). Remains a **regression / proof gate** for that feature. -- **Workstream:** COMPLETE — BuildingBlocks.Idempotency 1.0.0 (richer v1 evidence). +- **Workstream:** COMPLETE — BuildingBlocks.Idempotency 1.0.1 (richer v1 evidence). - **Hypothesis / problem:** With `EnableRequestFingerprint=true`, same key + same body replays; same key + different body conflicts (422) without production. - **Surface:** Same order-create path as Exp 3; fingerprint enabled via test-host `PostConfigure` only (Lab app default stays off). - **Primary behavior:** Miss runs production; same-body replay sets `X-Idempotent-Response`; different quantity → HTTP 422, no Mediator. @@ -220,6 +220,7 @@ Reusable implementation: [`src/BuildingBlocks/Idempotency`](../../../../src/Buil ### Experiment 15 — HTTP idempotency ProcessingTtl lease overlap - **Role:** Closes the lease-expiry gap deferred by Exp 4 / package docs: what happens when the first same-key request stays in `Processing` longer than `ProcessingTtl`. +- **Workstream:** COMPLETE — BuildingBlocks.Idempotency 1.0.1 (lease-overlap characterization; unchanged in STJ packaging polish). - **Hypothesis / problem:** After the Processing lease expires, a second client with the same `Idempotency-Key` may run production while the first handler is still in flight → duplicate orders/outbox. - **Surface:** HTTP `POST /api/v2/Order/order` → `BuildingBlocks.Idempotency` (`useLock: true`) → Mediator `CreateOrderCommand` → outbox. Test host only: short `ProcessingTtl` via `PostConfigure` + test-only gated handler wrapper (production handler unchanged). - **Primary behavior (characterized by this experiment):** Probe while first held → HTTP 409; after lease expiry → second request executes production; both complete with distinct `orderId`s and outbox rows when the packaged lease tradeoff admits overlap.