diff --git a/src/NServiceBus.AcceptanceTesting/AcceptanceTestingPersistence/AcceptanceTestingPersistence.cs b/src/NServiceBus.AcceptanceTesting/AcceptanceTestingPersistence/AcceptanceTestingPersistence.cs index b4db385548..631b063b1c 100644 --- a/src/NServiceBus.AcceptanceTesting/AcceptanceTestingPersistence/AcceptanceTestingPersistence.cs +++ b/src/NServiceBus.AcceptanceTesting/AcceptanceTestingPersistence/AcceptanceTestingPersistence.cs @@ -1,15 +1,16 @@ namespace NServiceBus; -using Features; using AcceptanceTesting; using Persistence; -public class AcceptanceTestingPersistence : PersistenceDefinition +public class AcceptanceTestingPersistence : PersistenceDefinition, IPersistenceDefinitionFactory { - internal AcceptanceTestingPersistence() + AcceptanceTestingPersistence() { - Supports(s => s.EnableFeatureByDefault()); - Supports(s => s.EnableFeatureByDefault()); - Supports(s => s.EnableFeatureByDefault()); + Supports(); + Supports(); + Supports(); } + + static AcceptanceTestingPersistence IPersistenceDefinitionFactory.Create() => new(); } \ No newline at end of file diff --git a/src/NServiceBus.AcceptanceTests/Core/Outbox/When_outbox_enabled_without_persister_supporting_it.cs b/src/NServiceBus.AcceptanceTests/Core/Outbox/When_outbox_enabled_without_persister_supporting_it.cs index a8dadf92f5..e9b57f39c3 100644 --- a/src/NServiceBus.AcceptanceTests/Core/Outbox/When_outbox_enabled_without_persister_supporting_it.cs +++ b/src/NServiceBus.AcceptanceTests/Core/Outbox/When_outbox_enabled_without_persister_supporting_it.cs @@ -19,24 +19,21 @@ public void Should_fail_to_start() Assert.That(exception.Message, Does.Contain("The selected persistence doesn't have support for outbox storage")); } - public class Context : ScenarioContext - { - } + public class Context : ScenarioContext; public class Endpoint : EndpointConfigurationBuilder { - public Endpoint() - { + public Endpoint() => EndpointSetup(c => { c.EnableOutbox(); c.ConfigureTransport().TransportTransactionMode = TransportTransactionMode.ReceiveOnly; c.UsePersistence(); }); - } } - class FakeNoOutboxSupportPersistence : PersistenceDefinition + class FakeNoOutboxSupportPersistence : PersistenceDefinition, IPersistenceDefinitionFactory { + public static FakeNoOutboxSupportPersistence Create() => new(); } } \ No newline at end of file diff --git a/src/NServiceBus.AcceptanceTests/Core/Persistence/When_a_persistence_does_not_provide_synchronized_storage_session.cs b/src/NServiceBus.AcceptanceTests/Core/Persistence/When_a_persistence_does_not_provide_synchronized_storage_session.cs index 66dd3934ec..fc57931e4a 100644 --- a/src/NServiceBus.AcceptanceTests/Core/Persistence/When_a_persistence_does_not_provide_synchronized_storage_session.cs +++ b/src/NServiceBus.AcceptanceTests/Core/Persistence/When_a_persistence_does_not_provide_synchronized_storage_session.cs @@ -6,6 +6,7 @@ using AcceptanceTesting; using EndpointTemplates; using Extensibility; +using Features; using Microsoft.Extensions.DependencyInjection; using NServiceBus.Persistence; using NUnit.Framework; @@ -18,69 +19,58 @@ public class When_a_persistence_does_not_provide_synchronized_storage_session : // is not altered by Fody to throw an ObjectDisposedException if it was disposed [Test] [Repeat(2)] - public async Task ReceiveFeature_should_work_without_ISynchronizedStorage() - { + public async Task ReceiveFeature_should_work_without_ISynchronizedStorage() => await Scenario.Define() .WithEndpoint(e => e.When(b => b.SendLocal(new MyMessage()))) .Done(c => c.MessageReceived) .Run(); - } - class FakeNoSynchronizedStorageSupportPersistence : PersistenceDefinition + class FakeNoSynchronizedStorageSupportPersistence : PersistenceDefinition, IPersistenceDefinitionFactory { - public FakeNoSynchronizedStorageSupportPersistence() + FakeNoSynchronizedStorageSupportPersistence() + { + Supports(); + Supports(); + } + + public static FakeNoSynchronizedStorageSupportPersistence Create() => new(); + + sealed class FakeStorage : Feature { - Supports(s => { }); - Supports(s => { }); + protected override void Setup(FeatureConfigurationContext context) + { + } } } class NoOpISubscriptionStorage : ISubscriptionStorage { - public Task> GetSubscriberAddressesForMessage(IEnumerable messageTypes, ContextBag context, CancellationToken cancellationToken = default) - { - return Task.FromResult>(null); - } + public Task> GetSubscriberAddressesForMessage(IEnumerable messageTypes, ContextBag context, CancellationToken cancellationToken = default) => Task.FromResult>(null); - public Task Subscribe(Subscriber subscriber, MessageType messageType, ContextBag context, CancellationToken cancellationToken = default) - { - return Task.CompletedTask; - } + public Task Subscribe(Subscriber subscriber, MessageType messageType, ContextBag context, CancellationToken cancellationToken = default) => Task.CompletedTask; - public Task Unsubscribe(Subscriber subscriber, MessageType messageType, ContextBag context, CancellationToken cancellationToken = default) - { - return Task.CompletedTask; - } + public Task Unsubscribe(Subscriber subscriber, MessageType messageType, ContextBag context, CancellationToken cancellationToken = default) => Task.CompletedTask; } class NoSyncEndpoint : EndpointConfigurationBuilder { - public NoSyncEndpoint() - { + public NoSyncEndpoint() => EndpointSetup(c => { // The subscription storage is needed because at this stage we have no way of DisablingPublishing on the non-generic version of ConfigureTransport c.RegisterComponents(container => container.AddSingleton()); c.UsePersistence(); }); - } } - public class MyMessageHandler : IHandleMessages + public class MyMessageHandler(Context testContext) : IHandleMessages { - public MyMessageHandler(Context context) - { - testContext = context; - } - public Task Handle(MyMessage message, IMessageHandlerContext context) { testContext.MessageReceived = true; return Task.CompletedTask; } - - Context testContext; } public class Context : ScenarioContext @@ -89,7 +79,5 @@ public class Context : ScenarioContext public bool MessageReceived { get; set; } } - public class MyMessage : ICommand - { - } + public class MyMessage : ICommand; } \ No newline at end of file diff --git a/src/NServiceBus.AcceptanceTests/Core/PublishSubscribe/When_configuring_subscription_authorizer.cs b/src/NServiceBus.AcceptanceTests/Core/PublishSubscribe/When_configuring_subscription_authorizer.cs index 1911a88c95..14c9e24171 100644 --- a/src/NServiceBus.AcceptanceTests/Core/PublishSubscribe/When_configuring_subscription_authorizer.cs +++ b/src/NServiceBus.AcceptanceTests/Core/PublishSubscribe/When_configuring_subscription_authorizer.cs @@ -100,10 +100,7 @@ public PublisherWithAuthorizer() } class StorageAccessor : FeatureStartupTask { - public StorageAccessor(ISubscriptionStorage subscriptionStorage, Context testContext) - { - testContext.SubscriptionStorage = (FakePersistence.FakeSubscriptionStorage)subscriptionStorage; - } + public StorageAccessor(ISubscriptionStorage subscriptionStorage, Context testContext) => testContext.SubscriptionStorage = (FakePersistence.FakeSubscriptionStorage)subscriptionStorage; protected override Task OnStart(IMessageSession session, CancellationToken cancellationToken = default) => Task.CompletedTask; @@ -111,27 +108,17 @@ public StorageAccessor(ISubscriptionStorage subscriptionStorage, Context testCon } } - public class AllowedEvent : IEvent - { - } + public class AllowedEvent : IEvent; - public class ForbiddenEvent : IEvent - { - } + public class ForbiddenEvent : IEvent; - class FakePersistence : PersistenceDefinition + class FakePersistence : PersistenceDefinition, IPersistenceDefinitionFactory { - public FakePersistence() - { - Supports(s => s.EnableFeatureByDefault(typeof(SubscriptionStorageFeature))); - } + FakePersistence() => Supports(); class SubscriptionStorageFeature : Feature { - protected override void Setup(FeatureConfigurationContext context) - { - context.Services.AddSingleton(new FakeSubscriptionStorage()); - } + protected override void Setup(FeatureConfigurationContext context) => context.Services.AddSingleton(new FakeSubscriptionStorage()); } public class FakeSubscriptionStorage : ISubscriptionStorage @@ -148,5 +135,7 @@ public Task Subscribe(Unicast.Subscriptions.MessageDrivenSubscriptions.Subscribe public Task> GetSubscriberAddressesForMessage(IEnumerable messageTypes, ContextBag context, CancellationToken cancellationToken = default) => throw new NotImplementedException(); } + + public static FakePersistence Create() => new(); } } \ No newline at end of file diff --git a/src/NServiceBus.AcceptanceTests/Core/Routing/MessageDrivenSubscriptions/Pub_from_sendonly.cs b/src/NServiceBus.AcceptanceTests/Core/Routing/MessageDrivenSubscriptions/Pub_from_sendonly.cs index 06c9c49857..e1ab956d9a 100644 --- a/src/NServiceBus.AcceptanceTests/Core/Routing/MessageDrivenSubscriptions/Pub_from_sendonly.cs +++ b/src/NServiceBus.AcceptanceTests/Core/Routing/MessageDrivenSubscriptions/Pub_from_sendonly.cs @@ -6,14 +6,15 @@ using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using NServiceBus; -using NServiceBus.AcceptanceTesting; -using NServiceBus.AcceptanceTests.EndpointTemplates; -using NServiceBus.Extensibility; -using NServiceBus.Features; +using AcceptanceTesting; +using EndpointTemplates; +using Extensibility; +using Features; using NServiceBus.Persistence; -using NServiceBus.Unicast.Subscriptions; +using Unicast.Subscriptions; using NServiceBus.Unicast.Subscriptions.MessageDrivenSubscriptions; using NUnit.Framework; +using Settings; using Conventions = AcceptanceTesting.Customization.Conventions; public class Pub_from_sendonly : NServiceBusAcceptanceTest @@ -39,88 +40,65 @@ public class Context : ScenarioContext public class SendOnlyPublisher : EndpointConfigurationBuilder { - public SendOnlyPublisher() - { + public SendOnlyPublisher() => EndpointSetup(b => { b.SendOnly(); - b.UsePersistence(typeof(HardCodedPersistence)); + b.UsePersistence(); b.DisableFeature(); }, metadata => metadata.RegisterSelfAsPublisherFor(this)); - } } public class Subscriber : EndpointConfigurationBuilder { - public Subscriber() - { - EndpointSetup(c => c.DisableFeature()); - } + public Subscriber() => EndpointSetup(c => c.DisableFeature()); - public class MyHandler : IHandleMessages + public class MyHandler(Context testContext) : IHandleMessages { - public MyHandler(Context context) - { - testContext = context; - } - public Task Handle(MyEvent messageThatIsEnlisted, IMessageHandlerContext context) { testContext.SubscriberGotTheEvent = true; return Task.CompletedTask; } - - Context testContext; } } - public class MyEvent : IEvent - { - } + public class MyEvent : IEvent; - public class HardCodedPersistence : PersistenceDefinition + public class HardCodedPersistence : PersistenceDefinition, IPersistenceDefinitionFactory { - internal HardCodedPersistence() - { - Supports(s => s.EnableFeatureByDefault()); - } + HardCodedPersistence() => Supports(); + + public static HardCodedPersistence Create() => new(); } - public class HardCodedPersistenceFeature : Feature + class HardCodedPersistenceFeature : Feature { - protected override void Setup(FeatureConfigurationContext context) - { - context.Services.AddSingleton(typeof(ISubscriptionStorage), typeof(HardcodedSubscriptionManager)); - } + protected override void Setup(FeatureConfigurationContext context) => + context.Services.AddSingleton(); } - public class HardcodedSubscriptionManager : ISubscriptionStorage + class HardcodedSubscriptionManager : ISubscriptionStorage { - public HardcodedSubscriptionManager() - { - addressTask = Task.FromResult(new[] - { - new Unicast.Subscriptions.MessageDrivenSubscriptions.Subscriber(Conventions.EndpointNamingConvention(typeof(Subscriber)), null) - }.AsEnumerable()); - } - - public Task Subscribe(Unicast.Subscriptions.MessageDrivenSubscriptions.Subscriber subscriber, MessageType messageType, ContextBag context, CancellationToken cancellationToken = default) - { - return Task.CompletedTask; - } + public Task Subscribe(Unicast.Subscriptions.MessageDrivenSubscriptions.Subscriber subscriber, + MessageType messageType, ContextBag context, CancellationToken cancellationToken = default) => + Task.CompletedTask; - public Task Unsubscribe(Unicast.Subscriptions.MessageDrivenSubscriptions.Subscriber subscriber, MessageType messageType, ContextBag context, CancellationToken cancellationToken = default) - { - return Task.CompletedTask; - } + public Task Unsubscribe(Unicast.Subscriptions.MessageDrivenSubscriptions.Subscriber subscriber, + MessageType messageType, ContextBag context, CancellationToken cancellationToken = default) => + Task.CompletedTask; - public Task> GetSubscriberAddressesForMessage(IEnumerable messageTypes, ContextBag context, CancellationToken cancellationToken = default) - { - return addressTask; - } + public Task> + GetSubscriberAddressesForMessage(IEnumerable messageTypes, ContextBag context, + CancellationToken cancellationToken = default) => addressTask; - Task> addressTask; + readonly Task> addressTask = + Task.FromResult(new[] + { + new Unicast.Subscriptions.MessageDrivenSubscriptions.Subscriber( + Conventions.EndpointNamingConvention(typeof(Subscriber)), null) + }.AsEnumerable()); } } \ No newline at end of file diff --git a/src/NServiceBus.AcceptanceTests/Recoverability/Cross_q_tx_msg_moved_to_error_q.cs b/src/NServiceBus.AcceptanceTests/Recoverability/Cross_q_tx_msg_moved_to_error_q.cs index 30af44ca69..b37c4fce3d 100644 --- a/src/NServiceBus.AcceptanceTests/Recoverability/Cross_q_tx_msg_moved_to_error_q.cs +++ b/src/NServiceBus.AcceptanceTests/Recoverability/Cross_q_tx_msg_moved_to_error_q.cs @@ -1,7 +1,6 @@ namespace NServiceBus.AcceptanceTests.Recoverability; using System; -using System.Linq; using System.Threading.Tasks; using AcceptanceTesting; using AcceptanceTesting.Customization; diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt index a569d08c7b..5a88a4da85 100644 --- a/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt @@ -544,7 +544,7 @@ namespace NServiceBus { public static void EnableInstallers(this NServiceBus.EndpointConfiguration config, string? username = null) { } } - public class LearningPersistence : NServiceBus.Persistence.PersistenceDefinition { } + public class LearningPersistence : NServiceBus.Persistence.PersistenceDefinition, NServiceBus.Persistence.IPersistenceDefinitionFactory { } public static class LearningSagaPersisterConfigurationExtensions { public static void SagaStorageDirectory(this NServiceBus.PersistenceExtensions persistenceExtensions, string path) { } @@ -668,25 +668,29 @@ namespace NServiceBus } public static class PersistenceConfig { + [System.Obsolete("Use \'UsePersistence\' instead. Will be removed in version 11.0.0.", true)] public static NServiceBus.PersistenceExtensions UsePersistence(this NServiceBus.EndpointConfiguration config, System.Type definitionType) { } public static NServiceBus.PersistenceExtensions UsePersistence(this NServiceBus.EndpointConfiguration config) - where T : NServiceBus.Persistence.PersistenceDefinition { } + where T : NServiceBus.Persistence.PersistenceDefinition, NServiceBus.Persistence.IPersistenceDefinitionFactory { } public static NServiceBus.PersistenceExtensions UsePersistence(this NServiceBus.EndpointConfiguration config) - where T : NServiceBus.Persistence.PersistenceDefinition + where T : NServiceBus.Persistence.PersistenceDefinition, NServiceBus.Persistence.IPersistenceDefinitionFactory where S : NServiceBus.StorageType { } } + [System.Obsolete("Use \'PersistenceExtensions\' instead. Will be removed in version 11.0.0.", true)] public class PersistenceExtensions : NServiceBus.Configuration.AdvancedExtensibility.ExposeSettings { public PersistenceExtensions(System.Type definitionType, NServiceBus.Settings.SettingsHolder settings, System.Type storageType) { } } - public class PersistenceExtensions : NServiceBus.PersistenceExtensions - where T : NServiceBus.Persistence.PersistenceDefinition + public class PersistenceExtensions : NServiceBus.Configuration.AdvancedExtensibility.ExposeSettings + where T : NServiceBus.Persistence.PersistenceDefinition, NServiceBus.Persistence.IPersistenceDefinitionFactory { public PersistenceExtensions(NServiceBus.Settings.SettingsHolder settings) { } + [System.Obsolete("Use \'PersistenceExtensions(SettingsHolder settings, StorageType? storageType = nu" + + "ll)\' instead. Will be removed in version 11.0.0.", true)] protected PersistenceExtensions(NServiceBus.Settings.SettingsHolder settings, System.Type storageType) { } } public class PersistenceExtensions : NServiceBus.PersistenceExtensions - where T : NServiceBus.Persistence.PersistenceDefinition + where T : NServiceBus.Persistence.PersistenceDefinition, NServiceBus.Persistence.IPersistenceDefinitionFactory where S : NServiceBus.StorageType { public PersistenceExtensions(NServiceBus.Settings.SettingsHolder settings) { } @@ -913,6 +917,8 @@ namespace NServiceBus } public abstract class StorageType { + public override bool Equals(object? obj) { } + public override int GetHashCode() { } public override string ToString() { } public sealed class Outbox : NServiceBus.StorageType { } public sealed class Sagas : NServiceBus.StorageType { } @@ -1557,16 +1563,26 @@ namespace NServiceBus.Persistence System.Threading.Tasks.ValueTask TryOpen(NServiceBus.Outbox.IOutboxTransaction transaction, NServiceBus.Extensibility.ContextBag context, System.Threading.CancellationToken cancellationToken = default); System.Threading.Tasks.ValueTask TryOpen(NServiceBus.Transport.TransportTransaction transportTransaction, NServiceBus.Extensibility.ContextBag context, System.Threading.CancellationToken cancellationToken = default); } + public interface IPersistenceDefinitionFactory + where out TDefinition : NServiceBus.Persistence.PersistenceDefinition, NServiceBus.Persistence.IPersistenceDefinitionFactory + { + TDefinition Create(); + } public interface ISynchronizedStorageSession { } public abstract class PersistenceDefinition { protected PersistenceDefinition() { } protected void Defaults(System.Action action) { } + [System.Obsolete("Use \'HasSupportFor()\' instead. Will be removed in version 11.0.0.", true)] public bool HasSupportFor(System.Type storageType) { } public bool HasSupportFor() where T : NServiceBus.StorageType { } + [System.Obsolete("Use \'Supports()\' instead. Will be removed in version 11.0.0.", true)] protected void Supports(System.Action action) where T : NServiceBus.StorageType { } + protected void Supports() + where TStorage : NServiceBus.StorageType + where TFeature : NServiceBus.Features.Feature { } } } namespace NServiceBus.Pipeline diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/NullableAnnotations.ApproveNullableTypes.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/NullableAnnotations.ApproveNullableTypes.approved.txt index d9b35fb0ff..9220358c2f 100644 --- a/src/NServiceBus.Core.Tests/ApprovalFiles/NullableAnnotations.ApproveNullableTypes.approved.txt +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/NullableAnnotations.ApproveNullableTypes.approved.txt @@ -52,8 +52,6 @@ NServiceBus.ImmediateRetry NServiceBus.IPipelineContext NServiceBus.IStartableEndpointWithExternallyManagedContainer NServiceBus.IToSagaExpression`1 -NServiceBus.LearningPersistence -NServiceBus.LearningSagaPersisterConfigurationExtensions NServiceBus.LearningTransport NServiceBus.LearningTransportConfigurationExtensions NServiceBus.LoadMessageHandlersExtensions @@ -80,10 +78,6 @@ NServiceBus.OutboxConfigExtensions NServiceBus.PendingTransportOperations NServiceBus.Persistence.CompletableSynchronizedStorageSessionExtensions NServiceBus.Persistence.ICompletableSynchronizedStorageSession -NServiceBus.Persistence.PersistenceDefinition -NServiceBus.PersistenceExtensions -NServiceBus.PersistenceExtensions`1 -NServiceBus.PersistenceExtensions`2 NServiceBus.Pipeline.ForkConnector`2 NServiceBus.Pipeline.IAuditActionContext NServiceBus.Pipeline.IAuditContext diff --git a/src/NServiceBus.Core.Tests/Persistence/PersistenceExtensionsTests.cs b/src/NServiceBus.Core.Tests/Persistence/PersistenceExtensionsTests.cs index d64fd57c44..6028005a2e 100644 --- a/src/NServiceBus.Core.Tests/Persistence/PersistenceExtensionsTests.cs +++ b/src/NServiceBus.Core.Tests/Persistence/PersistenceExtensionsTests.cs @@ -1,6 +1,7 @@ namespace NServiceBus.Core.Tests.Persistence; using System; +using NServiceBus.Features; using NServiceBus.Persistence; using Settings; using NUnit.Framework; @@ -11,17 +12,19 @@ public class When_configuring_storage_type_not_supported_by_persistence [Test] public void Should_throw_exception() { - var ex = Assert.Throws(() => new PersistenceExtensions(typeof(PartialPersistence), new SettingsHolder(), typeof(StorageType.Sagas))); - Assert.That(ex.Message, Does.StartWith("PartialPersistence does not support storage type Sagas.")); + var ex = Assert.Throws(() => new PersistenceExtensions(new SettingsHolder())); + Assert.That(ex.Message, Does.StartWith("PartialPersistence does not support storage type 'Sagas'.")); } - public class PartialPersistence : PersistenceDefinition + public class PartialPersistence : PersistenceDefinition, IPersistenceDefinitionFactory { - public PartialPersistence() + PartialPersistence() => Supports(); + + public static PartialPersistence Create() => new(); + + class FakeSubscriptionStorage : Feature { - Supports(s => - { - }); + protected internal override void Setup(FeatureConfigurationContext context) => throw new NotImplementedException(); } } } @@ -32,16 +35,18 @@ public class When_configuring_storage_type_supported_by_persistence [Test] public void Should_not_throw_exception() { - Assert.DoesNotThrow(() => new PersistenceExtensions(typeof(PartialPersistence), new SettingsHolder(), typeof(StorageType.Subscriptions))); + Assert.DoesNotThrow(() => new PersistenceExtensions(new SettingsHolder())); } - public class PartialPersistence : PersistenceDefinition + public class PartialPersistence : PersistenceDefinition, IPersistenceDefinitionFactory { - public PartialPersistence() + public PartialPersistence() => Supports(); + + public static PartialPersistence Create() => new(); + + class FakeSubscriptionStorage : Feature { - Supports(s => - { - }); + protected internal override void Setup(FeatureConfigurationContext context) => throw new NotImplementedException(); } } } \ No newline at end of file diff --git a/src/NServiceBus.Core.Tests/Persistence/PersistenceMergingTests.cs b/src/NServiceBus.Core.Tests/Persistence/PersistenceMergingTests.cs new file mode 100644 index 0000000000..2d4354c9b3 --- /dev/null +++ b/src/NServiceBus.Core.Tests/Persistence/PersistenceMergingTests.cs @@ -0,0 +1,131 @@ +namespace NServiceBus.Core.Tests.Persistence; + +using System.Linq; +using NServiceBus.Features; +using NServiceBus.Persistence; +using NUnit.Framework; + +[TestFixture] +public class When_no_storage_persistence_overrides_are_enabled +{ + [Test] + public void Should_use_all_storages_supported_by_persistence() + { + var config = new EndpointConfiguration("MyEndpoint"); + config.UsePersistence(); + + var registry = config.Settings.Get(); + var enabledPersistences = registry.Merge(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(enabledPersistences, Has.Count.EqualTo(1)); + Assert.That(enabledPersistences.ElementAt(0).SelectedStorages, Is.EquivalentTo(StorageType.GetAvailableStorageTypes())); + } + } + + class FakePersistence : PersistenceDefinition, IPersistenceDefinitionFactory + { + FakePersistence() + { + Supports(); + Supports(); + Supports(); + } + + public static FakePersistence Create() => new(); + + class FakeStorage : Feature + { + protected internal override void Setup(FeatureConfigurationContext context) => throw new System.NotImplementedException(); + } + } +} + +[TestFixture] +public class When_storage_overrides_are_provided +{ + [Test] + public void Should_replace_default_storages_by_overrides() + { + var config = new EndpointConfiguration("MyEndpoint"); + config.UsePersistence(); + config.UsePersistence(); + config.UsePersistence(); + + var registry = config.Settings.Get(); + var enabledPersistences = registry.Merge(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(enabledPersistences, Has.Count.EqualTo(2)); + Assert.That(enabledPersistences.ElementAt(0).SelectedStorages, Is.EquivalentTo([StorageType.Sagas.Instance, StorageType.Subscriptions.Instance])); + Assert.That(enabledPersistences.ElementAt(1).SelectedStorages, Is.EquivalentTo([StorageType.Outbox.Instance])); + } + } + + class FakePersistence2 : PersistenceDefinition, IPersistenceDefinitionFactory + { + FakePersistence2() + { + Supports(); + Supports(); + } + + public static FakePersistence2 Create() => new(); + + class FakeStorage : Feature + { + protected internal override void Setup(FeatureConfigurationContext context) => throw new System.NotImplementedException(); + } + } + + class FakePersistence : PersistenceDefinition, IPersistenceDefinitionFactory + { + FakePersistence() + { + Supports(); + Supports(); + Supports(); + } + + public static FakePersistence Create() => new(); + + class FakeStorage : Feature + { + protected internal override void Setup(FeatureConfigurationContext context) => throw new System.NotImplementedException(); + } + } +} + +[TestFixture] +public class When_explicitly_enabling_selected_storage +{ + [Test] + public void Should_not_use_other_supported_storages() + { + var config = new EndpointConfiguration("MyEndpoint"); + config.UsePersistence(); + + var registry = config.Settings.Get(); + var enabledPersistences = registry.Merge(); + + Assert.That(enabledPersistences.Any(p => p.SelectedStorages.Contains(StorageType.Subscriptions.Instance)), Is.False); + } + + class FakePersistence : PersistenceDefinition, IPersistenceDefinitionFactory + { + FakePersistence() + { + Supports(); + Supports(); + } + + public static FakePersistence Create() => new(); + + class FakeStorage : Feature + { + protected internal override void Setup(FeatureConfigurationContext context) => throw new System.NotImplementedException(); + } + } +} diff --git a/src/NServiceBus.Core.Tests/Persistence/PersistenceStartupTests.cs b/src/NServiceBus.Core.Tests/Persistence/PersistenceStartupTests.cs index 8d63611aa3..dc6e39f5e7 100644 --- a/src/NServiceBus.Core.Tests/Persistence/PersistenceStartupTests.cs +++ b/src/NServiceBus.Core.Tests/Persistence/PersistenceStartupTests.cs @@ -60,13 +60,25 @@ public void Should_not_prevent_using_different_persistence_for_sagas_and_outbox_ Assert.DoesNotThrow(() => config.Settings.ConfigurePersistence(), "Should not throw for a single single feature enabled out of the two."); } - class FakeSagaPersistence : PersistenceDefinition + class FakeSagaPersistence : PersistenceDefinition, IPersistenceDefinitionFactory { - public FakeSagaPersistence() => Supports(_ => { }); + FakeSagaPersistence() => Supports(); + public static FakeSagaPersistence Create() => new(); + + class FakeStorage : Feature + { + protected internal override void Setup(FeatureConfigurationContext context) => throw new System.NotImplementedException(); + } } - class FakeOutboxPersistence : PersistenceDefinition + class FakeOutboxPersistence : PersistenceDefinition, IPersistenceDefinitionFactory { - public FakeOutboxPersistence() => Supports(_ => { }); + FakeOutboxPersistence() => Supports(); + public static FakeOutboxPersistence Create() => new(); + + class FakeStorage : Feature + { + protected internal override void Setup(FeatureConfigurationContext context) => throw new System.NotImplementedException(); + } } } diff --git a/src/NServiceBus.Core.Tests/Persistence/PersistenceStorageMergerTests.cs b/src/NServiceBus.Core.Tests/Persistence/PersistenceStorageMergerTests.cs deleted file mode 100644 index 84e4f9fbc7..0000000000 --- a/src/NServiceBus.Core.Tests/Persistence/PersistenceStorageMergerTests.cs +++ /dev/null @@ -1,98 +0,0 @@ -namespace NServiceBus.Core.Tests.Persistence; - -using System.Collections.Generic; -using System.Linq; -using NServiceBus.Persistence; -using NUnit.Framework; - -[TestFixture] -public class When_no_storage_persistence_overrides_are_enabled -{ - [Test] - public void Should_use_all_storages_supported_by_persistence() - { - var config = new EndpointConfiguration("MyEndpoint"); - config.UsePersistence(); - var persistences = config.Settings.Get>(PersistenceComponent.PersistenceDefinitionsSettingsKey); - - var resultedEnabledPersistences = config.Settings.MergePersistences(persistences); - - Assert.That(resultedEnabledPersistences[0].SelectedStorages, Is.EquivalentTo(StorageType.GetAvailableStorageTypes())); - } - - class FakePersistence : PersistenceDefinition - { - public FakePersistence() - { - Supports(_ => { }); - Supports(_ => { }); - Supports(_ => { }); - } - } -} - -[TestFixture] -public class When_storage_overrides_are_provided -{ - [Test] - public void Should_replace_default_storages_by_overrides() - { - var config = new EndpointConfiguration("MyEndpoint"); - config.UsePersistence(); - config.UsePersistence(); - config.UsePersistence(); - var persistences = config.Settings.Get>(PersistenceComponent.PersistenceDefinitionsSettingsKey); - - var resultedEnabledPersistences = config.Settings.MergePersistences(persistences); - - using (Assert.EnterMultipleScope()) - { - Assert.That(resultedEnabledPersistences[0].SelectedStorages, Is.EquivalentTo([typeof(StorageType.Subscriptions)])); - Assert.That(resultedEnabledPersistences[1].SelectedStorages, Is.EquivalentTo([typeof(StorageType.Sagas)])); - } - } - - class FakePersistence2 : PersistenceDefinition - { - public FakePersistence2() - { - Supports(_ => { }); - Supports(_ => { }); - } - } - - class FakePersistence : PersistenceDefinition - { - public FakePersistence() - { - Supports(_ => { }); - Supports(_ => { }); - Supports(_ => { }); - } - } -} - -[TestFixture] -public class When_explicitly_enabling_selected_storage -{ - [Test] - public void Should_not_use_other_supported_storages() - { - var config = new EndpointConfiguration("MyEndpoint"); - config.UsePersistence(); - var persistences = config.Settings.Get>(PersistenceComponent.PersistenceDefinitionsSettingsKey); - - var resultedEnabledPersistences = config.Settings.MergePersistences(persistences); - - Assert.That(resultedEnabledPersistences.Any(p => p.SelectedStorages.Contains(typeof(StorageType.Subscriptions))), Is.False); - } - - class FakePersistence : PersistenceDefinition - { - public FakePersistence() - { - Supports(_ => { }); - Supports(_ => { }); - } - } -} diff --git a/src/NServiceBus.Core/Features/SettingsExtensions.cs b/src/NServiceBus.Core/Features/SettingsExtensions.cs index 47825adeb3..ad75aa9978 100644 --- a/src/NServiceBus.Core/Features/SettingsExtensions.cs +++ b/src/NServiceBus.Core/Features/SettingsExtensions.cs @@ -8,7 +8,7 @@ namespace NServiceBus.Features; /// /// Feature related extensions to the settings. /// -public static partial class SettingsExtensions +public static class SettingsExtensions { /// /// Marks the given feature as enabled by default. @@ -51,23 +51,33 @@ public static bool IsFeatureEnabled(this IReadOnlySettings settings, Type featur return settings.GetOrDefault(featureType.FullName) == FeatureState.Enabled; } + internal static bool IsFeatureEnabled(this IReadOnlySettings settings) where T : Feature + { + ArgumentNullException.ThrowIfNull(settings); + return settings.IsFeatureEnabled(typeof(T)); + } + internal static void EnableFeature(this SettingsHolder settings, Type featureType) { + ArgumentNullException.ThrowIfNull(settings); settings.Set(featureType.FullName, FeatureState.Enabled); } internal static void DisableFeature(this SettingsHolder settings, Type featureType) { + ArgumentNullException.ThrowIfNull(settings); settings.Set(featureType.FullName, FeatureState.Disabled); } internal static void MarkFeatureAsActive(this SettingsHolder settings, Type featureType) { + ArgumentNullException.ThrowIfNull(settings); settings.Set(featureType.FullName, FeatureState.Active); } internal static void MarkFeatureAsDeactivated(this SettingsHolder settings, Type featureType) { + ArgumentNullException.ThrowIfNull(settings); settings.Set(featureType.FullName, FeatureState.Deactivated); } } \ No newline at end of file diff --git a/src/NServiceBus.Core/Persistence/EnabledPersistence.cs b/src/NServiceBus.Core/Persistence/EnabledPersistence.cs index ee464832c9..0d12fb179c 100644 --- a/src/NServiceBus.Core/Persistence/EnabledPersistence.cs +++ b/src/NServiceBus.Core/Persistence/EnabledPersistence.cs @@ -1,10 +1,8 @@ -namespace NServiceBus; +#nullable enable + +namespace NServiceBus; -using System; using System.Collections.Generic; +using Persistence; -class EnabledPersistence -{ - public List SelectedStorages { get; set; } - public Type DefinitionType; -} \ No newline at end of file +record EnabledPersistence(IReadOnlyCollection SelectedStorages, PersistenceDefinition Definition); \ No newline at end of file diff --git a/src/NServiceBus.Core/Persistence/IPersistenceDefinitionFactory.cs b/src/NServiceBus.Core/Persistence/IPersistenceDefinitionFactory.cs new file mode 100644 index 0000000000..e9f02f31fe --- /dev/null +++ b/src/NServiceBus.Core/Persistence/IPersistenceDefinitionFactory.cs @@ -0,0 +1,17 @@ +#nullable enable + +namespace NServiceBus.Persistence; + +/// +/// Defines a factory for creating persistence definitions used in . +/// +/// The persistence definition type. +public interface IPersistenceDefinitionFactory + where TDefinition : PersistenceDefinition, IPersistenceDefinitionFactory +{ + /// + /// Creates the persistence definition. + /// + /// The persistence definition. + static abstract TDefinition Create(); +} \ No newline at end of file diff --git a/src/NServiceBus.Core/Persistence/Learning/LearningPersistence.cs b/src/NServiceBus.Core/Persistence/Learning/LearningPersistence.cs index 1c00bf777a..c9f4802ccf 100644 --- a/src/NServiceBus.Core/Persistence/Learning/LearningPersistence.cs +++ b/src/NServiceBus.Core/Persistence/Learning/LearningPersistence.cs @@ -1,4 +1,6 @@ -namespace NServiceBus; +#nullable enable + +namespace NServiceBus; using Features; using Persistence; @@ -6,7 +8,12 @@ /// /// Used to enable Learning persistence. /// -public class LearningPersistence : PersistenceDefinition +public class LearningPersistence : PersistenceDefinition, IPersistenceDefinitionFactory { - internal LearningPersistence() => Supports(s => s.EnableFeatureByDefault()); + LearningPersistence() => Supports(); + + /// + /// Creates the learning persistence definition. + /// + static LearningPersistence IPersistenceDefinitionFactory.Create() => new(); } \ No newline at end of file diff --git a/src/NServiceBus.Core/Persistence/Learning/LearningSynchronizedStorage.cs b/src/NServiceBus.Core/Persistence/Learning/LearningSynchronizedStorage.cs index 053de1d97a..01b788ed73 100644 --- a/src/NServiceBus.Core/Persistence/Learning/LearningSynchronizedStorage.cs +++ b/src/NServiceBus.Core/Persistence/Learning/LearningSynchronizedStorage.cs @@ -1,3 +1,5 @@ +#nullable enable + namespace NServiceBus; using Features; diff --git a/src/NServiceBus.Core/Persistence/Learning/LearningSynchronizedStorageSession.cs b/src/NServiceBus.Core/Persistence/Learning/LearningSynchronizedStorageSession.cs index 483ea8c212..d719c23ec6 100644 --- a/src/NServiceBus.Core/Persistence/Learning/LearningSynchronizedStorageSession.cs +++ b/src/NServiceBus.Core/Persistence/Learning/LearningSynchronizedStorageSession.cs @@ -1,3 +1,5 @@ +#nullable enable + namespace NServiceBus; using System; @@ -61,7 +63,7 @@ public async Task CompleteAsync(CancellationToken cancellationToken = default) deferredActions.Clear(); } - public async Task Read(Guid sagaId, SagaManifestCollection sagaManifests, CancellationToken cancellationToken = default) where TSagaData : class, IContainSagaData + public async Task Read(Guid sagaId, SagaManifestCollection sagaManifests, CancellationToken cancellationToken = default) where TSagaData : class, IContainSagaData { var sagaStorageFile = await Open(sagaId, typeof(TSagaData), sagaManifests, cancellationToken) .ConfigureAwait(false); @@ -81,7 +83,7 @@ public async Task Read(Guid sagaId, SagaManifestCollection public void Complete(IContainSagaData sagaData, SagaManifestCollection sagaManifests) => deferredActions.Add(new CompleteAction(sagaData, sagaFiles, sagaManifests)); - async Task Open(Guid sagaId, Type entityType, SagaManifestCollection sagaManifests, CancellationToken cancellationToken) + async Task Open(Guid sagaId, Type entityType, SagaManifestCollection sagaManifests, CancellationToken cancellationToken) { var sagaManifest = sagaManifests.GetForEntityType(entityType); diff --git a/src/NServiceBus.Core/Persistence/Learning/SagaPersister/LearningSagaPersisterConfigurationExtensions.cs b/src/NServiceBus.Core/Persistence/Learning/SagaPersister/LearningSagaPersisterConfigurationExtensions.cs index beb1542a41..dd0c9579d3 100644 --- a/src/NServiceBus.Core/Persistence/Learning/SagaPersister/LearningSagaPersisterConfigurationExtensions.cs +++ b/src/NServiceBus.Core/Persistence/Learning/SagaPersister/LearningSagaPersisterConfigurationExtensions.cs @@ -1,4 +1,6 @@ -namespace NServiceBus; +#nullable enable + +namespace NServiceBus; using System; using Features; diff --git a/src/NServiceBus.Core/Persistence/Learning/SagaPersister/SagaStorageFile.cs b/src/NServiceBus.Core/Persistence/Learning/SagaPersister/SagaStorageFile.cs index 86724c170e..253df6a821 100644 --- a/src/NServiceBus.Core/Persistence/Learning/SagaPersister/SagaStorageFile.cs +++ b/src/NServiceBus.Core/Persistence/Learning/SagaPersister/SagaStorageFile.cs @@ -1,3 +1,5 @@ +#nullable enable + namespace NServiceBus; using System; @@ -47,7 +49,7 @@ public async ValueTask DisposeAsync() fileStream = null; } - public static Task Open(Guid sagaId, SagaManifest manifest, CancellationToken cancellationToken = default) + public static Task Open(Guid sagaId, SagaManifest manifest, CancellationToken cancellationToken = default) { var filePath = manifest.GetFilePath(sagaId); @@ -56,7 +58,7 @@ public static Task Open(Guid sagaId, SagaManifest manifest, Can return noSagaFoundResult; } - return OpenWithRetryOnConcurrency(filePath, FileMode.Open, cancellationToken); + return OpenWithRetryOnConcurrency(filePath, FileMode.Open, cancellationToken)!; } public static Task Create(Guid sagaId, SagaManifest manifest, CancellationToken cancellationToken = default) @@ -96,6 +98,8 @@ await Task.Delay(100, cancellationToken) public async Task Write(IContainSagaData sagaData, CancellationToken cancellationToken = default) { + ObjectDisposedException.ThrowIf(fileStream is null, this); + fileStream.Position = 0; await JsonSerializer.SerializeAsync(fileStream, sagaData, sagaData.GetType(), Options, cancellationToken) .ConfigureAwait(false); @@ -107,14 +111,18 @@ await JsonSerializer.SerializeAsync(fileStream, sagaData, sagaData.GetType(), Op public void MarkAsCompleted() => isCompleted = true; - public ValueTask Read(CancellationToken cancellationToken = default) where TSagaData : class, IContainSagaData - => JsonSerializer.DeserializeAsync(fileStream, Options, cancellationToken); + public ValueTask Read(CancellationToken cancellationToken = default) where TSagaData : class, IContainSagaData + { + ObjectDisposedException.ThrowIf(fileStream is null, this); + + return JsonSerializer.DeserializeAsync(fileStream, Options, cancellationToken); + } - FileStream fileStream; + FileStream? fileStream; bool isCompleted; const int DefaultBufferSize = 4096; - static readonly Task noSagaFoundResult = Task.FromResult(null); + static readonly Task noSagaFoundResult = Task.FromResult(null); static readonly JsonSerializerOptions Options = new() { diff --git a/src/NServiceBus.Core/Persistence/PersistenceComponent.cs b/src/NServiceBus.Core/Persistence/PersistenceComponent.cs index 84f7519754..3cccfc0f35 100644 --- a/src/NServiceBus.Core/Persistence/PersistenceComponent.cs +++ b/src/NServiceBus.Core/Persistence/PersistenceComponent.cs @@ -1,91 +1,75 @@ -namespace NServiceBus; +#nullable enable + +namespace NServiceBus; using System; using System.Collections.Generic; using System.Linq; using Features; using Logging; -using Persistence; using Settings; static class PersistenceComponent { public static void ConfigurePersistence(this SettingsHolder settings) { - if (!settings.TryGet(PersistenceDefinitionsSettingsKey, out List definitions)) + if (!settings.TryGet(out var persistenceRegistry)) { return; } - var enabledPersistences = settings.MergePersistences(definitions); + var enabledPersistences = persistenceRegistry.Merge(); settings.ValidateSagaAndOutboxUseSamePersistence(enabledPersistences); - var resultingSupportedStorages = new List(); + var resultingSupportedStorages = new List(); var diagnostics = new Dictionary(); - foreach (var definition in enabledPersistences) + foreach (var enabledPersistence in enabledPersistences) { - var persistenceDefinition = definition.DefinitionType.Construct(); - + var persistenceDefinition = enabledPersistence.Definition; persistenceDefinition.ApplyDefaults(settings); - foreach (var storageType in definition.SelectedStorages) + foreach (var storageType in enabledPersistence.SelectedStorages) { - Logger.DebugFormat("Activating persistence '{0}' to provide storage for '{1}' storage.", definition.DefinitionType.Name, storageType); + Logger.DebugFormat("Activating persistence '{0}' to provide storage for '{1}' storage.", persistenceDefinition.Name, storageType); persistenceDefinition.ApplyActionForStorage(storageType, settings); resultingSupportedStorages.Add(storageType); - diagnostics.Add(storageType.Name, new + diagnostics.Add(storageType.ToString(), new { - Type = definition.DefinitionType.FullName, - Version = FileVersionRetriever.GetFileVersion(definition.DefinitionType) + Type = persistenceDefinition.FullName, + Version = FileVersionRetriever.GetFileVersion(persistenceDefinition.GetType()) }); } } - settings.Set(ResultingSupportedStoragesSettingsKey, resultingSupportedStorages); + settings.Set>(resultingSupportedStorages); settings.AddStartupDiagnosticsSection("Persistence", diagnostics); } - static void ValidateSagaAndOutboxUseSamePersistence(this SettingsHolder settings, List enabledPersistences) + static void ValidateSagaAndOutboxUseSamePersistence(this SettingsHolder settings, IReadOnlyCollection enabledPersistences) { - var sagaPersisterType = enabledPersistences.FirstOrDefault(p => p.SelectedStorages.Contains(typeof(StorageType.Sagas))); - var outboxPersisterType = enabledPersistences.FirstOrDefault(p => p.SelectedStorages.Contains(typeof(StorageType.Outbox))); - var bothFeaturesEnabled = settings.IsFeatureEnabled(typeof(Features.Sagas)) && settings.IsFeatureEnabled(typeof(Features.Outbox)); + var sagaPersisterDefinition = enabledPersistences.FirstOrDefault(p => p.SelectedStorages.Contains())?.Definition; + var outboxPersisterDefinition = enabledPersistences.FirstOrDefault(p => p.SelectedStorages.Contains())?.Definition; + var bothFeaturesEnabled = settings.IsFeatureEnabled() && settings.IsFeatureEnabled(); - if (sagaPersisterType != null - && outboxPersisterType != null - && sagaPersisterType.DefinitionType != outboxPersisterType.DefinitionType + if (sagaPersisterDefinition != null + && outboxPersisterDefinition != null + && sagaPersisterDefinition != outboxPersisterDefinition && bothFeaturesEnabled) { - throw new Exception($"Sagas and the Outbox need to use the same type of persistence. Saga persistence is configured to use {sagaPersisterType.DefinitionType.Name}. Outbox persistence is configured to use {outboxPersisterType.DefinitionType.Name}."); - } - } - - internal static List GetOrSetEnabledPersistences(this SettingsHolder settings) - { - if (settings.TryGet(PersistenceDefinitionsSettingsKey, out List definitions)) - { - return definitions; + throw new Exception($"Sagas and the Outbox need to use the same type of persistence. Saga persistence is configured to use '{sagaPersisterDefinition.Name}'. Outbox persistence is configured to use '{outboxPersisterDefinition.Name}'."); } - - definitions = []; - settings.Set(PersistenceDefinitionsSettingsKey, definitions); - return definitions; } internal static bool HasSupportFor(this IReadOnlySettings settings) where T : StorageType { - settings.TryGet(ResultingSupportedStoragesSettingsKey, out List supportedStorages); + _ = settings.TryGet(out IReadOnlyCollection supportedStorages); - return supportedStorages?.Contains(typeof(T)) ?? false; + return supportedStorages.Contains(); } - internal const string PersistenceDefinitionsSettingsKey = "PersistenceDefinitions"; - - const string ResultingSupportedStoragesSettingsKey = "ResultingSupportedStorages"; - static readonly ILog Logger = LogManager.GetLogger(typeof(PersistenceComponent)); } \ No newline at end of file diff --git a/src/NServiceBus.Core/Persistence/PersistenceConfig.cs b/src/NServiceBus.Core/Persistence/PersistenceConfig.cs index 7deb054bc6..c53e890762 100644 --- a/src/NServiceBus.Core/Persistence/PersistenceConfig.cs +++ b/src/NServiceBus.Core/Persistence/PersistenceConfig.cs @@ -8,14 +8,15 @@ namespace NServiceBus; /// /// Enables users to select persistence by calling .UsePersistence(). /// -public static class PersistenceConfig +public static partial class PersistenceConfig { /// /// Configures the given persistence to be used. /// /// The persistence definition eg , NHibernate etc. /// The instance to apply the settings to. - public static PersistenceExtensions UsePersistence(this EndpointConfiguration config) where T : PersistenceDefinition + public static PersistenceExtensions UsePersistence(this EndpointConfiguration config) + where T : PersistenceDefinition, IPersistenceDefinitionFactory { ArgumentNullException.ThrowIfNull(config); return new PersistenceExtensions(config.Settings); @@ -27,22 +28,11 @@ public static PersistenceExtensions UsePersistence(this EndpointConfigurat /// The persistence definition eg , NHibernate etc. /// The storage type. /// The instance to apply the settings to. - public static PersistenceExtensions UsePersistence(this EndpointConfiguration config) where T : PersistenceDefinition + public static PersistenceExtensions UsePersistence(this EndpointConfiguration config) + where T : PersistenceDefinition, IPersistenceDefinitionFactory where S : StorageType { ArgumentNullException.ThrowIfNull(config); return new PersistenceExtensions(config.Settings); } - - /// - /// Configures the given persistence to be used. - /// - /// The instance to apply the settings to. - /// The persistence definition eg , NHibernate etc. - public static PersistenceExtensions UsePersistence(this EndpointConfiguration config, Type definitionType) - { - ArgumentNullException.ThrowIfNull(config); - ArgumentNullException.ThrowIfNull(definitionType); - return new PersistenceExtensions(definitionType, config.Settings, null); - } } \ No newline at end of file diff --git a/src/NServiceBus.Core/Persistence/PersistenceDefinition.cs b/src/NServiceBus.Core/Persistence/PersistenceDefinition.cs index 8975b17fb3..3bbf2eb881 100644 --- a/src/NServiceBus.Core/Persistence/PersistenceDefinition.cs +++ b/src/NServiceBus.Core/Persistence/PersistenceDefinition.cs @@ -1,26 +1,31 @@ -namespace NServiceBus.Persistence; +#nullable enable + +namespace NServiceBus.Persistence; using System; using System.Collections.Generic; -using System.Linq; +using Features; using Settings; /// /// Base class for persistence definitions. /// -public abstract class PersistenceDefinition +public abstract partial class PersistenceDefinition { /// /// Used by the storage definitions to declare what they support. /// - protected void Supports(Action action) where T : StorageType + protected void Supports() + where TStorage : StorageType + where TFeature : Feature { - ArgumentNullException.ThrowIfNull(action); - if (storageToActionMap.ContainsKey(typeof(T))) + var storageType = StorageType.Get(); + if (storageToFeatureMap.TryGetValue(storageType, out var supportedStorageType)) { - throw new Exception($"Action for {typeof(T)} already defined."); + throw new Exception($"Storage {typeof(TStorage)} is already supported by {supportedStorageType}"); } - storageToActionMap[typeof(T)] = action; + + storageToFeatureMap[storageType] = typeof(TFeature); } /// @@ -35,28 +40,17 @@ protected void Defaults(Action action) /// /// True if supplied storage is supported. /// - public bool HasSupportFor() where T : StorageType - { - return HasSupportFor(typeof(T)); - } + public bool HasSupportFor() where T : StorageType => HasSupportFor(StorageType.Get()); - /// - /// True if supplied storage is supported. - /// - public bool HasSupportFor(Type storageType) - { - ArgumentNullException.ThrowIfNull(storageType); - return storageToActionMap.ContainsKey(storageType); - } + internal string Name => GetType().Name; + internal string FullName => GetType().FullName ?? Name; + + internal bool HasSupportFor(StorageType storageType) => storageToFeatureMap.ContainsKey(storageType); - internal void ApplyActionForStorage(Type storageType, SettingsHolder settings) + internal void ApplyActionForStorage(StorageType storageType, SettingsHolder settings) { - if (!storageType.IsSubclassOf(typeof(StorageType))) - { - throw new ArgumentException($"Storage type '{storageType.FullName}' is not a sub-class of StorageType", nameof(storageType)); - } - var actionForStorage = storageToActionMap[storageType]; - actionForStorage(settings); + var featureSupportingStorage = storageToFeatureMap[storageType]; + _ = settings.EnableFeatureByDefault(featureSupportingStorage); } internal void ApplyDefaults(SettingsHolder settings) @@ -67,16 +61,9 @@ internal void ApplyDefaults(SettingsHolder settings) } } - internal List GetSupportedStorages(List selectedStorages) - { - if (selectedStorages.Count > 0) - { - return selectedStorages; - } - - return storageToActionMap.Keys.ToList(); - } + internal IReadOnlyCollection GetSupportedStorages(IReadOnlyCollection selectedStorages) => + selectedStorages.Count > 0 ? selectedStorages : [.. storageToFeatureMap.Keys]; readonly List> defaults = []; - readonly Dictionary> storageToActionMap = []; + readonly Dictionary storageToFeatureMap = []; } \ No newline at end of file diff --git a/src/NServiceBus.Core/Persistence/PersistenceExtensions.cs b/src/NServiceBus.Core/Persistence/PersistenceExtensions.cs index 2aeefe034b..a06389b4b2 100644 --- a/src/NServiceBus.Core/Persistence/PersistenceExtensions.cs +++ b/src/NServiceBus.Core/Persistence/PersistenceExtensions.cs @@ -1,7 +1,7 @@ -namespace NServiceBus; +#nullable enable + +namespace NServiceBus; -using System; -using System.Collections.Generic; using Configuration.AdvancedExtensibility; using Persistence; using Settings; @@ -12,71 +12,35 @@ /// /// The persister definition eg , etc. /// The storage type. -public class PersistenceExtensions : PersistenceExtensions - where T : PersistenceDefinition - where S : StorageType -{ - /// - /// Initializes a new instance of . - /// - public PersistenceExtensions(SettingsHolder settings) : base(settings, typeof(S)) - { - } -} +/// +/// Initializes a new instance of . +/// +public class PersistenceExtensions(SettingsHolder settings) : PersistenceExtensions(settings, StorageType.Get()) + where T : PersistenceDefinition, IPersistenceDefinitionFactory + where S : StorageType; /// /// This class provides implementers of persisters with an extension mechanism for custom settings via extension /// methods. /// /// The persister definition eg , etc. -public class PersistenceExtensions : PersistenceExtensions where T : PersistenceDefinition +public partial class PersistenceExtensions : ExposeSettings + where T : PersistenceDefinition, IPersistenceDefinitionFactory { /// /// Default constructor. /// - public PersistenceExtensions(SettingsHolder settings) : base(typeof(T), settings, null) + public PersistenceExtensions(SettingsHolder settings) : this(settings, default(StorageType)) { } - /// - /// Constructor for a specific . - /// - protected PersistenceExtensions(SettingsHolder settings, Type storageType) : base(typeof(T), settings, storageType) + internal PersistenceExtensions(SettingsHolder settings, StorageType? storageType = null) : base(settings) { - } -} - -/// -/// This class provides implementers of persisters with an extension mechanism for custom settings via extension -/// methods. -/// -public class PersistenceExtensions : ExposeSettings -{ - /// - /// Initializes a new instance of . - /// - public PersistenceExtensions(Type definitionType, SettingsHolder settings, Type storageType) - : base(settings) - { - List definitions = settings.GetOrSetEnabledPersistences(); - - var enabledPersistence = new EnabledPersistence - { - DefinitionType = definitionType, - SelectedStorages = [] - }; - - if (storageType != null) + var registry = settings.GetOrCreate(); + var enablePersistence = registry.Enable(); + if (storageType is not null) { - var definition = definitionType.Construct(); - if (!definition.HasSupportFor(storageType)) - { - throw new Exception($"{definitionType.Name} does not support storage type {storageType.Name}. See http://docs.particular.net/nservicebus/persistence-in-nservicebus for supported variations."); - } - - enabledPersistence.SelectedStorages.Add(storageType); + enablePersistence.WithStorage(storageType); } - - definitions.Add(enabledPersistence); } } \ No newline at end of file diff --git a/src/NServiceBus.Core/Persistence/PersistenceRegistry.cs b/src/NServiceBus.Core/Persistence/PersistenceRegistry.cs new file mode 100644 index 0000000000..7083f19ce6 --- /dev/null +++ b/src/NServiceBus.Core/Persistence/PersistenceRegistry.cs @@ -0,0 +1,102 @@ +#nullable enable + +namespace NServiceBus; + +using System; +using System.Collections.Generic; +using Persistence; + +sealed class PersistenceRegistry +{ + public EnableBuilder Enable() + where TDefinition : PersistenceDefinition, IPersistenceDefinitionFactory + { + if (tracker.TryGetValue(typeof(TDefinition), out var position)) + { + return (EnableBuilder)definitions[position]; + } + + var strongBuilder = new EnableBuilder(); + // using the count here works because we never remove enabled persistences + tracker.Add(typeof(TDefinition), definitions.Count); + definitions.Add(strongBuilder); + return strongBuilder; + } + + public IReadOnlyCollection Merge() + { + // the order of the definitions is reversed when merging because the last UsePersistence calls have higher priority + // taking a copy to avoid modifying the original list + var builtPersistences = new List(definitions); + builtPersistences.Reverse(); + + var availableStorages = new List(StorageType.GetAvailableStorageTypes()); + var enabledPersistences = new List(); + + foreach (var (persistence, enabledStorageTypes) in builtPersistences) + { + var supportedStorages = persistence.GetSupportedStorages(enabledStorageTypes); + + var selectedStorages = new List(); + + foreach (var storageType in supportedStorages) + { + if (!availableStorages.Contains(storageType)) + { + continue; + } + + selectedStorages.Add(storageType); + availableStorages.Remove(storageType); + } + + if (selectedStorages.Count != 0) + { + enabledPersistences.Add(new EnabledPersistence(selectedStorages, persistence)); + } + } + + return enabledPersistences; + } + + public class EnableBuilder : IEnabledBuilder + where TDefinition : PersistenceDefinition, IPersistenceDefinitionFactory + { + public PersistenceDefinition Persistence { get; } = TDefinition.Create(); + + public IReadOnlyCollection EnabledStorageTypes => enabledStorageTypes; + + public void WithStorage() where TStorage : StorageType => WithStorage(StorageType.Get()); + + public void WithStorage(StorageType storageType) + { + if (!Persistence.HasSupportFor(storageType)) + { + throw new Exception($"{typeof(TDefinition).Name} does not support storage type '{storageType}'. See http://docs.particular.net/nservicebus/persistence-in-nservicebus for supported variations."); + } + + enabledStorageTypes.Add(storageType); + } + + readonly HashSet enabledStorageTypes = []; + } + + interface IEnabledBuilder + { + PersistenceDefinition Persistence { get; } + + IReadOnlyCollection EnabledStorageTypes { get; } + + void Deconstruct(out PersistenceDefinition persistence, out IReadOnlyCollection enabledStorageTypes) + { + persistence = Persistence; + enabledStorageTypes = EnabledStorageTypes; + } + } + + readonly Dictionary tracker = []; + // using a list to preserve the order of registrations since a dictionary does not guarantee it + // the order is important because the last UsePersistence calls have higher priority during merging + // that's why the list is reversed when merging. + readonly List definitions = []; +} \ No newline at end of file diff --git a/src/NServiceBus.Core/Persistence/PersistenceStorageMerger.cs b/src/NServiceBus.Core/Persistence/PersistenceStorageMerger.cs deleted file mode 100644 index 5f797c8f5c..0000000000 --- a/src/NServiceBus.Core/Persistence/PersistenceStorageMerger.cs +++ /dev/null @@ -1,45 +0,0 @@ -namespace NServiceBus; - -using System.Collections.Generic; -using Persistence; -using Settings; - -static class PersistenceStorageMerger -{ - public static List MergePersistences(this SettingsHolder settings, List definitions) - { - definitions.Reverse(); - - var availableStorages = StorageType.GetAvailableStorageTypes(); - var mergedEnabledPersistences = new List(); - - foreach (var definition in definitions) - { - var persistenceDefinition = definition.DefinitionType.Construct(); - var supportedStorages = persistenceDefinition.GetSupportedStorages(definition.SelectedStorages); - - var currentDefinition = new EnabledPersistence - { - DefinitionType = definition.DefinitionType, - SelectedStorages = [] - }; - - foreach (var storageType in supportedStorages) - { - if (availableStorages.Contains(storageType)) - { - currentDefinition.SelectedStorages.Add(storageType); - availableStorages.Remove(storageType); - persistenceDefinition.ApplyActionForStorage(storageType, settings); - } - } - - if (currentDefinition.SelectedStorages.Count != 0) - { - mergedEnabledPersistences.Add(currentDefinition); - } - } - - return mergedEnabledPersistences; - } -} \ No newline at end of file diff --git a/src/NServiceBus.Core/Persistence/StorageType.cs b/src/NServiceBus.Core/Persistence/StorageType.cs index de9cec00d9..346db27034 100644 --- a/src/NServiceBus.Core/Persistence/StorageType.cs +++ b/src/NServiceBus.Core/Persistence/StorageType.cs @@ -1,29 +1,44 @@ -namespace NServiceBus; +#nullable enable + +namespace NServiceBus; using System; using System.Collections.Generic; -using System.Linq; /// /// The storage types used for NServiceBus needs. /// -public abstract partial class StorageType +public abstract class StorageType { - StorageType(string storage) - { - this.storage = storage; - } + StorageType(string storage) => this.storage = storage; + + /// + public override string ToString() => storage; /// - public override string ToString() + public override bool Equals(object? obj) { - return storage; + if (obj is StorageType other) + { + return storage == other.storage; + } + + return false; } - internal static List GetAvailableStorageTypes() + /// + public override int GetHashCode() => storage.GetHashCode(); + + internal static IReadOnlyCollection GetAvailableStorageTypes() => + [Subscriptions.Instance, Sagas.Instance, Outbox.Instance]; + + internal static StorageType Get() where TStorage : StorageType => typeof(TStorage) switch { - return typeof(StorageType).GetNestedTypes().Where(t => t.BaseType == typeof(StorageType)).ToList(); - } + { } t when t == typeof(Subscriptions) => Subscriptions.Instance, + { } t when t == typeof(Sagas) => Sagas.Instance, + { } t when t == typeof(Outbox) => Outbox.Instance, + _ => throw new InvalidOperationException($"The storage type '{typeof(TStorage)}' is not supported.") + }; readonly string storage; @@ -32,9 +47,11 @@ internal static List GetAvailableStorageTypes() /// public sealed class Subscriptions : StorageType { - internal Subscriptions() : base("Subscriptions") + Subscriptions() : base("Subscriptions") { } + + internal static readonly StorageType Instance = new Subscriptions(); } /// @@ -42,9 +59,11 @@ internal Subscriptions() : base("Subscriptions") /// public sealed class Sagas : StorageType { - internal Sagas() : base("Sagas") + Sagas() : base("Sagas") { } + + internal static readonly StorageType Instance = new Sagas(); } /// @@ -52,8 +71,10 @@ internal Sagas() : base("Sagas") /// public sealed class Outbox : StorageType { - internal Outbox() : base("Outbox") + Outbox() : base("Outbox") { } + + internal static readonly StorageType Instance = new Outbox(); } } \ No newline at end of file diff --git a/src/NServiceBus.Core/Persistence/StorageTypeCollectionExtensions.cs b/src/NServiceBus.Core/Persistence/StorageTypeCollectionExtensions.cs new file mode 100644 index 0000000000..65b9e11ebe --- /dev/null +++ b/src/NServiceBus.Core/Persistence/StorageTypeCollectionExtensions.cs @@ -0,0 +1,23 @@ +#nullable enable + +namespace NServiceBus; + +using System.Collections.Generic; +using System.Linq; + +static class StorageTypeCollectionExtensions +{ + extension(IReadOnlyCollection? storageTypes) + { + public bool Contains() where TStorage : StorageType + { + if (storageTypes is null) + { + return false; + } + + var storageType = StorageType.Get(); + return storageTypes.Contains(storageType); + } + } +} \ No newline at end of file diff --git a/src/NServiceBus.Core/Utils/FileVersionRetriever.cs b/src/NServiceBus.Core/Utils/FileVersionRetriever.cs index 9add7cacb9..581bba0cf3 100644 --- a/src/NServiceBus.Core/Utils/FileVersionRetriever.cs +++ b/src/NServiceBus.Core/Utils/FileVersionRetriever.cs @@ -4,7 +4,7 @@ using System.Diagnostics; using System.Reflection; -class FileVersionRetriever +static class FileVersionRetriever { public static string GetFileVersion(Type type) => GetFileVersion(type.Assembly); diff --git a/src/NServiceBus.Core/obsoletes-v10.cs b/src/NServiceBus.Core/obsoletes-v10.cs index 13799a4d2c..9ca7c17ee8 100644 --- a/src/NServiceBus.Core/obsoletes-v10.cs +++ b/src/NServiceBus.Core/obsoletes-v10.cs @@ -7,8 +7,10 @@ namespace NServiceBus using System.Reflection; using System.Text.Json.Serialization; using System.Xml.Serialization; + using Configuration.AdvancedExtensibility; using NServiceBus.DataBus; using Particular.Obsoletes; + using Settings; [ObsoleteMetadata( Message = "The DataBus feature has been released as a dedicated package, 'NServiceBus.ClaimCheck'", @@ -230,6 +232,33 @@ public static DataBusExtensions UseDataBus dataBusFactory, IDataBusSerializer dataBusSerializer) => throw new NotImplementedException(); } + + public static partial class PersistenceConfig + { + [ObsoleteMetadata(ReplacementTypeOrMember = "UsePersistence", RemoveInVersion = "11", + TreatAsErrorFromVersion = "10")] + [Obsolete("Use 'UsePersistence' instead. Will be removed in version 11.0.0.", true)] + public static PersistenceExtensions UsePersistence(this EndpointConfiguration config, Type definitionType) => + throw new NotImplementedException(); + } + + [ObsoleteMetadata(ReplacementTypeOrMember = "PersistenceExtensions", RemoveInVersion = "11", + TreatAsErrorFromVersion = "10")] + [Obsolete("Use 'PersistenceExtensions' instead. Will be removed in version 11.0.0.", true)] + public class PersistenceExtensions : ExposeSettings + { + public PersistenceExtensions(Type definitionType, SettingsHolder settings, Type storageType) + : base(settings) => + throw new NotImplementedException(); + } + + public partial class PersistenceExtensions + { + [ObsoleteMetadata(ReplacementTypeOrMember = "PersistenceExtensions(SettingsHolder settings, StorageType? storageType = null)", RemoveInVersion = "11", + TreatAsErrorFromVersion = "10")] + [Obsolete("Use 'PersistenceExtensions(SettingsHolder settings, StorageType? storageType = null)' instead. Will be removed in version 11.0.0.", true)] + protected PersistenceExtensions(SettingsHolder settings, Type storageType) : base(settings) => throw new NotImplementedException(); + } } namespace NServiceBus.DataBus @@ -375,4 +404,22 @@ public class DataBus } } +namespace NServiceBus.Persistence +{ + using System; + using Particular.Obsoletes; + using Settings; + + public partial class PersistenceDefinition + { + [ObsoleteMetadata(ReplacementTypeOrMember = "Supports()", RemoveInVersion = "11", TreatAsErrorFromVersion = "10")] + [Obsolete("Use 'Supports()' instead. Will be removed in version 11.0.0.", true)] + protected void Supports(Action action) where T : StorageType => throw new NotImplementedException(); + + [ObsoleteMetadata(ReplacementTypeOrMember = "HasSupportFor()", RemoveInVersion = "11", TreatAsErrorFromVersion = "10")] + [Obsolete("Use 'HasSupportFor()' instead. Will be removed in version 11.0.0.", true)] + public bool HasSupportFor(Type storageType) => throw new NotImplementedException(); + } +} + #pragma warning restore CS1591 // Missing XML comment for publicly visible type or member \ No newline at end of file