Skip to content

feat: EF Core IIdempotencyStore implementation#374

Merged
samtrion merged 6 commits intomainfrom
copilot/add-ef-core-idempotency-store
Apr 14, 2026
Merged

feat: EF Core IIdempotencyStore implementation#374
samtrion merged 6 commits intomainfrom
copilot/add-ef-core-idempotency-store

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 14, 2026

IIdempotencyStore had no concrete implementation, making the idempotency interceptor a no-op for applications already using EF Core for persistence.

Changes

NetEvolve.Pulse.Extensibility

  • IdempotencyKeySchema — canonical schema constants (DefaultSchema, DefaultTableName, column names, max lengths) matching all provider configurations
  • IIdempotencyKeyRepository — storage abstraction interface (ExistsAsync(key, validFrom?, ct) and StoreAsync(key, createdAt, ct)); mirrors IOutboxRepository in the outbox pattern

NetEvolve.Pulse

  • IdempotencyKeyOptionsSchema, TableName, optional TimeToLive (logical expiry; no background cleanup); lives alongside OutboxOptions
  • IdempotencyStore — central IIdempotencyStore implementation that delegates to IIdempotencyKeyRepository and applies TTL cutoff logic; mirrors OutboxEventStore
  • AddIdempotency() — now also registers IdempotencyStore as the IIdempotencyStore implementation (Scoped, via TryAddScoped)

NetEvolve.Pulse.EntityFramework

  • IdempotencyKey — POCO entity (Key string PK, CreatedAt DateTimeOffset)
  • IIdempotencyStoreDbContext — interface exposing DbSet<IdempotencyKey> IdempotencyKeys; implement on your DbContext alongside IOutboxDbContext
  • IdempotencyKeyConfigurationBase + provider configs (SqlServer, PostgreSql, Sqlite, InMemory, MySql) — follow same pattern as outbox message configurations; MySQL uses bigint UTC-ticks for DateTimeOffset (Oracle MySQL provider lacks a native datetimeoffset mapping)
  • EntityFrameworkIdempotencyKeyRepository<TContext>IIdempotencyKeyRepository impl; ExistsAsync applies the validFrom TTL cutoff passed by the central store; StoreAsync handles concurrent duplicate-key violations gracefully (local tracker check + database-level constraint catch via IsDuplicateKeyException)
    • IsDuplicateKeyException walks the full exception chain and uses correct provider-specific message patterns: SQL Server ("Cannot insert duplicate key", "Violation of PRIMARY KEY constraint"), PostgreSQL ("23505"), SQLite ("UNIQUE constraint failed"), MySQL ("Duplicate entry"), InMemory (ArgumentException with "An item with the same key has already been added")
  • AddEntityFrameworkIdempotencyStore<TContext>() — calls AddIdempotency() internally, then registers EntityFrameworkIdempotencyKeyRepository<TContext> as IIdempotencyKeyRepository (Scoped)
  • ModelBuilderExtensions.ApplyPulseConfiguration — now also applies idempotency key configuration when the context implements IIdempotencyStoreDbContext

Integration Tests

Mirrors the outbox integration test structure across all five supported providers:

  • EntityFrameworkIdempotencyInitializer — test initializer using an in-test TestIdempotencyDbContext implementing IIdempotencyStoreDbContext
  • IdempotencyTestsBase — abstract base with 8 test scenarios: key not found, key found, different key isolation, multiple keys, idempotent duplicate store (same scope), cross-scope duplicate insert (fresh DbContext, exercises the database-level IsDuplicateKeyException path), TTL within window, TTL expired
  • Provider-specific test classes: InMemoryEntityFrameworkIdempotencyTests, SQLiteEntityFrameworkIdempotencyTests, SqlServerEntityFrameworkIdempotencyTests, PostgreSqlEntityFrameworkIdempotencyTests, MySqlEntityFrameworkIdempotencyTests

Usage

// DbContext
public class AppDbContext : DbContext, IIdempotencyStoreDbContext
{
    public DbSet<IdempotencyKey> IdempotencyKeys => Set<IdempotencyKey>();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
        modelBuilder.ApplyPulseConfiguration(this); // picks provider config automatically
    }
}

// Registration
services.AddDbContext<AppDbContext>(o => o.UseSqlServer(connectionString));
services.AddPulse(c => c
    .AddEntityFrameworkIdempotencyStore<AppDbContext>(o => o.TimeToLive = TimeSpan.FromHours(24))
);

After registering, generate a migration (dotnet ef migrations add AddIdempotencyKeys) to create the pulse.IdempotencyKey table.

Copilot AI linked an issue Apr 14, 2026 that may be closed by this pull request
6 tasks
Copilot AI changed the title [WIP] Add EF Core implementation for IIdempotencyStore feat: EF Core IIdempotencyStore implementation Apr 14, 2026
Copilot AI requested a review from samtrion April 14, 2026 08:24
/// expired keys are logically ignored.
/// </remarks>
/// <typeparam name="TContext">The DbContext type that implements <see cref="IIdempotencyStoreDbContext"/>.</typeparam>
internal sealed class EntityFrameworkIdempotencyStore<TContext> : IIdempotencyStore
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot I think we shoul implement a central implementation, lile OutboxEventStore.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in b95306a. The implementation now mirrors the OutboxEventStore pattern exactly:

  • IIdempotencyKeyRepository (Extensibility) — storage abstraction
  • IdempotencyStore (central, in NetEvolve.Pulse) — implements IIdempotencyStore, delegates to IIdempotencyKeyRepository (handles TTL cutoff logic)
  • EntityFrameworkIdempotencyKeyRepository<TContext> (EF Core pkg) — implements IIdempotencyKeyRepository
  • IdempotencyKeyOptions moved to NetEvolve.Pulse (like OutboxOptions)
  • AddIdempotency() now also registers IdempotencyStore via TryAddScoped
  • AddEntityFrameworkIdempotencyStore<TContext>() internally calls AddIdempotency() then registers IIdempotencyKeyRepository

@samtrion samtrion marked this pull request as ready for review April 14, 2026 10:42
@samtrion samtrion requested a review from a team as a code owner April 14, 2026 10:42
@samtrion samtrion requested a review from Hnogared April 14, 2026 10:42
Copilot AI requested a review from samtrion April 14, 2026 10:43
Copilot AI and others added 3 commits April 14, 2026 12:44
- Add IdempotencyKeySchema constants in NetEvolve.Pulse.Extensibility
- Add IdempotencyKey POCO entity in NetEvolve.Pulse.EntityFramework
- Add IIdempotencyStoreDbContext interface
- Add IdempotencyKeyOptions (Schema, TableName, TimeToLive?)
- Add abstract IdempotencyKeyConfigurationBase
- Add provider-specific configs: SqlServer, PostgreSql, Sqlite, InMemory
- Implement EntityFrameworkIdempotencyStore<TContext>
- Add AddEntityFrameworkIdempotencyStore<TContext> extension method
- Update ModelBuilderExtensions to apply idempotency config
- Add unit tests for extensions and store

Agent-Logs-Url: https://github.com/dailydevops/pulse/sessions/5dc3605c-3090-411c-8e88-75cca388557a

Co-authored-by: samtrion <3283596+samtrion@users.noreply.github.com>
…kIdempotencyStore

Agent-Logs-Url: https://github.com/dailydevops/pulse/sessions/5dc3605c-3090-411c-8e88-75cca388557a

Co-authored-by: samtrion <3283596+samtrion@users.noreply.github.com>
…entStore pattern)

- Add IIdempotencyKeyRepository interface in Extensibility (storage abstraction)
- Move IdempotencyKeyOptions to NetEvolve.Pulse (like OutboxOptions)
- Add central IdempotencyStore in NetEvolve.Pulse (implements IIdempotencyStore via IIdempotencyKeyRepository)
- Rename EntityFrameworkIdempotencyStore → EntityFrameworkIdempotencyKeyRepository (implements IIdempotencyKeyRepository)
- Update AddIdempotency() to also register IdempotencyStore (via TryAddScoped)
- Update AddEntityFrameworkIdempotencyStore() to register IIdempotencyKeyRepository instead of IIdempotencyStore
- Add IdempotencyStoreTests for central store (TTL cutoff, timestamp propagation)
- Rename EntityFrameworkIdempotencyStoreTests → EntityFrameworkIdempotencyKeyRepositoryTests

Agent-Logs-Url: https://github.com/dailydevops/pulse/sessions/9054478d-e37f-4fc1-b30d-88d52bce7f6f

Co-authored-by: samtrion <3283596+samtrion@users.noreply.github.com>
@samtrion samtrion force-pushed the copilot/add-ef-core-idempotency-store branch from b95306a to 4ab5f12 Compare April 14, 2026 10:44
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 14, 2026

Codecov Report

❌ Patch coverage is 94.52055% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.30%. Comparing base (8825ddc) to head (56d79d8).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
...ve.Pulse.EntityFramework/ModelBuilderExtensions.cs 90.00% 1 Missing and 1 partial ⚠️
...ork/Idempotency/IdempotencyKeyConfigurationBase.cs 95.00% 0 Missing and 1 partial ⚠️
...Idempotency/InMemoryIdempotencyKeyConfiguration.cs 50.00% 1 Missing ⚠️
...rk/Idempotency/MySqlIdempotencyKeyConfiguration.cs 87.50% 1 Missing ⚠️
...empotency/PostgreSqlIdempotencyKeyConfiguration.cs 80.00% 1 Missing ⚠️
...dempotency/SqlServerIdempotencyKeyConfiguration.cs 80.00% 1 Missing ⚠️
...k/Idempotency/SqliteIdempotencyKeyConfiguration.cs 87.50% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #374      +/-   ##
==========================================
+ Coverage   91.16%   91.30%   +0.14%     
==========================================
  Files         106      117      +11     
  Lines        3916     4061     +145     
  Branches      357      368      +11     
==========================================
+ Hits         3570     3708     +138     
- Misses        244      249       +5     
- Partials      102      104       +2     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@samtrion
Copy link
Copy Markdown
Contributor

@copilot Implement integration tests based on the outbox tests

Copilot AI and others added 2 commits April 14, 2026 11:27
…lServer, PostgreSql, MySql)

- Add EntityFrameworkIdempotencyInitializer (mirrors EntityFrameworkInitializer for outbox)
- Add IdempotencyTestsBase with 7 test cases: Exists/Store/TTL/Duplicate scenarios
- Add per-provider concrete test classes: InMemory, SQLite, SqlServer, PostgreSql, MySql
- Add MySqlIdempotencyKeyConfiguration (bigint UTC-ticks for DateTimeOffset, varchar for Key)
- Fix ModelBuilderExtensions: use MySqlIdempotencyKeyConfiguration for MySQL (was using SqlServer)
- All 267 integration tests pass (net9.0, InMemory + container providers)
- All 2238 unit tests pass

Agent-Logs-Url: https://github.com/dailydevops/pulse/sessions/f72fbb61-f6f4-418e-a7a6-6e10ffbd56ee

Co-authored-by: samtrion <3283596+samtrion@users.noreply.github.com>
… DateTimeOffset conversion

Agent-Logs-Url: https://github.com/dailydevops/pulse/sessions/f72fbb61-f6f4-418e-a7a6-6e10ffbd56ee

Co-authored-by: samtrion <3283596+samtrion@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 14, 2026

@copilot Implement integration tests based on the outbox tests

Done in eaaf790. The integration tests mirror the outbox test structure exactly:

  • EntityFrameworkIdempotencyInitializer — mirrors EntityFrameworkInitializer; configures TestIdempotencyDbContext implementing IIdempotencyStoreDbContext
  • IdempotencyTestsBase — 7 test scenarios: key not found, key found, different key, multiple keys, duplicate store (idempotent), TTL within window, TTL expired
  • Per-provider concrete classes: InMemoryEntityFrameworkIdempotencyTests, SQLiteEntityFrameworkIdempotencyTests, SqlServerEntityFrameworkIdempotencyTests, PostgreSqlEntityFrameworkIdempotencyTests, MySqlEntityFrameworkIdempotencyTests

Also added MySqlIdempotencyKeyConfiguration (was missing — MySQL needs bigint UTC-ticks for DateTimeOffset, not datetimeoffset) and fixed ModelBuilderExtensions to use it.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • 1.0.17.172.in-addr.arpa
    • Triggering command: /usr/sbin/mysqld mysqld 172.17.0.5 --dport 3306 ! -i docker0 -j DROP (dns block)
    • Triggering command: /usr/sbin/mysqld mysqld (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

@samtrion
Copy link
Copy Markdown
Contributor

@copilot You missed integration tests with duplicated keys. See method IsDuplicateKeyException

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 14, 2026

Just as a heads up, I was blocked by some firewall rules while working on your feedback. Expand below for details.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • 1.0.17.172.in-addr.arpa
    • Triggering command: /usr/sbin/mysqld mysqld (dns block)
    • Triggering command: /usr/sbin/mysqld mysqld exec --process /tmp/runc-process2886189869 --detach --pid-file /run/containerd/io.containerd.runtime.v2.task/moby/60ba7d18c8f96c7d320967ceeaaa79c53b729c7bf3e0a9e95d7582aa0fbb12fe/fc5d5b9d6d32d4287ddbabf0555f474d8eb1d1284295684dede27d012c57bc1b.pid 60ba7d18c8f96c7d320967ceeaaa79c53b729c7bf3e0a9e95d7582aa0fbb12fe rpierignore&#39; &#39;/hgrep (dns block)
    • Triggering command: /usr/sbin/mysqld mysqld exec --process /tmp/runc-process584785863 --detach --pid-file /run/containerd/io.containerd.runtime.v2.task/moby/e89fe43afc84065cff790991724401da8e539a14f5d762ea8a3b367e7c62b584/b465398822194793e35b34b80b049b5177754d21b680661711733fc0f42fd54d.pid e89fe43afc84065cff790991724401da8e539a14f5d762ea8a3b367e7c62b584 /../configuratiogosu (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

@samtrion samtrion merged commit 2a4a927 into main Apr 14, 2026
11 checks passed
@samtrion samtrion deleted the copilot/add-ef-core-idempotency-store branch April 14, 2026 13:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: IIdempotencyStore – EF Core implementation

2 participants