diff --git a/src/Extensions/src/Eventuous.Extensions.DependencyInjection/Registrations/Stores.cs b/src/Extensions/src/Eventuous.Extensions.DependencyInjection/Registrations/Stores.cs
index 068d593bd..6194a5d99 100644
--- a/src/Extensions/src/Eventuous.Extensions.DependencyInjection/Registrations/Stores.cs
+++ b/src/Extensions/src/Eventuous.Extensions.DependencyInjection/Registrations/Stores.cs
@@ -13,88 +13,93 @@ public static partial class ServiceCollectionExtensions {
///
extension(IServiceCollection services) {
///
- /// Registers the event reader
+ /// Registers a singleton event reader, preserving existing registrations.
///
/// Implementation of
///
public IServiceCollection AddEventReader<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] T>() where T : class, IEventReader {
+ services.TryAddSingleton();
+
if (EventuousDiagnostics.Enabled) {
- services.TryAddSingleton();
services.TryAddSingleton(sp => TracedEventReader.Trace(sp.GetRequiredService()));
}
- else { services.TryAddSingleton(); }
+ else { services.TryAddSingleton(sp => sp.GetRequiredService()); }
return services;
}
///
- /// Registers the event reader
+ /// Registers a singleton event reader, preserving existing registrations.
///
/// Function to create an instance of
/// Implementation of
///
public IServiceCollection AddEventReader(Func getService) where T : class, IEventReader {
+ services.TryAddSingleton(getService);
+
if (EventuousDiagnostics.Enabled) {
- services.TryAddSingleton(getService);
services.TryAddSingleton(sp => TracedEventReader.Trace(sp.GetRequiredService()));
}
- else { services.TryAddSingleton(getService); }
+ else { services.TryAddSingleton(sp => sp.GetRequiredService()); }
return services;
}
///
- /// Registers the event writer
+ /// Registers a singleton event writer, preserving existing registrations.
///
/// Implementation of
///
public IServiceCollection AddEventWriter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] T>() where T : class, IEventWriter {
+ services.TryAddSingleton();
+
if (EventuousDiagnostics.Enabled) {
- services.TryAddSingleton();
services.TryAddSingleton(sp => TracedEventWriter.Trace(sp.GetRequiredService()));
}
- else { services.TryAddSingleton(); }
+ else { services.TryAddSingleton(sp => sp.GetRequiredService()); }
return services;
}
///
- /// Registers the event writer
+ /// Registers a singleton event writer, preserving existing registrations.
///
/// Function to create an instance of
/// Implementation of
///
public IServiceCollection AddEventWriter(Func getService) where T : class, IEventWriter {
+ services.TryAddSingleton(getService);
+
if (EventuousDiagnostics.Enabled) {
- services.TryAddSingleton(getService);
services.TryAddSingleton(sp => TracedEventWriter.Trace(sp.GetRequiredService()));
}
- else { services.TryAddSingleton(getService); }
+ else { services.TryAddSingleton(sp => sp.GetRequiredService()); }
return services;
}
///
- /// Registers the event reader and writer
+ /// Registers a shared singleton event reader and writer, preserving existing registrations.
///
/// Implementation of and
///
public IServiceCollection AddEventReaderWriter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] T>() where T : class, IEventWriter, IEventReader {
+ services.TryAddSingleton();
+
if (EventuousDiagnostics.Enabled) {
- services.TryAddSingleton();
services.TryAddSingleton(sp => TracedEventReader.Trace(sp.GetRequiredService()));
services.TryAddSingleton(sp => TracedEventWriter.Trace(sp.GetRequiredService()));
}
else {
- services.TryAddSingleton();
- services.TryAddSingleton();
+ services.TryAddSingleton(sp => sp.GetRequiredService());
+ services.TryAddSingleton(sp => sp.GetRequiredService());
}
return services;
}
///
- /// Registers the event reader and writer implemented by one class
+ /// Registers a shared singleton event reader and writer, preserving existing registrations.
///
/// Function to create an instance of the class,
/// which implements both and
@@ -102,59 +107,62 @@ public IServiceCollection AddEventWriter(Func getService
///
public IServiceCollection AddEventReaderWriter(Func getService)
where T : class, IEventWriter, IEventReader {
+ services.TryAddSingleton(getService);
+
if (EventuousDiagnostics.Enabled) {
- services.TryAddSingleton(getService);
services.TryAddSingleton(sp => TracedEventReader.Trace(sp.GetRequiredService()));
services.TryAddSingleton(sp => TracedEventWriter.Trace(sp.GetRequiredService()));
}
else {
- services.TryAddSingleton(getService);
- services.TryAddSingleton(getService);
+ services.TryAddSingleton(sp => sp.GetRequiredService());
+ services.TryAddSingleton(sp => sp.GetRequiredService());
}
return services;
}
///
- /// Registers an event store service as reader, writer, and event store
+ /// Registers one singleton as reader, writer, and event store, preserving existing registrations.
///
/// Implementation of
///
public IServiceCollection AddEventStore<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] T>() where T : class, IEventStore {
+ services.TryAddSingleton();
+
if (EventuousDiagnostics.Enabled) {
- services.TryAddSingleton();
- services.AddSingleton(sp => new TracedEventStore(sp.GetRequiredService()));
- services.AddSingleton(sp => sp.GetRequiredService());
- services.AddSingleton(sp => sp.GetRequiredService());
- services.AddSingleton(sp => sp.GetRequiredService());
+ services.TryAddSingleton(sp => new TracedEventStore(sp.GetRequiredService()));
+ services.TryAddSingleton(sp => sp.GetRequiredService());
+ services.TryAddSingleton(sp => sp.GetRequiredService());
+ services.TryAddSingleton(sp => sp.GetRequiredService());
}
else {
- services.TryAddSingleton();
- services.TryAddSingleton();
- services.TryAddSingleton();
+ services.TryAddSingleton(sp => sp.GetRequiredService());
+ services.TryAddSingleton(sp => sp.GetRequiredService());
+ services.TryAddSingleton(sp => sp.GetRequiredService());
}
return services;
}
///
- /// Registers an event store service as reader, writer, and event store
+ /// Registers one singleton as reader, writer, and event store, preserving existing registrations.
///
/// Function to create an instance of the class, which implements
/// Implementation of
///
public IServiceCollection AddEventStore(Func getService) where T : class, IEventStore {
+ services.TryAddSingleton(getService);
+
if (EventuousDiagnostics.Enabled) {
- services.TryAddSingleton(getService);
- services.AddSingleton(sp => new TracedEventStore(sp.GetRequiredService()));
- services.AddSingleton(sp => sp.GetRequiredService());
- services.AddSingleton(sp => sp.GetRequiredService());
- services.AddSingleton(sp => sp.GetRequiredService());
+ services.TryAddSingleton(sp => new TracedEventStore(sp.GetRequiredService()));
+ services.TryAddSingleton(sp => sp.GetRequiredService());
+ services.TryAddSingleton(sp => sp.GetRequiredService());
+ services.TryAddSingleton(sp => sp.GetRequiredService());
}
else {
- services.TryAddSingleton(getService);
- services.TryAddSingleton(getService);
- services.TryAddSingleton(getService);
+ services.TryAddSingleton(sp => sp.GetRequiredService());
+ services.TryAddSingleton(sp => sp.GetRequiredService());
+ services.TryAddSingleton(sp => sp.GetRequiredService());
}
return services;
diff --git a/src/Extensions/test/Eventuous.Tests.DependencyInjection/Eventuous.Tests.DependencyInjection.csproj b/src/Extensions/test/Eventuous.Tests.DependencyInjection/Eventuous.Tests.DependencyInjection.csproj
index 252054daa..611de3d8c 100644
--- a/src/Extensions/test/Eventuous.Tests.DependencyInjection/Eventuous.Tests.DependencyInjection.csproj
+++ b/src/Extensions/test/Eventuous.Tests.DependencyInjection/Eventuous.Tests.DependencyInjection.csproj
@@ -6,6 +6,7 @@
+
diff --git a/src/Extensions/test/Eventuous.Tests.DependencyInjection/StoreRegistrationTests.cs b/src/Extensions/test/Eventuous.Tests.DependencyInjection/StoreRegistrationTests.cs
new file mode 100644
index 000000000..4000e58e3
--- /dev/null
+++ b/src/Extensions/test/Eventuous.Tests.DependencyInjection/StoreRegistrationTests.cs
@@ -0,0 +1,180 @@
+using Eventuous.Diagnostics;
+using Eventuous.Testing;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Shouldly;
+
+namespace Eventuous.Tests.DependencyInjection;
+
+// Diagnostics is process-wide. No other tests may observe the temporary setting.
+[NotInParallel]
+public class StoreRegistrationTests {
+ [Test]
+ [Arguments(false, false, Registration.Store)]
+ [Arguments(false, true, Registration.Store)]
+ [Arguments(true, false, Registration.Store)]
+ [Arguments(true, true, Registration.Store)]
+ [Arguments(false, false, Registration.ReaderWriter)]
+ [Arguments(false, true, Registration.ReaderWriter)]
+ [Arguments(true, false, Registration.ReaderWriter)]
+ [Arguments(true, true, Registration.ReaderWriter)]
+ [Arguments(false, false, Registration.Separate)]
+ [Arguments(false, true, Registration.Separate)]
+ [Arguments(true, false, Registration.Separate)]
+ [Arguments(true, true, Registration.Separate)]
+ public async Task WrittenEventsAreVisibleThroughEveryRegisteredInterface(bool tracing, bool factory, Registration registration) {
+ using var diagnostics = new DiagnosticsSetting(tracing);
+ var factoryCalls = 0;
+ using var host = new HostBuilder().ConfigureServices(services => {
+ Register(services, registration, factory, _ => { factoryCalls++; return new(); });
+ }).Build();
+ await host.StartAsync();
+
+ var writer = host.Services.GetRequiredService();
+ var reader = host.Services.GetRequiredService();
+ var stream = new StreamName("shared-store");
+ await Append(writer, stream, "first");
+ (await Read(reader, stream)).ShouldBe(new[] { "first" });
+ if (registration == Registration.Store) {
+ var store = host.Services.GetRequiredService();
+ (await store.StreamExists(stream)).ShouldBeTrue();
+ await Append(store, stream, "second", new(0));
+ (await Read(reader, stream)).ShouldBe(new[] { "first", "second" });
+ }
+
+ using (var scope = host.Services.CreateScope()) {
+ scope.ServiceProvider.GetRequiredService().ShouldBeSameAs(reader);
+ scope.ServiceProvider.GetRequiredService().ShouldBeSameAs(writer);
+ }
+ if (factory) factoryCalls.ShouldBe(1);
+ await host.StopAsync();
+ }
+
+ [Test]
+ [Arguments(false, false, true)]
+ [Arguments(false, true, true)]
+ [Arguments(true, false, true)]
+ [Arguments(true, true, true)]
+ [Arguments(false, false, false)]
+ [Arguments(false, true, false)]
+ [Arguments(true, false, false)]
+ [Arguments(true, true, false)]
+ public async Task StandaloneRegistrationUsesTheExistingBackingStore(bool tracing, bool factory, bool readerOnly) {
+ using var diagnostics = new DiagnosticsSetting(tracing);
+ var backing = new InMemoryEventStore();
+ var stream = new StreamName("standalone-store");
+ var factoryCalls = 0;
+ using var host = new HostBuilder().ConfigureServices(services => {
+ services.AddSingleton(backing);
+ InMemoryEventStore Create(IServiceProvider _) { factoryCalls++; return new(); }
+ if (readerOnly) {
+ if (factory) services.AddEventReader(Create);
+ else services.AddEventReader();
+ }
+ else {
+ if (factory) services.AddEventWriter(Create);
+ else services.AddEventWriter();
+ }
+ }).Build();
+ await host.StartAsync();
+
+ if (readerOnly) {
+ await Append(backing, stream, "seeded");
+ (await Read(host.Services.GetRequiredService(), stream)).ShouldBe(new[] { "seeded" });
+ }
+ else {
+ await Append(host.Services.GetRequiredService(), stream, "written");
+ (await Read(backing, stream)).ShouldBe(new[] { "written" });
+ }
+ factoryCalls.ShouldBe(0);
+ await host.StopAsync();
+ }
+
+ [Test]
+ [Arguments(false, false)]
+ [Arguments(false, true)]
+ [Arguments(true, false)]
+ [Arguments(true, true)]
+ public async Task RepeatedRegistrationKeepsOneServiceAndOneFactory(bool tracing, bool factory) {
+ using var diagnostics = new DiagnosticsSetting(tracing);
+ var firstCalls = 0;
+ var secondCalls = 0;
+ using var host = new HostBuilder().ConfigureServices(services => {
+ Register(services, Registration.Store, factory, _ => { firstCalls++; return new(); });
+ Register(services, Registration.Store, factory, _ => { secondCalls++; return new(); });
+ }).Build();
+ await host.StartAsync();
+
+ var writer = host.Services.GetServices().ShouldHaveSingleItem();
+ var reader = host.Services.GetServices().ShouldHaveSingleItem();
+ var store = host.Services.GetServices().ShouldHaveSingleItem();
+ var stream = new StreamName("repeated-store");
+ await Append(writer, stream, "once");
+ (await Read(reader, stream)).ShouldBe(new[] { "once" });
+ (await store.StreamExists(stream)).ShouldBeTrue();
+ firstCalls.ShouldBe(factory ? 1 : 0);
+ secondCalls.ShouldBe(0);
+ await host.StopAsync();
+ }
+
+ [Test]
+ [Arguments(false, false)]
+ [Arguments(false, true)]
+ [Arguments(true, false)]
+ [Arguments(true, true)]
+ public async Task RegistrationPreservesExplicitInterfaceOverrides(bool tracing, bool factory) {
+ using var diagnostics = new DiagnosticsSetting(tracing);
+ var existing = new InMemoryEventStore();
+ var factoryCalls = 0;
+ using var host = new HostBuilder().ConfigureServices(services => {
+ services.AddSingleton(existing);
+ services.AddSingleton(existing);
+ services.AddSingleton(existing);
+ Register(services, Registration.Store, factory, _ => { factoryCalls++; return new(); });
+ }).Build();
+ await host.StartAsync();
+
+ var stream = new StreamName("explicit-store");
+ await Append(host.Services.GetRequiredService(), stream, "preserved");
+ (await Read(existing, stream)).ShouldBe(new[] { "preserved" });
+ host.Services.GetRequiredService().ShouldBeSameAs(existing);
+ host.Services.GetRequiredService().ShouldBeSameAs(existing);
+ factoryCalls.ShouldBe(0);
+ await host.StopAsync();
+ }
+
+ static void Register(IServiceCollection services, Registration registration, bool factory, Func create) {
+ switch (registration) {
+ case Registration.Store:
+ if (factory) services.AddEventStore(create);
+ else services.AddEventStore();
+ break;
+ case Registration.ReaderWriter:
+ if (factory) services.AddEventReaderWriter(create);
+ else services.AddEventReaderWriter();
+ break;
+ case Registration.Separate:
+ if (factory) services.AddEventReader(create).AddEventWriter(create);
+ else services.AddEventReader().AddEventWriter();
+ break;
+ }
+ }
+
+ static Task Append(IEventWriter writer, StreamName stream, string value, ExpectedStreamVersion? version = null)
+ => writer.AppendEvents(stream, version ?? ExpectedStreamVersion.NoStream, new[] { new NewStreamEvent(Guid.NewGuid(), value, new()) }, default);
+
+ static async Task Read(IEventReader reader, StreamName stream)
+ => (await reader.ReadEvents(stream, StreamReadPosition.Start, 10, true, default)).Select(x => (string)x.Payload!).ToArray();
+
+ public enum Registration { Store, ReaderWriter, Separate }
+
+ sealed class DiagnosticsSetting : IDisposable {
+ readonly bool _previous = EventuousDiagnostics.Enabled;
+ public DiagnosticsSetting(bool enabled) => Set(enabled);
+ public void Dispose() => Set(_previous);
+ static void Set(bool enabled) {
+ if (enabled) EventuousDiagnostics.Enable();
+ else EventuousDiagnostics.Disable();
+ }
+ }
+}