Skip to content

fix(di): share store singletons and preserve registrations - #622

Open
nmummau wants to merge 1 commit into
Eventuous:devfrom
nmummau:fix/issue-608-store-registrations
Open

nmummau wants to merge 1 commit into
Eventuous:devfrom
nmummau:fix/issue-608-store-registrations

Conversation

@nmummau

@nmummau nmummau commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Map reader, writer, and store interfaces to the same registered singleton regardless of tracing. Preserve existing services and avoid duplicate traced store registrations.

Host-based regressions reproduced 16 failures before the fix. All 31 DI tests now pass on net10.0; Stores.cs line coverage is 78/78.

Fixes #608
Fixes #610
Fixes #611

Map reader, writer, and store interfaces to the same registered singleton regardless of tracing. Preserve existing services and avoid duplicate traced store registrations.

Host-based regressions reproduced 16 failures before the fix. All 31 DI tests now pass on net10.0; Stores.cs line coverage is 78/78.

Fixes Eventuous#608
@github-actions

Copy link
Copy Markdown

Test Results

   48 files  ± 0     48 suites  ±0   14m 7s ⏱️ - 1m 32s
  645 tests +28    645 ✅ +28  0 💤 ±0  0 ❌ ±0 
1 318 runs  +84  1 318 ✅ +84  0 💤 ±0  0 ❌ ±0 

Results for commit f3fe99e. ± Comparison against base commit 60a7643.

@nmummau

nmummau commented Oct 20, 2026

Copy link
Copy Markdown
Contributor Author

@alexeyzimarev would you please review?

@nmummau

nmummau commented Oct 20, 2026

Copy link
Copy Markdown
Contributor Author

/review

@qodo-free-for-open-source-projects

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (3) 📘 Rule violations (0) 📎 Requirement gaps (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Disposable stores can be disposed repeatedly 🐞 Bug ☼ Reliability
Description
When tracing is disabled, AddEventStore<T> registers T and three singleton interface factories
that each return that same instance. If a custom store implements IDisposable or
IAsyncDisposable and the interfaces are resolved, the container tracks the factory results
separately, so host shutdown can dispose the store multiple times; the aliases can also dispose an
externally supplied instance.
Code

src/Extensions/src/Eventuous.Extensions.DependencyInjection/Registrations/Stores.cs[R139-141]

+                services.TryAddSingleton<IEventReader>(sp => sp.GetRequiredService<T>());
+                services.TryAddSingleton<IEventWriter>(sp => sp.GetRequiredService<T>());
+                services.TryAddSingleton<IEventStore>(sp => sp.GetRequiredService<T>());
Evidence
The new registrations return the concrete T from each interface factory; the API accepts arbitrary
store implementations, and the new tests show that a concrete instance may already have been
supplied by the caller.

src/Extensions/src/Eventuous.Extensions.DependencyInjection/Registrations/Stores.cs[129-141]
src/Extensions/src/Eventuous.Extensions.DependencyInjection/Registrations/Stores.cs[153-165]
src/Extensions/test/Eventuous.Tests.DependencyInjection/StoreRegistrationTests.cs[62-78]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Factory-based interface aliases can make the container track one disposable store several times, including a store supplied as an externally owned instance.
## Fix Focus Areas
- src/Extensions/src/Eventuous.Extensions.DependencyInjection/Registrations/Stores.cs[129-141]
- src/Extensions/src/Eventuous.Extensions.DependencyInjection/Registrations/Stores.cs[153-165]
## Recommended Fix
Revise the alias and ownership strategy so resolving every interface shares the backing store without registering multiple disposable factory results for that object. Cover both container-created disposable stores and pre-registered instances with shutdown tests.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Scoped stores can break singleton resolution 🐞 Bug ☼ Reliability
Description
TryAddSingleton<T>() leaves an existing scoped registration of T in place, but the new singleton
interface factories resolve that scoped T. When a host with scope validation resolves an
interface, resolution throws; without validation, the singleton can retain a scoped instance beyond
its intended lifetime.
Code

src/Extensions/src/Eventuous.Extensions.DependencyInjection/Registrations/Stores.cs[R139-141]

+                services.TryAddSingleton<IEventReader>(sp => sp.GetRequiredService<T>());
+                services.TryAddSingleton<IEventWriter>(sp => sp.GetRequiredService<T>());
+                services.TryAddSingleton<IEventStore>(sp => sp.GetRequiredService<T>());
Evidence
The conditional concrete registration does not replace an existing T, while the newly added
singleton aliases require it. The same pattern is present in the factory overload and the standalone
reader/writer overloads.

src/Extensions/src/Eventuous.Extensions.DependencyInjection/Registrations/Stores.cs[129-141]
src/Extensions/src/Eventuous.Extensions.DependencyInjection/Registrations/Stores.cs[153-165]
src/Extensions/src/Eventuous.Extensions.DependencyInjection/Registrations/Stores.cs[20-26]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A pre-registered scoped concrete store can become a dependency of the newly added singleton interface aliases.
## Fix Focus Areas
- src/Extensions/src/Eventuous.Extensions.DependencyInjection/Registrations/Stores.cs[129-141]
- src/Extensions/src/Eventuous.Extensions.DependencyInjection/Registrations/Stores.cs[153-165]
## Recommended Fix
Before adding singleton aliases to an existing concrete registration, detect a non-singleton lifetime and reject it with a clear registration-time error, or choose an explicit lifetime policy that does not capture scoped services. Add a scope-validation regression test.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Unused store types can prevent host builds 🐞 Bug ≡ Correctness
Description
The generic AddEventStore<T> overload now registers T even when existing reader, writer, and
store registrations cause every conditional interface registration to be skipped. With tracing
disabled and build-time service validation enabled, an unused T whose constructor has an
unresolved dependency can therefore fail host construction, although the existing interfaces need no
replacement.
Code

src/Extensions/src/Eventuous.Extensions.DependencyInjection/Registrations/Stores.cs[130]

+            services.TryAddSingleton<T>();
Evidence
The unconditional new concrete registration precedes conditional interface registrations. The added
override test demonstrates the all-interfaces-already-registered case, but uses a constructable
replacement and does not exercise build-time validation.

src/Extensions/src/Eventuous.Extensions.DependencyInjection/Registrations/Stores.cs[129-141]
src/Extensions/test/Eventuous.Tests.DependencyInjection/StoreRegistrationTests.cs[125-143]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
An existing registration for every store interface leaves the new concrete registration unused, but build-time validation can still reject its constructor.
## Fix Focus Areas
- src/Extensions/src/Eventuous.Extensions.DependencyInjection/Registrations/Stores.cs[129-141]
## Recommended Fix
When no interface or wrapper will need the concrete store, avoid adding its descriptor. Add a regression test with all three interfaces pre-registered, an unconstructable replacement type, tracing disabled, and build-time validation enabled.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
❌ Tickets: failed to load — Ticket collection could not establish complete evidence
Review mode: Auto: ⚖️ Balanced: DI singleton and tracing behavior affect multiple public registration paths.

Grey Divider

Tip of the day
💡 Did you know, you can open the rule or relationship behind a finding in one click

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment on lines +139 to +141
services.TryAddSingleton<IEventReader>(sp => sp.GetRequiredService<T>());
services.TryAddSingleton<IEventWriter>(sp => sp.GetRequiredService<T>());
services.TryAddSingleton<IEventStore>(sp => sp.GetRequiredService<T>());

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.

Remediation recommended

1. Disposable stores can be disposed repeatedly 🐞 Bug ☼ Reliability

When tracing is disabled, AddEventStore<T> registers T and three singleton interface factories
that each return that same instance. If a custom store implements IDisposable or
IAsyncDisposable and the interfaces are resolved, the container tracks the factory results
separately, so host shutdown can dispose the store multiple times; the aliases can also dispose an
externally supplied instance.
Agent Prompt
## Issue description
Factory-based interface aliases can make the container track one disposable store several times, including a store supplied as an externally owned instance.
## Fix Focus Areas
- src/Extensions/src/Eventuous.Extensions.DependencyInjection/Registrations/Stores.cs[129-141]
- src/Extensions/src/Eventuous.Extensions.DependencyInjection/Registrations/Stores.cs[153-165]
## Recommended Fix
Revise the alias and ownership strategy so resolving every interface shares the backing store without registering multiple disposable factory results for that object. Cover both container-created disposable stores and pre-registered instances with shutdown tests.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +139 to +141
services.TryAddSingleton<IEventReader>(sp => sp.GetRequiredService<T>());
services.TryAddSingleton<IEventWriter>(sp => sp.GetRequiredService<T>());
services.TryAddSingleton<IEventStore>(sp => sp.GetRequiredService<T>());

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.

Remediation recommended

2. Scoped stores can break singleton resolution 🐞 Bug ☼ Reliability

TryAddSingleton<T>() leaves an existing scoped registration of T in place, but the new singleton
interface factories resolve that scoped T. When a host with scope validation resolves an
interface, resolution throws; without validation, the singleton can retain a scoped instance beyond
its intended lifetime.
Agent Prompt
## Issue description
A pre-registered scoped concrete store can become a dependency of the newly added singleton interface aliases.
## Fix Focus Areas
- src/Extensions/src/Eventuous.Extensions.DependencyInjection/Registrations/Stores.cs[129-141]
- src/Extensions/src/Eventuous.Extensions.DependencyInjection/Registrations/Stores.cs[153-165]
## Recommended Fix
Before adding singleton aliases to an existing concrete registration, detect a non-singleton lifetime and reject it with a clear registration-time error, or choose an explicit lifetime policy that does not capture scoped services. Add a scope-validation regression test.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

/// <typeparam name="T">Implementation of <see cref="IEventStore"/> </typeparam>
/// <returns></returns>
public IServiceCollection AddEventStore<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] T>() where T : class, IEventStore {
services.TryAddSingleton<T>();

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.

Remediation recommended

3. Unused store types can prevent host builds 🐞 Bug ≡ Correctness

The generic AddEventStore<T> overload now registers T even when existing reader, writer, and
store registrations cause every conditional interface registration to be skipped. With tracing
disabled and build-time service validation enabled, an unused T whose constructor has an
unresolved dependency can therefore fail host construction, although the existing interfaces need no
replacement.
Agent Prompt
## Issue description
An existing registration for every store interface leaves the new concrete registration unused, but build-time validation can still reject its constructor.
## Fix Focus Areas
- src/Extensions/src/Eventuous.Extensions.DependencyInjection/Registrations/Stores.cs[129-141]
## Recommended Fix
When no interface or wrapper will need the concrete store, avoid adding its descriptor. Add a regression test with all three interfaces pre-registered, an unconstructable replacement type, tracing disabled, and build-time validation enabled.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant