From 293ea5cab5eaca7a3d74c26e95f36289eaf73c2b Mon Sep 17 00:00:00 2001 From: Sergio Aquilini Date: Sun, 16 Jun 2019 20:43:26 +0200 Subject: [PATCH] RE 0.10 (#31) * Upgrade referenced packages * Added Silverback.EventSourcing / Silverback.EventSourcing.EntityFrameworkCore * Upgrade to Confluent.Kafka 1.0.0 * New icon * Fixed cast issue in QueryPublisher * OutboundQueueWorker scheduler * Testing * Improve error policies (Publish) * Fix disposed IServiceProvider issue in OutboundQueueWorker * Fix AddBackgroundTaskManager ambiguity * Small refactoring and unit tests for ReflectionHelper * OutboundQueueWorkerService leveraging IHostedService * Upgrade to Confluent.Kafka 1.0.1 * Set up CI with Azure Pipelines [skip ci] * Update azure-pipelines.yml for Azure Pipelines Enabled tests * Update azure-pipelines.yml for Azure Pipelines (#29) Added tests * Update README.md Add azure pipeline status badge * Improving logs * Add code coverage to azure pipeline * Enable all tests on CI pipeline * Topic/update low risk nu get packages (#27) * Fix some csproj formatting issues * Update FluentAssertion, Microsoft.NET.Test.Sdk, Microsoft.EntityFrameworkCore.InMemory,NLog and NSubstitute * Improved message unwrapping * Added some summaries * Fix issue #24 - IPublisher couldn't be resolved when applying configuration because it is scoped * Update azure-pipelines.yml for Azure Pipelines Trigger develop * Publish code coverage * Try to fix coverage report * Exclude xunit from code coverage * Feature/deserializer error handling (#30) * Refactored Silverback.Integration to better handle deserializer errors. * Avoid double serialization of byte array (useful when moving a not deserialized message) * Extended Silverback.Examples to handle new policies * Log deserialization errors * Fix duplicated failed attempts header * Make MaxFailedAttempts stack when chained * Improve logs --- .gitignore | 6 + .travis.yml | 12 - README.md | 2 +- Silverback.sln | 37 +- azure-pipelines.yml | 28 + graphics/Icon.png | Bin 3164 -> 1041 bytes graphics/Icon2.psd | Bin 0 -> 48317 bytes nuget/Update.ps1 | 81 ++- .../Data/ExamplesDbContext.cs | 19 +- .../DependencyInjectionHelper.cs | 3 +- .../JobScheduler.cs | 78 --- .../Messages/MessageMoved.cs | 13 + .../Silverback.Examples.Common.csproj | 16 +- .../ConsumerServiceA.cs | 48 +- .../LogHeadersBehavior.cs | 4 +- .../Silverback.Examples.ConsumerA.csproj | 15 +- .../SubscriberService.cs | 6 + .../Silverback.Examples.Main/Menu/MenuItem.cs | 2 +- .../Silverback.Examples.Main.csproj | 19 +- .../MultipleOutboundConnectorsUseCase.cs | 17 - .../EfCore/DeferredOutboundUseCase.cs | 20 +- .../UseCases/EfCore/OutboundWorkerUseCase.cs | 76 +++ .../RetryAndMoveErrorPolicyUseCase.cs | 4 +- .../RetryAndMoveErrorPolicyUseCase2.cs | 57 ++ .../UseCases/UseCase.cs | 45 +- .../src/Silverback.Examples.Main/nlog.config | 2 +- .../Silverback.PerformanceTester.csproj | 2 +- .../src/Baskets.Domain/Baskets.Domain.csproj | 3 - .../Baskets.Infrastructure.csproj | 3 - .../Baskets.Integration.csproj | 3 - .../Baskets.Service/Baskets.Service.csproj | 5 +- .../src/Catalog.Domain/Catalog.Domain.csproj | 3 - .../Catalog.Infrastructure.csproj | 3 - .../Catalog.Integration.csproj | 3 - .../Catalog.Service/Catalog.Service.csproj | 5 +- .../src/Common.Api/Common.Api.csproj | 2 +- .../src/Common.Data/Common.Data.csproj | 3 - .../src/Common.Domain/Common.Domain.csproj | 3 - .../Common.Infrastructure.csproj | 3 - .../DependencyInjectionExtensions.cs | 22 + .../DbContextDistributedLockManager.cs | 157 +++++ .../Background/Model/Lock.cs | 21 + .../DbContextEventsPublisher.cs | 24 +- ...Silverback.Core.EntityFrameworkCore.csproj | 14 +- .../Domain/DomainEntity.cs | 33 +- .../Domain/DomainEntityEventsAccessor.cs | 15 - .../Domain/DomainEvent.cs | 11 +- .../Domain/IAggregateRoot.cs | 1 + .../Domain/IDomainEntity.cs | 19 - .../Domain/IDomainEvent.cs | 4 +- .../Messaging/Messages/ICommand.cs | 1 + .../Messaging/Messages/IEvent.cs | 1 + .../Messaging/Messages/IIntegrationCommand.cs | 1 + .../Messaging/Messages/IIntegrationEvent.cs | 1 + .../Messaging/Messages/IQuery.cs | 1 + .../Messaging/Messages/IRequest.cs | 1 + .../Silverback.Core.Model.csproj | 16 +- .../BusPluginOptionsExtensions.cs | 4 +- .../Silverback.Core.Rx.csproj | 18 +- src/Silverback.Core/AssemblyInfo.cs | 11 - .../DependencyInjectionExtensions.cs | 2 + .../DistributedBackgroundService.cs | 65 ++ .../Background/DistributedLock.cs | 40 ++ .../Background/DistributedLockSettings.cs | 26 + .../Background/IDistributedLockManager.cs | 20 + .../Background/NullLockManager.cs | 22 + .../RecurringDistributedBackgroundService.cs | 50 ++ .../DependencyInjectionExtensions.cs | 2 +- .../Messaging/Messages/IMessage.cs | 1 + .../Messaging/Messages/IMessageWithSource.cs | 10 + .../Messaging/Messages/IMessagesSource.cs | 14 + .../Messaging/Messages/ISilverbackEvent.cs | 1 + .../Messaging/Messages/MessagesSource.cs | 55 ++ .../Messages/TransactionAbortedEvent.cs | 1 + .../Messages/TransactionCompletedEvent.cs | 1 + .../Messages/TransactionStartedEvent.cs | 1 + .../Messaging/Publishing/Publisher.cs | 39 +- .../ISingleMessageArgumentResolver.cs | 1 + .../Messaging/Subscribers/ISubscriber.cs | 1 + .../Messaging/Subscribers/SubscribedMethod.cs | 1 + .../Subscribers/SubscribedMethodInvoker.cs | 8 +- .../Properties/AssemblyInfo.cs | 10 +- src/Silverback.Core/Silverback.Core.csproj | 9 +- .../SilverbackConcurrencyException.cs | 27 + src/Silverback.Core/Util/ReflectionHelper.cs | 3 +- .../DbContextEventStoreRepository.cs | 101 +++ ...k.EventSourcing.EntityFrameworkCore.csproj | 31 + .../Domain/EntityEvent.cs | 17 + .../Domain/EventSourcingDomainEntity.cs | 76 +++ .../Domain/IEntityEvent.cs | 14 + .../Domain/Util/EntityActivator.cs | 29 + .../Domain/Util/EventsApplier.cs | 61 ++ .../Domain/Util/PropertiesMapper.cs | 58 ++ .../EventStore/EventEntity.cs | 16 + .../EventStore/EventSerializer.cs | 24 + .../EventStore/EventStoreEntity.cs | 19 + .../EventStore/EventStoreRepository.cs | 95 +++ .../EventStore/IEventEntity.cs | 16 + .../EventStore/IEventSourcingAggregate.cs | 20 + .../EventStore/IEventStoreEntity.cs | 17 + .../Properties/AssemblyInfo.cs | 6 + .../Silverback.EventSourcing.csproj | 30 + ...ilverback.Integration.Configuration.csproj | 16 +- .../Infrastructure/DefaultSerializer.cs | 2 +- .../BrokerOptionsBuilderExtensions.cs | 33 +- .../Repositories/DbContextInboundLog.cs | 2 +- .../DbContextOutboundQueueConsumer.cs | 23 +- .../Repositories/QueuedOutboundMessage.cs | 1 + ...ack.Integration.EntityFrameworkCore.csproj | 21 +- .../Messaging/Broker/InMemoryBroker.cs | 8 +- .../Messaging/Broker/InMemoryConsumer.cs | 2 +- .../Messaging/Broker/InMemoryProducer.cs | 9 +- .../Silverback.Integration.InMemory.csproj | 16 +- .../Messaging/Broker/KafkaBroker.cs | 2 +- .../Messaging/Broker/KafkaConsumer.cs | 5 +- .../Messaging/Broker/KafkaProducer.cs | 11 +- .../Broker/MessageReceivedHandler.cs | 1 + .../Proxies/ConfluentConsumerConfigProxy.cs | 149 +++-- .../Proxies/ConfluentProducerConfigProxy.cs | 158 ++--- .../Silverback.Integration.Kafka.csproj | 12 +- .../Silverback.Integration.S3.csproj | 20 +- src/Silverback.Integration/AssemblyInfo.cs | 3 +- .../Messaging/Batch/MessageBatch.cs | 48 +- .../Messaging/Broker/Broker.cs | 3 +- .../Messaging/Broker/Consumer.cs | 20 +- .../Messaging/Broker/IBroker.cs | 1 + .../Broker/MessageReceivedEventArgs.cs | 13 +- .../Messaging/Broker/Producer.cs | 21 +- .../Messaging/Broker/RawMessage.cs | 21 + .../Configuration/BrokerOptionsBuilder.cs | 69 +- .../Configuration/ErrorPolicyBuilder.cs | 36 +- .../Connectors/ExactlyOnceInboundConnector.cs | 19 +- .../Connectors/IOutboundQueueWorker.cs | 13 + .../Messaging/Connectors/InboundConnector.cs | 60 +- .../Messaging/Connectors/InboundConsumer.cs | 54 +- .../Connectors/InboundMessageUnwrapper.cs | 14 + .../Connectors/LoggedInboundConnector.cs | 6 +- .../OffsetStoredInboundConnector.cs | 8 +- .../Connectors/OutboundQueueWorker.cs | 62 +- .../Connectors/OutboundQueueWorkerService.cs | 28 + .../Connectors/Repositories/IInboundLog.cs | 1 + .../Repositories/IOutboundQueueConsumer.cs | 7 +- .../Repositories/InMemoryOutboundQueue.cs | 11 +- .../Messaging/ErrorHandling/ErrorAction.cs | 1 + .../ErrorHandling/ErrorHandlerEventArgs.cs | 22 - .../ErrorHandling/ErrorPolicyBase.cs | 106 ++- .../ErrorHandling/ErrorPolicyChain.cs | 31 +- .../ErrorPolicyTryProcessExtension.cs | 56 -- .../Messaging/ErrorHandling/IErrorPolicy.cs | 4 +- .../ErrorHandling/InboundMessageProcessor.cs | 102 +++ .../ErrorHandling/MoveMessageErrorPolicy.cs | 31 +- .../ErrorHandling/RetryErrorPolicy.cs | 16 +- .../ErrorHandling/SkipMessageErrorPolicy.cs | 8 +- .../Messaging/IConsumerEndpoint.cs | 1 + .../LargeMessages/IOffloadStoreCleaner.cs | 1 + .../LargeMessages/IOffloadStoreReader.cs | 1 + .../Messaging/Messages/FailedMessage.cs | 28 +- .../Messaging/Messages/IInboundBatch.cs | 18 + .../Messaging/Messages/IInboundMessage.cs | 36 +- .../Messaging/Messages/IMessageKeyProvider.cs | 1 + .../Messaging/Messages/IOutboundMessage.cs | 10 + .../Messaging/Messages/InboundBatch.cs | 42 ++ .../Messaging/Messages/InboundMessage.cs | 34 +- .../Messages/InboundMessageHelper.cs | 30 + .../Messaging/Messages/MessageHeader.cs | 2 + .../Messages/MessageHeaderCollection.cs | 9 + .../MessageHeaderCollectionExtensions.cs | 29 + .../Messaging/Messages/MessageLogger.cs | 120 +++- .../Messaging/Messages/OutboundMessage.cs | 4 +- .../Serialization/IMessageSerializer.cs | 1 + .../Serialization/JsonMessageSerializer.cs | 3 + .../Serialization/MessageEncoding.cs | 1 + .../Silverback.Integration.csproj | 17 +- ...back.Core.EntityFrameworkCore.Tests.csproj | 20 +- .../TestTypes/Base/Domain/DomainEntity.cs | 34 +- .../Base/Domain/DomainEntityEventsAccessor.cs | 15 - .../TestTypes/Base/Domain/DomainEvent.cs | 4 +- .../TestTypes/Base/Domain/IAggregateRoot.cs | 11 - .../TestTypes/Base/Domain/IDomainEntity.cs | 19 - .../TestTypes/Base/Domain/IDomainEvent.cs | 4 +- .../TestTypes/Base/IQuery.cs | 1 + .../TestTypes/TestAggregateRoot.cs | 6 +- .../TestTypes/TestDbContext.cs | 16 +- .../{EntityTests.cs => DomainEntityTests.cs} | 22 +- .../Publishing/QueryPublisherTests.cs | 34 +- .../Silverback.Core.Model.Tests.csproj | 18 +- .../TestTypes/Domain/TestAggregateRoot.cs | 8 +- .../{Subscribers.cs => QueriesHandler.cs} | 3 + .../Messaging/MessageObservableTests.cs | 4 +- .../Messaging/Publishing/PublisherTests.cs | 8 +- .../Messaging/TypedMessageObservableTests.cs | 4 +- .../Silverback.Core.Rx.Tests.csproj | 14 +- .../TestTypes/Messages/Base/IQuery.cs | 1 + .../DistributedBackgroundServiceTests.cs | 104 +++ ...urringDistributedBackgroundServiceTests.cs | 122 ++++ .../Messaging/Publishing/PublisherTests.cs | 124 +++- .../Publishing/PublisherTestsData.cs | 21 - .../Silverback.Core.Tests.csproj | 18 +- .../TestTypes/AsyncTestingUtil.cs | 36 + .../TestTypes/Background/TestLockManager.cs | 65 ++ .../TestTypes/Messages/Base/IQuery.cs | 1 + .../TestTypes/ParallelTestingUtil.cs | 8 +- .../ExclusiveSubscriberTestService.cs | 1 - .../LimitedParallelSubscriberTestService.cs | 1 - .../NonExclusiveSubscriberTestService.cs | 1 - .../NonParallelSubscriberTestService.cs | 1 - .../ParallelSubscriberTestService.cs | 1 - .../TestRequestReplierReturningNull.cs | 32 + ...TestRequestReplierWithWrongResponseType.cs | 32 + .../Util/ReflectionHelperTests.cs | 55 ++ .../DbContextEventStoreRepositoryTests.cs | 625 ++++++++++++++++++ ...tSourcing.EntityFrameworkCore.Tests.csproj | 30 + .../TestTypes/Person.cs | 65 ++ .../TestTypes/PersonEvent.cs | 14 + .../TestTypes/PersonEventStore.cs | 16 + .../TestTypes/PersonEventStoreRepository.cs | 22 + .../TestTypes/TestDbContext.cs | 16 + .../Domain/EventSourcingDomainEntityTests.cs | 258 ++++++++ .../Domain/Util/EntityActivatorTests.cs | 68 ++ .../Util/EntityReflectionHelperTests.cs | 81 +++ .../Domain/Util/PropertiesMapperTests.cs | 59 ++ .../EventStore/EventStoreRepositoryTests.cs | 437 ++++++++++++ .../Silverback.EventSourcing.Tests.csproj | 29 + .../TestTypes/InMemoryEventStore.cs | 25 + .../TestTypes/Person.cs | 77 +++ .../TestTypes/PersonEventStore.cs | 18 + .../TestTypes/PersonEventStoreRepository.cs | 49 ++ .../Configuration/ConfigurationReaderTests.cs | 21 +- ...ack.Integration.Configuration.Tests.csproj | 16 +- .../Types/FakeSerializerSettings.cs | 1 + .../Types/IIntegrationCommand.cs | 1 + .../Types/IIntegrationEvent.cs | 1 + .../Messaging/Broker/InMemoryBrokerTests.cs | 26 +- ...lverback.Integration.InMemory.Tests.csproj | 18 +- .../Program.cs | 2 +- ...back.Integration.Kafka.TestConsumer.csproj | 21 +- ...back.Integration.Kafka.TestProducer.csproj | 13 +- .../Silverback.Integration.Kafka.Tests.csproj | 15 +- .../DependencyInjectionExtensionsTests.cs | 30 +- .../DeferredOutboundConnectorTests.cs | 2 +- .../Connectors/InboundConnectorTests.cs | 151 ++++- .../Connectors/LoggedInboundConnectorTests.cs | 1 - .../OffsetStoredInboundConnectorTests.cs | 14 +- .../OutboundConnectorRouterTests.cs | 17 +- .../OutboundConnectorRouterTestsData.cs | 21 - .../Connectors/OutboundQueueWorkerTests.cs | 46 +- .../InMemoryOutboundQueueTests.cs | 40 +- .../ErrorHandling/ErrorPolicyBaseTests.cs | 88 ++- .../ErrorHandling/ErrorPolicyBaseTestsData.cs | 63 -- .../ErrorHandling/ErrorPolicyChainTests.cs | 39 +- .../MoveMessageErrorPolicyTests.cs | 82 ++- .../ErrorHandling/RetryErrorPolicyTests.cs | 30 +- .../SkipMessageErrorPolicyTests.cs | 4 +- .../Messages/InboundMessageHelperTests.cs | 49 ++ .../Messages/MessageHeaderCollectionTests.cs | 57 ++ .../Messaging/Publishing/PublisherTests.cs | 66 ++ .../JsonMessageSerializerTests.cs | 12 + .../Silverback.Integration.Tests.csproj | 15 +- .../TestTypes/Domain/IEvent.cs | 1 + .../TestTypes/Domain/IIntegrationEvent.cs | 1 + .../TestTypes/Domain/TestInternalEventOne.cs | 1 + .../TestTypes/InMemoryChunkStore.cs | 21 +- .../TestTypes/InMemoryStoredChunk.cs | 1 + .../TestTypes/TestConsumer.cs | 3 +- .../TestTypes/TestErrorPolicy.cs | 4 +- .../TestTypes/TestProducer.cs | 7 +- .../TestTypes/TestSerializer.cs | 34 + .../Messaging/KafkaConsumerConfig.cs | 3 +- .../Program.cs | 8 +- .../ProxyClassGenerator.cs | 4 +- ...egration.Kafka.ConfigClassGenerator.csproj | 2 +- 271 files changed, 6252 insertions(+), 1399 deletions(-) delete mode 100644 .travis.yml create mode 100644 azure-pipelines.yml create mode 100644 graphics/Icon2.psd delete mode 100644 samples/Examples/src/Silverback.Examples.Common/JobScheduler.cs create mode 100644 samples/Examples/src/Silverback.Examples.Common/Messages/MessageMoved.cs create mode 100644 samples/Examples/src/Silverback.Examples.Main/UseCases/EfCore/OutboundWorkerUseCase.cs create mode 100644 samples/Examples/src/Silverback.Examples.Main/UseCases/ErrorHandling/RetryAndMoveErrorPolicyUseCase2.cs create mode 100644 src/Silverback.Core.EntityFrameworkCore/Background/Configuration/DependencyInjectionExtensions.cs create mode 100644 src/Silverback.Core.EntityFrameworkCore/Background/DbContextDistributedLockManager.cs create mode 100644 src/Silverback.Core.EntityFrameworkCore/Background/Model/Lock.cs rename src/Silverback.Core.EntityFrameworkCore/{ => EntityFrameworkCore}/DbContextEventsPublisher.cs (80%) delete mode 100644 src/Silverback.Core.Model/Domain/DomainEntityEventsAccessor.cs delete mode 100644 src/Silverback.Core.Model/Domain/IDomainEntity.cs delete mode 100644 src/Silverback.Core/AssemblyInfo.cs create mode 100644 src/Silverback.Core/Background/Configuration/DependencyInjectionExtensions.cs create mode 100644 src/Silverback.Core/Background/DistributedBackgroundService.cs create mode 100644 src/Silverback.Core/Background/DistributedLock.cs create mode 100644 src/Silverback.Core/Background/DistributedLockSettings.cs create mode 100644 src/Silverback.Core/Background/IDistributedLockManager.cs create mode 100644 src/Silverback.Core/Background/NullLockManager.cs create mode 100644 src/Silverback.Core/Background/RecurringDistributedBackgroundService.cs create mode 100644 src/Silverback.Core/Messaging/Messages/IMessageWithSource.cs create mode 100644 src/Silverback.Core/Messaging/Messages/IMessagesSource.cs create mode 100644 src/Silverback.Core/Messaging/Messages/MessagesSource.cs create mode 100644 src/Silverback.Core/SilverbackConcurrencyException.cs create mode 100644 src/Silverback.EventSourcing.EntityFrameworkCore/EventStore/DbContextEventStoreRepository.cs create mode 100644 src/Silverback.EventSourcing.EntityFrameworkCore/Silverback.EventSourcing.EntityFrameworkCore.csproj create mode 100644 src/Silverback.EventSourcing/Domain/EntityEvent.cs create mode 100644 src/Silverback.EventSourcing/Domain/EventSourcingDomainEntity.cs create mode 100644 src/Silverback.EventSourcing/Domain/IEntityEvent.cs create mode 100644 src/Silverback.EventSourcing/Domain/Util/EntityActivator.cs create mode 100644 src/Silverback.EventSourcing/Domain/Util/EventsApplier.cs create mode 100644 src/Silverback.EventSourcing/Domain/Util/PropertiesMapper.cs create mode 100644 src/Silverback.EventSourcing/EventStore/EventEntity.cs create mode 100644 src/Silverback.EventSourcing/EventStore/EventSerializer.cs create mode 100644 src/Silverback.EventSourcing/EventStore/EventStoreEntity.cs create mode 100644 src/Silverback.EventSourcing/EventStore/EventStoreRepository.cs create mode 100644 src/Silverback.EventSourcing/EventStore/IEventEntity.cs create mode 100644 src/Silverback.EventSourcing/EventStore/IEventSourcingAggregate.cs create mode 100644 src/Silverback.EventSourcing/EventStore/IEventStoreEntity.cs create mode 100644 src/Silverback.EventSourcing/Properties/AssemblyInfo.cs create mode 100644 src/Silverback.EventSourcing/Silverback.EventSourcing.csproj create mode 100644 src/Silverback.Integration/Messaging/Broker/RawMessage.cs create mode 100644 src/Silverback.Integration/Messaging/Connectors/IOutboundQueueWorker.cs create mode 100644 src/Silverback.Integration/Messaging/Connectors/InboundMessageUnwrapper.cs create mode 100644 src/Silverback.Integration/Messaging/Connectors/OutboundQueueWorkerService.cs delete mode 100644 src/Silverback.Integration/Messaging/ErrorHandling/ErrorHandlerEventArgs.cs delete mode 100644 src/Silverback.Integration/Messaging/ErrorHandling/ErrorPolicyTryProcessExtension.cs create mode 100644 src/Silverback.Integration/Messaging/ErrorHandling/InboundMessageProcessor.cs create mode 100644 src/Silverback.Integration/Messaging/Messages/IInboundBatch.cs create mode 100644 src/Silverback.Integration/Messaging/Messages/InboundBatch.cs create mode 100644 src/Silverback.Integration/Messaging/Messages/InboundMessageHelper.cs create mode 100644 src/Silverback.Integration/Messaging/Messages/MessageHeaderCollectionExtensions.cs delete mode 100644 tests/Silverback.Core.EntityFrameworkCore.Tests/TestTypes/Base/Domain/DomainEntityEventsAccessor.cs delete mode 100644 tests/Silverback.Core.EntityFrameworkCore.Tests/TestTypes/Base/Domain/IAggregateRoot.cs delete mode 100644 tests/Silverback.Core.EntityFrameworkCore.Tests/TestTypes/Base/Domain/IDomainEntity.cs rename tests/Silverback.Core.Model.Tests/Domain/{EntityTests.cs => DomainEntityTests.cs} (69%) rename tests/Silverback.Core.Model.Tests/TestTypes/Subscribers/{Subscribers.cs => QueriesHandler.cs} (84%) create mode 100644 tests/Silverback.Core.Tests/Background/DistributedBackgroundServiceTests.cs create mode 100644 tests/Silverback.Core.Tests/Background/RecurringDistributedBackgroundServiceTests.cs delete mode 100644 tests/Silverback.Core.Tests/Messaging/Publishing/PublisherTestsData.cs create mode 100644 tests/Silverback.Core.Tests/TestTypes/AsyncTestingUtil.cs create mode 100644 tests/Silverback.Core.Tests/TestTypes/Background/TestLockManager.cs create mode 100644 tests/Silverback.Core.Tests/TestTypes/Subscribers/TestRequestReplierReturningNull.cs create mode 100644 tests/Silverback.Core.Tests/TestTypes/Subscribers/TestRequestReplierWithWrongResponseType.cs create mode 100644 tests/Silverback.Core.Tests/Util/ReflectionHelperTests.cs create mode 100644 tests/Silverback.EventSourcing.EntityFrameworkCore.Tests/EventStore/DbContextEventStoreRepositoryTests.cs create mode 100644 tests/Silverback.EventSourcing.EntityFrameworkCore.Tests/Silverback.EventSourcing.EntityFrameworkCore.Tests.csproj create mode 100644 tests/Silverback.EventSourcing.EntityFrameworkCore.Tests/TestTypes/Person.cs create mode 100644 tests/Silverback.EventSourcing.EntityFrameworkCore.Tests/TestTypes/PersonEvent.cs create mode 100644 tests/Silverback.EventSourcing.EntityFrameworkCore.Tests/TestTypes/PersonEventStore.cs create mode 100644 tests/Silverback.EventSourcing.EntityFrameworkCore.Tests/TestTypes/PersonEventStoreRepository.cs create mode 100644 tests/Silverback.EventSourcing.EntityFrameworkCore.Tests/TestTypes/TestDbContext.cs create mode 100644 tests/Silverback.EventSourcing.Tests/Domain/EventSourcingDomainEntityTests.cs create mode 100644 tests/Silverback.EventSourcing.Tests/Domain/Util/EntityActivatorTests.cs create mode 100644 tests/Silverback.EventSourcing.Tests/Domain/Util/EntityReflectionHelperTests.cs create mode 100644 tests/Silverback.EventSourcing.Tests/Domain/Util/PropertiesMapperTests.cs create mode 100644 tests/Silverback.EventSourcing.Tests/EventStore/EventStoreRepositoryTests.cs create mode 100644 tests/Silverback.EventSourcing.Tests/Silverback.EventSourcing.Tests.csproj create mode 100644 tests/Silverback.EventSourcing.Tests/TestTypes/InMemoryEventStore.cs create mode 100644 tests/Silverback.EventSourcing.Tests/TestTypes/Person.cs create mode 100644 tests/Silverback.EventSourcing.Tests/TestTypes/PersonEventStore.cs create mode 100644 tests/Silverback.EventSourcing.Tests/TestTypes/PersonEventStoreRepository.cs delete mode 100644 tests/Silverback.Integration.Tests/Messaging/Connectors/OutboundConnectorRouterTestsData.cs delete mode 100644 tests/Silverback.Integration.Tests/Messaging/ErrorHandling/ErrorPolicyBaseTestsData.cs create mode 100644 tests/Silverback.Integration.Tests/Messaging/Messages/InboundMessageHelperTests.cs create mode 100644 tests/Silverback.Integration.Tests/Messaging/Messages/MessageHeaderCollectionTests.cs create mode 100644 tests/Silverback.Integration.Tests/Messaging/Publishing/PublisherTests.cs create mode 100644 tests/Silverback.Integration.Tests/TestTypes/TestSerializer.cs diff --git a/.gitignore b/.gitignore index 41cb108bb..95854ab69 100644 --- a/.gitignore +++ b/.gitignore @@ -329,6 +329,12 @@ __pycache__/ *.odx.cs *.xsd.cs +# Nuget local report +nuget/ + +# Coverage report +/tests/**/coverage.cobertura.xml + # OpenCover UI analysis results OpenCover/ diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index fb8af35cd..000000000 --- a/.travis.yml +++ /dev/null @@ -1,12 +0,0 @@ -language: csharp -mono: none -sudo: required -dist: xenial -dotnet: 2.2 -script: - - dotnet restore Silverback.sln - - dotnet build --no-restore Silverback.sln - - dotnet test --no-build --filter CI!=false Silverback.sln -global: - - DOTNET_SKIP_FIRST_TIME_EXPERIENCE=true - - DOTNET_CLI_TELEMETRY_OPTOUT=1 \ No newline at end of file diff --git a/README.md b/README.md index 0273a5a57..dd93595ee 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # Silverback -[![Build Status](https://travis-ci.com/BEagle1984/silverback.svg?branch=develop)](https://travis-ci.com/BEagle1984/silverback) +[![Build Status](https://dev.azure.com/beagle1984/Silverback/_apis/build/status/BEagle1984.silverback?branchName=develop)](https://dev.azure.com/beagle1984/Silverback/_build/latest?definitionId=2&branchName=develop) [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/BEagle1984/silverback/blob/master/LICENSE) Silverback is a simple framework to build reactive, event-driven, microservices. diff --git a/Silverback.sln b/Silverback.sln index 1dc370d07..464bc72da 100644 --- a/Silverback.sln +++ b/Silverback.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.27130.2026 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29009.5 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Silverback.Core", "src\Silverback.Core\Silverback.Core.csproj", "{FE7472E6-E52F-4352-82AD-AFB700397B20}" EndProject @@ -45,15 +45,18 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tools", "Tools", "{9818A770 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Silverback.Integration.Kafka.ConfigClassGenerator", "tools\Silverback.Integration.Kafka.ConfigClassGenerator\Silverback.Integration.Kafka.ConfigClassGenerator.csproj", "{228CC403-4FBE-4E4F-9B74-891F30A404E0}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{1348F753-96CC-4A2E-A7D4-F5C0BE4C8BA1}" - ProjectSection(SolutionItems) = preProject - .travis.yml = .travis.yml - EndProjectSection -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Silverback.Integration.InMemory", "src\Silverback.Integration.InMemory\Silverback.Integration.InMemory.csproj", "{7918D431-D931-4353-841B-4E6098D1CF70}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Silverback.Integration.InMemory", "src\Silverback.Integration.InMemory\Silverback.Integration.InMemory.csproj", "{7918D431-D931-4353-841B-4E6098D1CF70}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Silverback.Integration.InMemory.Tests", "tests\Silverback.Integration.InMemory.Tests\Silverback.Integration.InMemory.Tests.csproj", "{837AA9C2-1682-4B0C-8B4D-0CBE42281E48}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Silverback.EventSourcing", "src\Silverback.EventSourcing\Silverback.EventSourcing.csproj", "{2DB57B2C-26CF-4635-AE66-972981DA77D9}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Silverback.EventSourcing.Tests", "tests\Silverback.EventSourcing.Tests\Silverback.EventSourcing.Tests.csproj", "{AB4EC3F2-A116-439E-B715-78ED415A96CC}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Silverback.EventSourcing.EntityFrameworkCore", "src\Silverback.EventSourcing.EntityFrameworkCore\Silverback.EventSourcing.EntityFrameworkCore.csproj", "{9AD7ECBA-BF16-4FA1-BEB3-AE7FCEFC3C7B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Silverback.EventSourcing.EntityFrameworkCore.Tests", "tests\Silverback.EventSourcing.EntityFrameworkCore.Tests\Silverback.EventSourcing.EntityFrameworkCore.Tests.csproj", "{C1A5F0B5-5E48-44E6-AE01-86FAFE676630}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -144,6 +147,22 @@ Global {837AA9C2-1682-4B0C-8B4D-0CBE42281E48}.Debug|Any CPU.Build.0 = Debug|Any CPU {837AA9C2-1682-4B0C-8B4D-0CBE42281E48}.Release|Any CPU.ActiveCfg = Release|Any CPU {837AA9C2-1682-4B0C-8B4D-0CBE42281E48}.Release|Any CPU.Build.0 = Release|Any CPU + {2DB57B2C-26CF-4635-AE66-972981DA77D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2DB57B2C-26CF-4635-AE66-972981DA77D9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2DB57B2C-26CF-4635-AE66-972981DA77D9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2DB57B2C-26CF-4635-AE66-972981DA77D9}.Release|Any CPU.Build.0 = Release|Any CPU + {AB4EC3F2-A116-439E-B715-78ED415A96CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AB4EC3F2-A116-439E-B715-78ED415A96CC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AB4EC3F2-A116-439E-B715-78ED415A96CC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AB4EC3F2-A116-439E-B715-78ED415A96CC}.Release|Any CPU.Build.0 = Release|Any CPU + {9AD7ECBA-BF16-4FA1-BEB3-AE7FCEFC3C7B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9AD7ECBA-BF16-4FA1-BEB3-AE7FCEFC3C7B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9AD7ECBA-BF16-4FA1-BEB3-AE7FCEFC3C7B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9AD7ECBA-BF16-4FA1-BEB3-AE7FCEFC3C7B}.Release|Any CPU.Build.0 = Release|Any CPU + {C1A5F0B5-5E48-44E6-AE01-86FAFE676630}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C1A5F0B5-5E48-44E6-AE01-86FAFE676630}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C1A5F0B5-5E48-44E6-AE01-86FAFE676630}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C1A5F0B5-5E48-44E6-AE01-86FAFE676630}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -160,6 +179,8 @@ Global {BEEEAE81-0CE8-48E9-AE58-D76C8C6C58C4} = {6AFB87A3-501B-4A87-B94C-205AEB118D97} {228CC403-4FBE-4E4F-9B74-891F30A404E0} = {9818A770-01ED-4340-BD4A-CFDDA6C56168} {837AA9C2-1682-4B0C-8B4D-0CBE42281E48} = {6AFB87A3-501B-4A87-B94C-205AEB118D97} + {AB4EC3F2-A116-439E-B715-78ED415A96CC} = {6AFB87A3-501B-4A87-B94C-205AEB118D97} + {C1A5F0B5-5E48-44E6-AE01-86FAFE676630} = {6AFB87A3-501B-4A87-B94C-205AEB118D97} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {89DA4AF5-B982-42AD-A829-8A72BCB21227} diff --git a/azure-pipelines.yml b/azure-pipelines.yml new file mode 100644 index 000000000..3d19a4546 --- /dev/null +++ b/azure-pipelines.yml @@ -0,0 +1,28 @@ +# ASP.NET Core +# Build and test ASP.NET Core projects targeting .NET Core. +# Add steps that run tests, create a NuGet package, deploy, and more: +# https://docs.microsoft.com/azure/devops/pipelines/languages/dotnet-core + +trigger: +- master +- develop + +pool: + vmImage: 'ubuntu-latest' + +variables: + buildConfiguration: 'Release' + +steps: +- script: dotnet build Silverback.sln --configuration $(buildConfiguration) + displayName: 'dotnet build $(buildConfiguration)' +- script: dotnet test Silverback.sln --logger trx --collect "Code coverage" --filter CI!=false /p:CollectCoverage=true /p:CoverletOutputFormat=cobertura /p:Exclude="[xunit.*]*" + displayName: 'dotnet test' +- task: PublishTestResults@2 + inputs: + testRunner: VSTest + testResultsFiles: '**/*.trx' +- task: PublishCodeCoverageResults@1 + inputs: + summaryFileLocation: $(System.DefaultWorkingDirectory)/**/coverage.cobertura.xml + codecoverageTool: cobertura \ No newline at end of file diff --git a/graphics/Icon.png b/graphics/Icon.png index e17a5d0c89f8a178f754b7aea3b1c0bc8355f309..5e25b6f36d640659f3326f3e300722d355f841d0 100644 GIT binary patch delta 1020 zcmV>6Ap#H6r>8qE6AN1)&ZRbX$V0Janii%IGFV1Z#L(@X)3A5WA?kKb%>c*P0Im zu8!Y%=DV{q&m$AS;cz&d-%E?x2dAf})o*| zU~fRh;1?GcX#)B-Ha9oRFF#;mf|D80Gy~Vs3)8V!833U9?tVE= zVz)Kmr^5|k9RO>%0jvXH4L5*w0IcB#unvGVzuo{x#gG}nU=Y!06oMdFwksBkI6FH- zE|)WHlgVVz+ke|@+oz%^I6OQ=Hk++G-dYgP0Keal@$qrn!2^KT>&48>4Ep=~NxL(M zL?YPQ+QQ`IB()YmVZiY4aNXY-9UUd@?h6|m8|drnqt+pQ1X^2Ljn|SSA)n8iwq0Ic zhA4{gdcBUc1A#!z_ru{ZR#sNf)zt+6NTpI(T3V_j>VK~g&w%IWXXEws^fW|KG;Moz zbp-&HCOILS&0>Cj9;H$V05C8xK&1yz7;t`mj@Q>$0Dvfpn46okY`ea`4oQ+oyE7;h z3V3>Y0szM|$_lD&aDRUvnx+}S%+AhI2lU-g8K7wzGMNnC-`@cMJv}{$#bT}mW`@t_ zLw9#KfPc!zkQC}fNL5uFA0Hdnp->3HV31a}CgE@xJ3Bk@csu}rySqDTO{zplCzs2i zy}ccwPzV4JjYjeE@&Z*=iS%x7Z*AL@N+oP>ZxiX4<60^#%Q7Ax9{~V@ARv)QSndq8 zNRos?p+K$UYAtYZaDZa52;g&Ph{xk@%2$v~CV%1c`KWbVFNXH^_KcmOqoV^;Q&V+o z1OkDYf9Z7ESol014;B^{sN;3r08P_ybaZ6w3`0Xh7#SIHMY}o?8XO#?)@(>QoX_WR za&lr^MDl8=SO>rwZUE~5SYvB|lk>L!E3{{YEsvQu8FZ(`btjwU+;Ba!*3N(yl%2G0(#L`7 qA12BLn(g@fPkMwo91e%m5dHyKgCB^f7&-703U(oZY3Cr1)iuA}LYU2PH?bCBsIP zk3--(fZ>Y|PLSkepuGe|59y)ksi(y4sVI6V&_jWuMqI#XiXMXG;DZ9h2_QpAVneZ$ zz=|wVqG*beNG?S#xw~AhdXQ(kT2a!=U>1At)|64imsh z02g_le=QIQob&m7Zc!BBcDupzJUEU6%d!9f!!XcnHlb-66h%S3UPrZBRU}FJOw+Va z0o(*||1bu57=Hj>1n_1s7=@vMi%gsi0ggqgX6v%H{HZ0Q?)k^wS*d(*%$J@QYwD`15EqIv9yW5DJCh z_xs`V`Cy%WLI_+g*A@V+RtvhW+r0plN(H4-39GBCD1Q_R56k89H30trkb4?~ep&!7 zavb-oo}Qjd@pv3vU0n!=!w3Wdrt$fF9)u9AtgIlL&9;5X;NTzvfdF`(hu`lvAxM&h zVzG#ol@&aC^ax8!OCL9z&A(!EX&gvT1He0hK;SpYWO6u>NFW-GA`*!p5C}k)Wvs5Q zVs>^GhJRsfevo0nah#(UpEz*>u~-ZY!$6WG>IKN)$E|&{}AmGG_6F7J791@8H zfX!vFy1I(R#YLpkY0S>fnjaY)9K^`T2x_$&R)1GlvAn#D#l=M|EG$$Cg~A^I{P9T* za2>?>?(XiZLqkKJfq?<^^zjeKPG^8qS5Hpp`js9e}6xEdU_Cx#V|KFhr4(0qE@S+R;!uDQ!k*)fMr=+ zx^$`SizyI72$IPp#>U1_C=^gE7Lm*4Fg`vGx7!Vu%S8hz(KKyBmgN$FuYU{x7XyL7 z`-6jn2|DxL-QCD!GMJf}L9JFZXTH&Bn19B%S}kmBYybcVAs8PYhu`nt@go$3AP5*6 z8$-2PMX6N6*w`34IyyiI0n4&r7zT!6xMf)$Q50nkz}#U1NN^nY-q6s{E7bT{EQa;< zb=FYes+9(e9sTkXHuyYCMG7#*{AV@Le+IWTq>3PhJRsv z4xoBi0Dj%q*Z0eTfdM3wNyOuEgu`J>O-*5KZ4I(4qtR$=G2So?bCA?{LI_q@SJBng zg?K#f7=krk(Aj_N0W^mgktC@M;Pb-(@M16+ygD#25K1PKNF)-7L?WnED){QFueP~; zv)M$e)!GJt4&LQ*;fEi7!1K>PkAKe2PDcZREb8ynV{f~u-$G#a>e?Ha08#~y3K<2Vj(w;Nus z7lA+kkw^qxU0sMqqk{n6boxH09&j=k3|{T;?+^6$_97OGAruOsv$GTT?|W*SYQR;$&xq9~sNSlR16PCeja zI2=A63Wdy>7ex^~&!bc-!7z+1Gj5%As|>@~mh;Q9jJdfveDJ{sP*rtb#=BfD@H`Ju z6irVEg+d62!^Z(!bn-4o0DpL%e~ngkG`AN70fZ2EJf1zDw}P=Q14U8LXf#l-*D*IY zhZ{F;IEFx%0wDx~AV3sF_Nt*r%jH5S6tX5I zTU?X=wL-9a0d-qhmhsIu-{7NTGFyC zi&!jX0-*nBq_?Xb1XWc{PmpC9)6>)V~^~$2!DcUyfqCycI+5VpFVB2x#<7517U?gn;@-LYg;?4R;%IW&6~J$ z=Z+%))C(vCK@i|}yFmyUJ}>}55c=%KQNyTSdg&zyf?z_jdjcKkUJ&fd;lmF<#CP9) zw{HlvS`-AK@4x^EA>Gt?>-x4Dlt?6S_Uu`>-EMQ4SmT7g*XNg+K`DJ}>~5W&KuH-pQB?7cQWqqXQis9ou4sb%(GUPer#!t0&Or==JN@ z9c7Dc>V=)lO{V~y%IEXp^5x4Wgq@bdPD{a>BiJDn3Wa?oO8e2C01U&Zw9E3^>CBlk zxNzYDL{Z%034hk@aW4q=phy#?bUKa6$;n;EYKLhUM&-Z&bX{Mz7dh4n&#v?5&*S9D zlUvRK?D4>LEVzC%J9*-Fl z=x*Y~6X^D6g`lb`*4EbW<(FUXXgvL`q9}NL&Ui2Y0PfZ6^=;iSnx^5~Z@+C@UDBI$I#*J;;sitWFU=~L9!hebO{H#@EtJOlORN7KeT8%H4%XoaO zzNeffLN|-WVy09oL6Ri%&_mO-U0Yvv2$wHkM!T|lw;aI=!>W3{j#jG$P1DQ*N|Gd$ zN+lGF#S9!CoI33Zt|*GqD~jSPb_485&Ti`M?S-akSXx>#XVm)FZltyKMe}=F=_L{g z41W&~qh7B=k|eCGtRR=mVPRnbOG`_C2k_6Q2><{ptyb%2q9}&kZnqiLoz5WD@bEAy zl?s-Zm)rIdtu1t#)6=dGHC_}&q*5vKP-bmy4Y^zn*=!b>Oy;4YC|BXIExsQBIYm)A z2_fh0x&B^zyIn&=LzthR$NKuZxvnV;Du4P8Yu}G1RvwQBXV0Dm$8jhYi^%8m$Y!%h zr_;#i^M3|#!>LjGJsEl^%W_H(gb^CaZ{NO+Y&MJD-d=DVw_{JxDnbZGM@KO;GXq(c z&A>*NgVptEZBM<<tvtyWPkm$A0ChJk^BT^plT34(w`B7y1YX)~v=UZQB%=JI$v@Or&?;e{8# zah!QsBArfSetv$oUawyTaCbijJ;~|toT4b2uIsP5TrPJw9EPGOs8lLgSXe-JcQ<&R z-v*!^h;TTJQmJGHMZ59VHBXICrGHZB@9#H_&tx)4r_+^UvG_iKf9=6_0l9VWlkR)l#4_eMN7$6#3&4<0O43aKXy4GqET^@8KLpHciZIjXi7B}Gv*jmqcqJ3c%zG6KUeKnOucN5}u&MM7FB#Nq~@xgb0C{1P~B)GSf*0CNpsk5>VF*(N%Xn zR|QYd^<3ksE8dEDpew85h4-Q^-XeeZ`P zSC6%{rjBR``{?0TOuval$3XL^Sxc*`^%z6Q;NhCZM0ditO69I41pWz4GmfU|zh&v* zo&HDMogY3^aP6kASDs4a#$w#m$ML*XdHaXqXvwwK<*=)|~n4Y~buUe>X zEfbravsQVa-Q?W>mC3mn-qijt?b7q+*wwupWk*WTiC`wH8qXWJZ}2C)#r2cW2ypG1g+VTpiICz?#-R$?b7V>;sTU&0MDc9p|;td4_1-xM7jm8{Ek>gwG z_S@QX+`cgkiHxUQ^x3`6R=?BZ<`A!~!4vRL&(6kzl#j$$ORG|l+m}mu%(Z)3_;y<> zZ^#w+NSPdVB~NR>>!PGN?7Zj_TST|t2l)&U^MOdKtCg$~i)(3#nA7Kp7pBxX|f#c-nlErtw{~glZs!O)R(h#UjXQ%`q5qjCu8jyh6cJD43_9 zst771EM*rQ)u4?Io8Km5DaaAbIVPdrU??FT|mbNP%kHguxGAb;glqwt! zdtsx;+hX$<*;-p&PP+}&k6+<-NJ^y$b{Zc_s>)B5qp-^D^V{5Zv8ucX0J%=5qp-ki zH{~~q)*O?ypfShlFgtQ=LcWk=ZD=qX8qJnQTb_dosk&O7y&LLsk3E3;DWP@%YP&7Z z)?l&Qa_oZ5lw-~}=jRmI%|cFr)glUc_5!0ISY_0z?A=h$^E$z{*j%bKNPS<6h=8VB zQL~&r(7%-`y#(!~)}u~5GaM>MxSZ4qwA#Er5gW<$Y`K*vW<#+Gn1?pDLYo~cu*i-I zFM@!>pdqr9oKiMt_X>4(%lR8rgwM9(msNvc7ajKeJTb>&E->Zf7nmDztj31?9D`kKC@`9hqS?_P zSMzaKgSF6Pk?q#MuNp#HY;U%?n?y$uFLNdX{y)`RbQbV98cI`6;|>{>UbhLBknT)6-opEQiX~rJ`X_aHPS9=M^Wuo75Cme$7BvPJd0@3a(fo ziYTH9#KaYil$10vaVesRCJ+->G*VL1#KfhDBAP%ysBB_&Nv zT#6{73B<$|jg*u$F>xuPh$avdS2R*m(!|82h$5OmOkB}ONl6nEmm-R20x@w#BPAtG zOk9d6q6x&r6^)dXG%;~0qKGCC6IV1+Qqsi4rHCS$KulcGNJ&W(6PF^2XaX^DMI$98 zO-x*hD543(#1)N{lr%AMDWZrb5EEB4Qc}{y#HENLnm|lk(MU;26BCyrif95caYZ8~ zB~46RiYTH9#KaYil$10vaVesRCJ+->G*VL1#KfhDBAP%GsbH_*(;h0LEdG8lS(e0S-aZ6jg4& zD7phJ7=dBDm$|(7+>hq1bGBoOQl}sDU^K%idhzH%{mfJ90mKSp8Q?61r?F1-2U_Pf zEVDyoDybn};(@65e%4qQSU4^VMDkDjCTLHuA$S=Q=z%S-^14PND;w#iqmJd}CeWuXVRKlCuC z!R1q8<~I3PC=WART;<9G$T5Vv)V{n)5(JYHfios$Kn*=IUF+aDhbPcb?Ad`7(odz= z-3?ai>YlFD>!_da_E(Ilbz$8qpHi2Div+Lrx%?Df)9xzvZj#d{uMq8UED=tR!dYWU zD{7i*e6m(x7}LTX$tH8eMuw(KvGtzTh{e{~T?}61wHGf!4D{3A?)9`TgwudEsG&}G zlPtb;jHm^gNvmCCT57%5=JvsvOwny$$uw~wjTs5oG*S=RZX<5u10A

_-5P>zO@dII`{9{j zTMkD2=)S_}h*7~`5=Ui$sPTmY8v2AU7LaLUvJ@ypr_o5VMcOe1HbW{AeIkY6+Xeup zEoNT=(x)v|gSP~TgbOPJ>z81>RF}grq(jLtnc$=RP6QUFh*(g$s$s{;81u7aFmRsW5F2d)YWR~@h+cn$B1nCKN6)82PkgMKS#Rt?9g;-U#=*jv5Oy=rP8vB?2%=Ya5P2)o++ ztq4b|vl^CTSc^?A%L@cTcnF4@SQvYmtOgb?#;~Ks4GOMBnp+($4h+8l;Y(HoQ1Nwh zAiQRUQ*49q+Ylb>3bZ&OjMg@*MYKU<)T7MC`bB#)gartvdh2VUf7VYUBr&x~6>d<4 z{h)!6mXvu~SJFD+CfX-)@M;n(H%n}DiGF`h4ZLy1=5@gPP+D4TZs@fb&zOr0=ypDq zg4w0lNL9Mtf>^gKe$UCY&NPFzhG3zbA(&?a*kb6}bXSI84QmN`^ddrrd=P?-y$u}N z)r4$(UB%BJ)ECvOTAZRi7X_+(j+sD7*>gZuUM`lWoGs?cSrg_^tL<=fIpF2MtFVh) zPFE#9?u;t3RCJtJD>jPgy@=cb&@ynopt{`-Cw=RT(;apKT7}^_v#Bs(km)r9GVeG_ zvQEz>{a?=_y6?9VeP#;Loq7wzXq0-Jle7Rv4}>fn^(E7L2tz!*hF!E*z@ygZq}>83 ztF7nk0q+WyM31*P@Q3=5EHadgBxA@#@MMf6pG<|`cP2TB%mc4&5jhR~v}V!*K3f|( zo19P9kju!G;95L_1qsuU)Eb(w?bZrCqK4qxJ^v2JK(8 z8@11AU)8>&-J$(f+o>bE6x|@*XkCuZsw>mY(=FCD>b$z&=`Ph>r`w?G&^@8sqI*lX zL-(ETfIeQIsps@L`lF?HW)W4vAOaG~UxBh5cTHNrsDRG5y zRdJ`pHN~~Xt%@uF|i_XaiTl%g2d|* z?@fFr@y*096AvZzPa2n0m^3fRk+d@DiljS|o=AERqXuQr}PA zla`h?KCL)yahgBvk7@U$J)ibT+Wz#+^xX8SbVvHR={KftOn)wjba$NO*Z-ZtR8UtfaeE%o~g?mlUbH&&%7}6j?5P`cV@+BO~|UsT9$QL*8N$pW$hj~V4!*6 zf`RP=Zyfmaz#W5hgT@c48sr*u#h`}ZN#m^Jn#Wx~Zp*lR<0p(?IR4`CkB_n6W^W`Kgl|2`J|gCy)jukId5|F9BmAKQ{l2{G0N(TeGY+ z*2}Fg7sM5m7Mxq~RKbBlOQFB;p~4@g=1g5a_1>vpO`9-HoOZ{w&!=Zkw@qI^eMb>j zbVku_MLTAUo?)A@Va8|0V~QQccNTwHGO1))$zMu#l?tWa(nm`7l@*qqQ?{vGS6*3u zY58k22h5y5bKT5ODzYn@D(~*t0n=^Ticg_>l+Uk?4uc`iM?zp+`xsT2x^Qz`uGw{T7_M;GPBh7M3r(df|>!_*2d~ z<<&*Q7C9F^wm4z&g2i_(?p!i+$)A?&JT>psHK)G2bi&g1rLUYe>@?SDPoLiJbld3< zpAmP)!ZYqYZG)||p{n7ghTZlu`}Ov39mS4o9bb#n#jC|H8>cs3-S}10^rmZ? zzHXk;d|mT*&NAmZ=a0)~ExUEuzUA|l-?cpGTIhP9C9&m!uwceeLVpT@V;_n1EmoT;sWiGhm(pROocapQ{pZ425SZqI0Uw{KlJ zdFAg{ez~e*)t}GOpJhL5^Vy@%KL6}b&nY=){qHotYxv#fbH|*!`rMu8Ri1bE`N`)m zKmQ*W2p3#^!Os^iyzq(DBUYcc`tyq_FS>6{`Wo+=?H5nI_|{AGmpCtZ?f0hNul@be zOC6W~{W9UQ>n=Ndx&89L|H1Hw8~zadW8)uRyTWqCEmy{0>Av#at7cqv_thC!ue$p4 zYpSo=crADBrPu!ar_=xR%5~=JZoNMF`oQ%&ZkTh!V{6B*y>jj0bH*YDvrQ_BSw_bMZ!P}g-y|=z{{l*RBH(Ymn{Ov1l|Kg4XcWn8y_0RX* zIqc5M?mTjr`>q{#*WUfYJ^AM6UI@Uih=z+`r zO8y%7>o*Uc@!-1;&3H(k{3*}B{ldvF zy!PVk7his<;-%-elx%tS<>@a!^-AF@kN@5J_eWo~yt;8~-quI{k@t^BUbDQm@%8-I zAA6(VjVHEE+xE(mx;AVcGHYr{$k+{jBb@4?nkm{_Vef|2n*L^%rSh zT=(UuFYo!v_|;QiSA6}(H%q?R`EBdBhrV0$ea81U?V7mjZ$A|Ouyyw-yLbNR{W18{ z<$H$ix$9^1&zn1IIzQRlviH!wOZN}mfA<0Ffh`9Y9{lo9+u?-6HyoLCGBfti^@GE(VBy0M3@S4~<< zN?Lka|Mc|!S?THNS$Ij$Vp%exK@i+TG7^FKemYGy(Pn6L8Jgh7F#gvBU(n0|D&xR0 zfe#RjA05$X^>OhDiAl*RFvSrb33FjOIkG>|=rua6UK^JXpBSf0H9=&CPCt5pA+F?P zTW0o|=Nsd*u3G6oztYoBW{nY|ZmD@z!6b=~eyf3*AZ25org!tv$T)H`NwzTIyb z{O0`6#Gf8s{rp>j&-YYJxb}{V{`RLA-u~Cme}3e}cXoCzXk4-8x;r<%^zIjX^D7rN zwOxGuU5{?r{^h<5qSXSmdP-A5d|VzSVYFd@9w<07J2TFB{#98>!E?3ScALgFeCoa6 z>ax0lcAxpjaq&n%!uY(+Zvqjw4|dF)Z}E$tDJ1kv!GsVA!4F8Pj+U80W{?^02Fd-S z77MF>sDB{g-ux5#CH?&14M%UDd-nS?7ao~n@El#SWyyp4{<&nvb2;gkt?m4B?`ut! z-%PJ~@`s1-{MW~8eq3_OyQjUn{P{b#yEnh|tUaf5#`n|rZ{L3Kq^AS7c0T;g(bGC53c!WS&&q0{o>j0H-7)<&#(U+Bv)R2Yw&%l?W1Ys zFNG`Bgnp!iIABV?0j4K8aIu>q?1!(9hFT%n;A5oVNE!Tb;6@8D49`czZ0w$mC25S+ z)nqaR5=nw)0G;fn^J$u4bP)oTu|ac!5?-d6D5cRtUIU%nLOfoLk%qDDUVE{VPHJJ^ zyW#sb$$4oLAu!u1-H-WFM*?qZg!gxv3rzwizhe;^#BG`oeYa_qGKz20g~CuCe&RLY zXv1BDGkzgnG}$3uG?Uf5Xf-kpjZquIO_9L`W;m$zk?u=i^l!=v9+9KrUQEAOCbU~j z3s;m8oxya&TN{HU1>?#w1$-v5S!JAA?){rw3L|}-ql2jk)DTy|4RZ`&^>F666lSjN zWI1V~D>ng{!FJFzZl4$8^$ksK_Po*w6f+Jn-tKB};WJhdK0Dow?NU5_UhZD*Q9rwU zt&P%i8pUsSA^t@8(htIX%`I34l@Ki>;G=UG05ik4KYXz`$b~O=#n)*{kR%trjH}++ zBH{{5$#$duCf`#oN=IY67gZedcKu-4__flnp3cyJKof1I0en{H1_J4u2S&jeZ(thz+(jEjd!nK1=^<%j z)r|cJQ@|%`X>03BDJAtQ>zWA}a|hALvyBW<=qkgD$EE2PY`OlE;+bdNV{Ojf2xd;L zYoxw?j-sjc?S4+0z}3O}W+}y%igRt1UZ=wW(<}0fw;G12q$yT1&kIYh(kd*5F>aQ3 zl^gFRZhwg!SK_8|={8s~gBf3&uddlfH968mxC8?fdc>ow+to1q2*Z!EZU^v9Q_s4; zm_pf709!6}`-{u7(U#K>E*U8B!B@>MX5bXM6al=JdfLO*3Dgmgv>PqOSf$b@j22v1 zkv_OAp_EyA)j%1prbULqJ&De=yB%V?YCR%(rW;=LitpjI`5kB^xPK%kPn(vU#7hxQ zyFWPz0RVxyFpn2P^f8G?eB8vNL0Y?MlOUp~2-DTrytXEeNh0)9eiP+8=!q%=u9vOPz%p=EkNm_hL_iRmIvl}9Vlm+S9l?~cO}*=u1R=& z5T!!K)K$_~Ann(01e9%T33EhulfPMN51b@ezRGu!2VNlE;1W3lP|OSiZN*Uv{tqRvKdiYpv-O*Z8d}?dhF1cJlKNd3cx3-!6p+Z6nx|eRD{{0*NqkI_XZT@ zmti3cPM3==E=oCNCPCq-x}fCfhhJcG1w`3gvWr2id7`*_wO` zi%dM=d@)ChGlPWq`9872%igYTb5TpV(CP3u`()*Z-!OpA$16HQ;)GRrAO4sF)o_rc zt(A4-GVKe!u7*6dV@ja9TY=z2_I}0XwjPiJvJEe9lkqpop`G*sLPy8q`5$MC@U?BaaHlNtd*?_z} zMo4&6h$3PFYk^y@@W%=$PC@0?$YudBh9@#er7BuQN$!%!7>P7l3UYIk?-HDrNW*x+ zuksrfeOIMB&|y(D_-%Fy3f7L2CoUXk9lAK9)N)_`c4k&G4XRf)R#dAOV9|~ zU}`X3I20TfJRvwLs1XjsTFy{zL@-Gh7@QtlDkKRhLYmM|$PkVQM}?b(TZA~FL?{zx z3Y9{YU>7bFE)p&l{wS;$ZWsP6+$G#2JTH7M>=eEfb_t!rUg1XJCYbgbO-`LemXdSG zi6oa4lBuJ0qm#&nU>qmNG=U?96mdhi;lV^9D>yZ{L`W2pg;XJ3=ub|aNS2VZf;)qH za(j>?cL@W6I+k;`D7Mm!1G#YwB6|xGe^B6w7(s>HSHabR02_&4CbAd79ZP63ieC9- ziTcJ8Y7k%TS>~J#8$7xw2=2!9!VgpQq9_(p4LmLbb(sx{(gJt3yVlTEX1@TJ@mIuU z`W11R6lr2-Ve&jlfSLA*BNg0W03sU)RX zc#or(E(12nI7S?|^f+pXIA(#7YS$49A8EY8fZ?MR>a;6^61l?yCKOoLfeNq@K1=~S z-er&i-9ryU6h3@_!XImR62;);*h7FuUwHPdsPO*VhP3nPpW z%jF@U3WZ!w$TO3KUhjk7A_~ce))dZ@;D$|{eUS$B{2Unh25e0Vp>sW@@Pj50_f?Z-l@6!J0xU~njYaT3K)xr|jOwtYuU${r#0awY*s-wTb#x#s7 zVQ9$0*TFBhHH%OxjgIJIE%?ikuTxaNnj1cpmX*U`3 zpXu~I^Ce>bbIgC1T1Cu%#)V)Ut(=b|FIJ1Mjq zN)JDfihG>ma6bfw={Qx|ND=}-Y$C?Why5^2l^pj>L8ScYhY>X}G-$7e;fZF!9f-r`ACQJtV7`q44Gsg8ALMR zuRq)ek}UWe0x{|EoT(-AcO3|7ww50xJ+9&a5(#T+EMj9tNx^#oFF{fQ<%K(BEm z1G2B(qXGqqK6_U%*!DApv7B3>36{ge#r#eRlnT1!2+_9%gS)c#G6HvH947j;SoS^! zXe5V-4k&t3dfo*@>9N2|_J`p)2!unCjf^I#$ODATha!7LKWI{SS4w6pTUcRT}5P`ED^u8x}1fWL&aujOmaPTmO=+i-R1kxPo_z7YTz?YE2 z9ebp35W+u07}Feua3_Q@_Yny1C3}L0;l7WSaR@N%0|>-+?3Ypn+y@{G<xpN*TZsB3oIe|$XvL#!E%y^?s#y+lyV(xGpV1{MF>l&q>T|| zOl3Jq;%zC`gIoiYVV2R@yKWIw;UYpSeyuEOJEWC?B zFMFi7mpbZ7U$1yn8q{5LS-YnqWo1u0`JNNHigJgj6Ew1k4xNU>&M?y(u*rXx2I zRTTq!;^4`EmJ04h>28wl=E&xxC+&~C$1tS%Pzc-jCvEzJeSo+Ha08cxo8>qGHv!@f zMA%V?y9|@*F2jKlcNfAgLb~^`r(K4Jgu}Q$F{lZ~1(R?`qqLt9b}-8PN_z;&?c^@p zWEk39C=7t@hPt3Wm>A@6+hK60EE*tm95)`GsDR|nlJIPfY|5h(l)R@fOi(D12(rNa zmWL$Hdf=-IGg-g}FcAd{Dm`WD#jaRu@_x}U68{_P^8a96{W6=vOorSy$(?Gb<|FM> zm)ADs&mmN^G7IvM8Z^C1h1XN*Qp- z8%QxJ_)Q9aI20udK*4Xy1NVV90pZBGDW5)3&P~8Nm<@$uGajkn&6IVs?8oF%Q_q{t zG>+mjTfiOV%^(E0QcUIoE{6&>Pmlu!jLlOh%VYS^O|5gbw7SF^8*H7IJz6-51+rD` zd6!q9sAfP{!{@OM<@ob6?F!{Db-^?{oVcRMEwYv_zkoJ_5Mw!q zyAP>2tMn6t|MNSEIsghHL|(wHdOFfwF*Gz5Gz-aAFrkC7umA?yUcgkK1(Hn#9$kdTJlQf>V5JP6Q47M(E&|L7Ff2~~BEJOy zlj{lZ00WZ(g&CFRZlrn#ri9S~mIT?Lm1Bf<>2|u{QPCKqRR$@F4E6(TD=I$N5JoSU z5E(=nF`2Q6gXsX&U^*C5upRiQSR}=M^wMefepnVI&CEQ??6@jKvUmTXH3Fk}R4y)0 z(pA~I%Vz%9=-y~0OC6hRRl+(nNzc>@DADn&_Gfw6+CaMuU={z}-GzlVcd*lF03zCH zFmhNYv=ynFOmvL}ES)?_5`~qMCxO&fj!jTtCqsK)dO=ida>xu&G+nHo6pIQ0W;a=< zfv3z^^J7=^vX|41x3FOtMx~7~cLGB-2+yVCYmsiwwUaq?CCH z5}Ipb^6QNwd7vL%S=~A6OIg&Vi@J_V>E>&!Wq+@ALMH;KhEZ3JF+#PR1^Ya|p!g{A zgr{0Ipx&YRH&k2>)Q`>~5STntn8TPfCC1VvCP|4A!eh8oCn-^R;Za1_Q;a~An64z; zuf)JC9F-AVfL!dkX{So}(||k8;ofuX7IA+KwkP(S&*egYEL&%E5@hi*p1I*r0G&DE zMnH&84RAvsWX4$(Zm5a{DmVmU!me2$VT3HS3qncWCubo~La4V=fOb|H(Psg(PqT0% zjg$eea!++ zPoF7+4x;aQWCiGpn#mf_2R*}{zps)8PrL7VWF_beZO5Sn;NdgcA}U*CH2}}6&v{^k zAju&5b^zmvzUNW&pbzCJdJxKUEE5Gonr^g(@`Ox*+N{9kY!A31?R#f z&ljg3Ys#=Lr~p|q1Xe(W)7e!zQ;ox8M1(Q5DCw}rZb{}qW*9je^K;Es6LpmGxV&6z zfuVpZH=C^sMdSEijt1J)*y~h!2YIQFFHh$Ji(zRiAuSMw*h09?LaxzbDS(+DseC9P z4^qKAI84LAE4pwr5KSf=D}n1RE$u!+z>vktb2$QK`wpt{RDI!->w zYu9&l9OAX_z;1kA@7fUzJ|id4w{&!@p9XO)9hb8d2YFpZXYgB=03NRC=vem?&3iz) zB5wzI{WL&&(VlKG2Y7wu-5ni)pJ@!kwu52Y&yyubJBXu`MoFYR#Nzhx`anm=-IaSO zUz*Me2Hwl-FG33UF?MfY>~`|HbsZhoFrbpQlhJg}{s`!Q=Jh87RV|E%FskVuDe}03^_H4WI$}UK2rXhp60ol{`5ID(vpA6{eZCes2c}gx4-8L*?cB zJIFa*bm92bOs%d=fy0%$`%_HK!*wI3=3oV4Y95`Hk6H7WHCOuTe~UGT- _eventsPublisher; + private readonly DbContextEventsPublisher _eventsPublisher; public ExamplesDbContext(IPublisher publisher) { - InitEventsPublisher(publisher); + _eventsPublisher = new DbContextEventsPublisher(publisher, this); } public ExamplesDbContext(DbContextOptions options, IPublisher publisher) : base(options) { - InitEventsPublisher(publisher); - } - - private void InitEventsPublisher(IPublisher publisher) - { - _eventsPublisher = new DbContextEventsPublisher( - DomainEntityEventsAccessor.EventsSelector, - DomainEntityEventsAccessor.ClearEventsAction, - publisher, - this); + _eventsPublisher = new DbContextEventsPublisher(publisher, this); } public DbSet OutboundMessages { get; set; } public DbSet InboundMessages { get; set; } public DbSet StoredOffsets { get; set; } + public DbSet Locks { get; set; } + public DbSet Customers { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) diff --git a/samples/Examples/src/Silverback.Examples.Common/DependencyInjectionHelper.cs b/samples/Examples/src/Silverback.Examples.Common/DependencyInjectionHelper.cs index b6c1e8baa..8a89b1887 100644 --- a/samples/Examples/src/Silverback.Examples.Common/DependencyInjectionHelper.cs +++ b/samples/Examples/src/Silverback.Examples.Common/DependencyInjectionHelper.cs @@ -13,7 +13,6 @@ public static class DependencyInjectionHelper public static IServiceCollection GetServiceCollection() => new ServiceCollection() .AddDbContext(options => options .UseSqlServer(Configuration.ConnectionString)) - .AddLogging(logging => logging.SetMinimumLevel(LogLevel.Trace)) - .AddSingleton(); + .AddLogging(logging => logging.SetMinimumLevel(LogLevel.Trace)); } } diff --git a/samples/Examples/src/Silverback.Examples.Common/JobScheduler.cs b/samples/Examples/src/Silverback.Examples.Common/JobScheduler.cs deleted file mode 100644 index 4bbe0ba35..000000000 --- a/samples/Examples/src/Silverback.Examples.Common/JobScheduler.cs +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright (c) 2018-2019 Sergio Aquilini -// This code is licensed under MIT license (see LICENSE file for details) - -using System; -using System.Collections.Generic; -using System.Threading; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; - -namespace Silverback.Examples.Common -{ - public class JobScheduler : IDisposable - { - private readonly IServiceProvider _serviceProvider; - private readonly ILogger _logger; - private readonly Dictionary _timers = new Dictionary(); - private readonly Dictionary _isRunning = new Dictionary(); - - public JobScheduler(IServiceProvider serviceProvider, ILogger logger) - { - _serviceProvider = serviceProvider; - _logger = logger; - } - - public void AddJob(string name, TimeSpan interval, Action job) - { - _isRunning.Add(name, false); - - _timers.Add(name, - new Timer(_ => RunJob(name, job, _serviceProvider.CreateScope().ServiceProvider), null, TimeSpan.Zero, - interval)); - } - - private void RunJob(string name, Action job, IServiceProvider serviceProvider) - { - if (IsRunning(name)) - return; - - _logger.LogInformation($"Running job '{name}'..."); - try - { - job(serviceProvider); - _logger.LogInformation($"Job '{name}' was successful."); - } - catch (Exception ex) - { - _logger.LogError(ex, $"Job '{name}' failed."); - } - finally - { - _isRunning[name] = false; - } - } - - private bool IsRunning(string name) - { - lock (_isRunning) - { - if (_isRunning[name]) - return true; - - _isRunning[name] = true; - } - return false; - } - - public void Dispose() - { - if (_timers == null) - return; - - foreach (var timer in _timers.Values) - { - timer?.Dispose(); - } - } - } -} \ No newline at end of file diff --git a/samples/Examples/src/Silverback.Examples.Common/Messages/MessageMoved.cs b/samples/Examples/src/Silverback.Examples.Common/Messages/MessageMoved.cs new file mode 100644 index 000000000..fbf956fdc --- /dev/null +++ b/samples/Examples/src/Silverback.Examples.Common/Messages/MessageMoved.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Silverback.Examples.Common.Messages +{ + public class MessageMovedEvent + { + public Guid Id { get; set; } + + public string Destination { get; set; } + } +} diff --git a/samples/Examples/src/Silverback.Examples.Common/Silverback.Examples.Common.csproj b/samples/Examples/src/Silverback.Examples.Common/Silverback.Examples.Common.csproj index e80ba1d5a..bac1933f0 100644 --- a/samples/Examples/src/Silverback.Examples.Common/Silverback.Examples.Common.csproj +++ b/samples/Examples/src/Silverback.Examples.Common/Silverback.Examples.Common.csproj @@ -2,7 +2,7 @@ netstandard2.0 - 7.1 + latest @@ -13,13 +13,13 @@ - - - - - - - + + + + + + + diff --git a/samples/Examples/src/Silverback.Examples.ConsumerA/ConsumerServiceA.cs b/samples/Examples/src/Silverback.Examples.ConsumerA/ConsumerServiceA.cs index cc101b3b0..3464bd6b0 100644 --- a/samples/Examples/src/Silverback.Examples.ConsumerA/ConsumerServiceA.cs +++ b/samples/Examples/src/Silverback.Examples.ConsumerA/ConsumerServiceA.cs @@ -8,7 +8,6 @@ using Silverback.Examples.Common.Consumer; using Silverback.Examples.Common.Data; using Silverback.Examples.Common.Messages; -using Silverback.Examples.Common.Serialization; using Silverback.Messaging; using Silverback.Messaging.Broker; using Silverback.Messaging.Configuration; @@ -48,17 +47,46 @@ protected override void Configure(BusConfigurator configurator, IServiceProvider }, Consumers = 2 }) - .AddInbound(CreateConsumerEndpoint("silverback-examples-bad-events"), policy => policy + .AddInbound(CreateConsumerEndpoint("silverback-examples-error-events"), policy => policy .Chain( - policy.Retry(TimeSpan.FromMilliseconds(500)).MaxFailedAttempts(2), - policy.Move(new KafkaProducerEndpoint("silverback-examples-bad-events-error") - { - Configuration = new KafkaProducerConfig + policy + .Retry(TimeSpan.FromMilliseconds(500)) + .MaxFailedAttempts(2), + policy + .Move(new KafkaProducerEndpoint("silverback-examples-error-events") + { + Configuration = new KafkaProducerConfig + { + BootstrapServers = "PLAINTEXT://kafka:9092", + ClientId = "consumer-service-a" + } + }) + .MaxFailedAttempts(2), + policy + .Move(new KafkaProducerEndpoint("silverback-examples-events") + { + Configuration = new KafkaProducerConfig + { + BootstrapServers = "PLAINTEXT://kafka:9092", + ClientId = "consumer-service-a" + } + }) + .Transform( + (msg, ex) => new IntegrationEventA + { + Id = Guid.NewGuid(), + Content = $"Transformed BadEvent (exception: {ex.Message})" + }, + (headers, ex) => + { + headers.Add("exception-message", ex.Message); + return headers; + }) + .Publish(msg => new MessageMovedEvent { - BootstrapServers = "PLAINTEXT://kafka:9092", - ClientId = "consumer-service-a" - } - }))) + Id = (msg.Message as IntegrationEvent)?.Id ?? Guid.Empty, + Destination = msg.Endpoint.Name + }))) .AddInbound(CreateConsumerEndpoint("silverback-examples-custom-serializer", GetCustomSerializer())) // Special inbound (not logged) diff --git a/samples/Examples/src/Silverback.Examples.ConsumerA/LogHeadersBehavior.cs b/samples/Examples/src/Silverback.Examples.ConsumerA/LogHeadersBehavior.cs index ed8ba28bf..3ba720823 100644 --- a/samples/Examples/src/Silverback.Examples.ConsumerA/LogHeadersBehavior.cs +++ b/samples/Examples/src/Silverback.Examples.ConsumerA/LogHeadersBehavior.cs @@ -21,8 +21,6 @@ public LogHeadersBehavior(ILogger logger) public async Task> Handle(IEnumerable messages, MessagesHandler next) { - var result = await next(messages); - foreach (var message in messages.OfType()) { if (message.Headers != null && message.Headers.Any()) @@ -33,7 +31,7 @@ public async Task> Handle(IEnumerable messages, Mess } } - return result; + return await next(messages); } } } diff --git a/samples/Examples/src/Silverback.Examples.ConsumerA/Silverback.Examples.ConsumerA.csproj b/samples/Examples/src/Silverback.Examples.ConsumerA/Silverback.Examples.ConsumerA.csproj index 7f1189405..b71fed1b0 100644 --- a/samples/Examples/src/Silverback.Examples.ConsumerA/Silverback.Examples.ConsumerA.csproj +++ b/samples/Examples/src/Silverback.Examples.ConsumerA/Silverback.Examples.ConsumerA.csproj @@ -3,6 +3,7 @@ Exe netcoreapp2.2 + latest @@ -25,13 +26,13 @@ - - - - - - - + + + + + + + diff --git a/samples/Examples/src/Silverback.Examples.ConsumerA/SubscriberService.cs b/samples/Examples/src/Silverback.Examples.ConsumerA/SubscriberService.cs index a84667d36..5ac557f01 100644 --- a/samples/Examples/src/Silverback.Examples.ConsumerA/SubscriberService.cs +++ b/samples/Examples/src/Silverback.Examples.ConsumerA/SubscriberService.cs @@ -65,5 +65,11 @@ void OnBatchProcessed(BatchProcessedEvent message) { _logger.LogInformation($"Successfully processed batch '{message.BatchId} ({message.BatchSize} messages)"); } + + [Subscribe] + void OnMessageMoved(MessageMovedEvent @event) + { + _logger.LogInformation($"MessageMovedEvent :: Message '{@event.Id}' moved to '{@event.Destination}'"); + } } } \ No newline at end of file diff --git a/samples/Examples/src/Silverback.Examples.Main/Menu/MenuItem.cs b/samples/Examples/src/Silverback.Examples.Main/Menu/MenuItem.cs index 2ab99e869..48842dd12 100644 --- a/samples/Examples/src/Silverback.Examples.Main/Menu/MenuItem.cs +++ b/samples/Examples/src/Silverback.Examples.Main/Menu/MenuItem.cs @@ -9,7 +9,7 @@ namespace Silverback.Examples.Main.Menu { public abstract class MenuItem { - private static Dictionary _cache = new Dictionary(); + private static readonly Dictionary _cache = new Dictionary(); protected MenuItem(string name, int sortIndex = 100) { Name = name; diff --git a/samples/Examples/src/Silverback.Examples.Main/Silverback.Examples.Main.csproj b/samples/Examples/src/Silverback.Examples.Main/Silverback.Examples.Main.csproj index d63b82c5e..bfd9ea71f 100644 --- a/samples/Examples/src/Silverback.Examples.Main/Silverback.Examples.Main.csproj +++ b/samples/Examples/src/Silverback.Examples.Main/Silverback.Examples.Main.csproj @@ -3,6 +3,7 @@ Exe netcoreapp2.2 + latest @@ -16,6 +17,8 @@ + + @@ -23,14 +26,14 @@ - - - - - - - - + + + + + + + + diff --git a/samples/Examples/src/Silverback.Examples.Main/UseCases/Advanced/MultipleOutboundConnectorsUseCase.cs b/samples/Examples/src/Silverback.Examples.Main/UseCases/Advanced/MultipleOutboundConnectorsUseCase.cs index 3af48e7ea..1b55ec0f7 100644 --- a/samples/Examples/src/Silverback.Examples.Main/UseCases/Advanced/MultipleOutboundConnectorsUseCase.cs +++ b/samples/Examples/src/Silverback.Examples.Main/UseCases/Advanced/MultipleOutboundConnectorsUseCase.cs @@ -2,10 +2,8 @@ // This code is licensed under MIT license (see LICENSE file for details) using System; -using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; -using Silverback.Examples.Common; using Silverback.Examples.Common.Data; using Silverback.Examples.Common.Messages; using Silverback.Messaging; @@ -55,20 +53,5 @@ protected override async Task Execute(IServiceProvider serviceProvider) await dbContext.SaveChangesAsync(); } - - protected override void PreExecute(IServiceProvider serviceProvider) - { - // Setup OutboundWorker to run every 50 milliseconds using a poor-man scheduler - serviceProvider.GetRequiredService().AddJob( - "OutboundWorker", - TimeSpan.FromMilliseconds(50), - s => s.GetRequiredService().ProcessQueue()); - } - - protected override void PostExecute(IServiceProvider serviceProvider) - { - // Let the worker run for some time before - Thread.Sleep(2000); - } } } \ No newline at end of file diff --git a/samples/Examples/src/Silverback.Examples.Main/UseCases/EfCore/DeferredOutboundUseCase.cs b/samples/Examples/src/Silverback.Examples.Main/UseCases/EfCore/DeferredOutboundUseCase.cs index c9f9bfb9d..62e2f3f7d 100644 --- a/samples/Examples/src/Silverback.Examples.Main/UseCases/EfCore/DeferredOutboundUseCase.cs +++ b/samples/Examples/src/Silverback.Examples.Main/UseCases/EfCore/DeferredOutboundUseCase.cs @@ -4,16 +4,13 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; -using Silverback.Examples.Common; using Silverback.Examples.Common.Data; using Silverback.Examples.Common.Messages; using Silverback.Messaging; using Silverback.Messaging.Broker; using Silverback.Messaging.Configuration; -using Silverback.Messaging.Connectors; using Silverback.Messaging.Messages; using Silverback.Messaging.Publishing; @@ -52,22 +49,7 @@ protected override async Task Execute(IServiceProvider serviceProvider) var dbContext = serviceProvider.GetRequiredService(); await dbContext.SaveChangesAsync(); } - - protected override void PreExecute(IServiceProvider serviceProvider) - { - // Setup OutboundWorker to run every 50 milliseconds using a poor-man scheduler - serviceProvider.GetRequiredService().AddJob( - "OutboundWorker", - TimeSpan.FromMilliseconds(50), - s => s.GetRequiredService().ProcessQueue()); - } - - protected override void PostExecute(IServiceProvider serviceProvider) - { - // Let the worker run for some time before - Thread.Sleep(2000); - } - + public class CustomHeadersBehavior : IBehavior { public async Task> Handle(IEnumerable messages, MessagesHandler next) diff --git a/samples/Examples/src/Silverback.Examples.Main/UseCases/EfCore/OutboundWorkerUseCase.cs b/samples/Examples/src/Silverback.Examples.Main/UseCases/EfCore/OutboundWorkerUseCase.cs new file mode 100644 index 000000000..6a2de93b7 --- /dev/null +++ b/samples/Examples/src/Silverback.Examples.Main/UseCases/EfCore/OutboundWorkerUseCase.cs @@ -0,0 +1,76 @@ +// Copyright (c) 2018-2019 Sergio Aquilini +// This code is licensed under MIT license (see LICENSE file for details) + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Silverback.Background; +using Silverback.Examples.Common.Data; +using Silverback.Messaging; +using Silverback.Messaging.Broker; +using Silverback.Messaging.Configuration; +using Silverback.Messaging.Connectors; +using Silverback.Messaging.Messages; + +namespace Silverback.Examples.Main.UseCases.EfCore +{ + public class OutboundWorkerUseCase : UseCase + { + private CancellationTokenSource _cancellationTokenSource; + + public OutboundWorkerUseCase() : base("Outbound worker (start background processing)", 15, 1) + { + } + + protected override void ConfigureServices(IServiceCollection services) => services + .AddBus(options => options.UseModel()) + .AddDbDistributedLockManager() + .AddBroker(options => options + .AddDbOutboundConnector() + .AddDbOutboundWorker( + interval: TimeSpan.FromMilliseconds(100), + distributedLockSettings: new DistributedLockSettings + { + AcquireRetryInterval = TimeSpan.FromSeconds(1) + })); + + protected override void Configure(BusConfigurator configurator, IServiceProvider serviceProvider) + { + configurator.Connect(endpoints => endpoints + .AddOutbound(new KafkaProducerEndpoint("silverback-examples-events") + { + Configuration = new KafkaProducerConfig + { + BootstrapServers = "PLAINTEXT://kafka:9092", + ClientId = GetType().FullName + } + })); + + _cancellationTokenSource = new CancellationTokenSource(); + + Console.WriteLine("Starting OutboundWorker background process (press ESC to stop)..."); + + var service = serviceProvider.GetRequiredService(); + service.StartAsync(CancellationToken.None); + _cancellationTokenSource.Token.Register(() => service.StopAsync(CancellationToken.None)); + } + + protected override Task Execute(IServiceProvider serviceProvider) + { + while (Console.ReadKey(false).Key != ConsoleKey.Escape) + { + } + + Console.WriteLine("Canceling..."); + + _cancellationTokenSource.Cancel(); + + // Let the worker gracefully exit + Thread.Sleep(2000); + + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/samples/Examples/src/Silverback.Examples.Main/UseCases/ErrorHandling/RetryAndMoveErrorPolicyUseCase.cs b/samples/Examples/src/Silverback.Examples.Main/UseCases/ErrorHandling/RetryAndMoveErrorPolicyUseCase.cs index 083daa806..57b989ab3 100644 --- a/samples/Examples/src/Silverback.Examples.Main/UseCases/ErrorHandling/RetryAndMoveErrorPolicyUseCase.cs +++ b/samples/Examples/src/Silverback.Examples.Main/UseCases/ErrorHandling/RetryAndMoveErrorPolicyUseCase.cs @@ -15,7 +15,7 @@ namespace Silverback.Examples.Main.UseCases.ErrorHandling { public class RetryAndMoveErrorPolicyUseCase : UseCase { - public RetryAndMoveErrorPolicyUseCase() : base("Retry then move to dead letter", 10) + public RetryAndMoveErrorPolicyUseCase() : base("Processing error -> retry (x2) -> move to same topic (x2) -> move to another topic", 10, 1) { } @@ -25,7 +25,7 @@ protected override void ConfigureServices(IServiceCollection services) => servic protected override void Configure(BusConfigurator configurator, IServiceProvider serviceProvider) => configurator.Connect(endpoints => endpoints - .AddOutbound(new KafkaProducerEndpoint("silverback-examples-bad-events") + .AddOutbound(new KafkaProducerEndpoint("silverback-examples-error-events") { Configuration = new KafkaProducerConfig { diff --git a/samples/Examples/src/Silverback.Examples.Main/UseCases/ErrorHandling/RetryAndMoveErrorPolicyUseCase2.cs b/samples/Examples/src/Silverback.Examples.Main/UseCases/ErrorHandling/RetryAndMoveErrorPolicyUseCase2.cs new file mode 100644 index 000000000..e50f6e665 --- /dev/null +++ b/samples/Examples/src/Silverback.Examples.Main/UseCases/ErrorHandling/RetryAndMoveErrorPolicyUseCase2.cs @@ -0,0 +1,57 @@ +// Copyright (c) 2018-2019 Sergio Aquilini +// This code is licensed under MIT license (see LICENSE file for details) + +using System; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Silverback.Examples.Common.Messages; +using Silverback.Messaging; +using Silverback.Messaging.Broker; +using Silverback.Messaging.Configuration; +using Silverback.Messaging.Messages; +using Silverback.Messaging.Publishing; +using Silverback.Messaging.Serialization; + +namespace Silverback.Examples.Main.UseCases.ErrorHandling +{ + public class RetryAndMoveErrorPolicyUseCase2 : UseCase + { + public RetryAndMoveErrorPolicyUseCase2() : base("Deserialization error -> retry (x2) -> move to same topic (x2) -> move to another topic", 11, 1) + { + } + + protected override void ConfigureServices(IServiceCollection services) => services + .AddBus(options => options.UseModel()) + .AddBroker(); + + protected override void Configure(BusConfigurator configurator, IServiceProvider serviceProvider) => + configurator.Connect(endpoints => endpoints + .AddOutbound(new KafkaProducerEndpoint("silverback-examples-error-events") + { + Configuration = new KafkaProducerConfig + { + BootstrapServers = "PLAINTEXT://kafka:9092", + ClientId = GetType().FullName + }, + Serializer = new BuggySerializer() + })); + + protected override async Task Execute(IServiceProvider serviceProvider) + { + var publisher = serviceProvider.GetService(); + + await publisher.PublishAsync(new BadIntegrationEvent { Content = DateTime.Now.ToString("HH:mm:ss.fff") }); + } + + private class BuggySerializer : IMessageSerializer + { + public byte[] Serialize(object message) => new byte[] { 0, 1, 2, 3, 4 }; + + public object Deserialize(byte[] message) + { + throw new NotImplementedException(); + } + } + } +} \ No newline at end of file diff --git a/samples/Examples/src/Silverback.Examples.Main/UseCases/UseCase.cs b/samples/Examples/src/Silverback.Examples.Main/UseCases/UseCase.cs index f17387750..9df6f5b36 100644 --- a/samples/Examples/src/Silverback.Examples.Main/UseCases/UseCase.cs +++ b/samples/Examples/src/Silverback.Examples.Main/UseCases/UseCase.cs @@ -2,7 +2,10 @@ // This code is licensed under MIT license (see LICENSE file for details) using System; +using System.Reflection; using System.Threading.Tasks; +using Autofac; +using Autofac.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using NLog.Extensions.Logging; @@ -15,11 +18,14 @@ namespace Silverback.Examples.Main.UseCases { public abstract class UseCase : MenuItem { - private const int ExecutionsCount = 3; + private const bool UseAutofac = false; - protected UseCase(string name, int sortIndex = 100) + private readonly int _executionsCount; + + protected UseCase(string name, int sortIndex = 100, int executionCount = 3) : base(name, sortIndex) { + _executionsCount = executionCount; } public void Execute() @@ -33,17 +39,46 @@ public void Execute() ConfigureServices(services); - using (var serviceProvider = services.BuildServiceProvider()) + var serviceProvider = BuildServiceProvider(services); + + try { CreateScopeAndConfigure(serviceProvider); - for (int i = 0; i < ExecutionsCount; i++) + for (int i = 0; i < _executionsCount; i++) { CreateScopeAndExecute(serviceProvider); } CreateScopeAndPostExecute(serviceProvider); } + finally + { + ((IDisposable) serviceProvider)?.Dispose(); + } + } + + private IServiceProvider BuildServiceProvider(IServiceCollection services) + { + if (UseAutofac) + { + return ConfigureAutofac(services); + } + else + { + return services.BuildServiceProvider(new ServiceProviderOptions + { + ValidateScopes = true + }); + } + } + + private IServiceProvider ConfigureAutofac(IServiceCollection services) + { + var containerBuilder = new ContainerBuilder(); + containerBuilder.RegisterAssemblyModules(Assembly.GetExecutingAssembly()); + containerBuilder.Populate(services); + return new AutofacServiceProvider(containerBuilder.Build()); } private void CreateScopeAndConfigure(IServiceProvider serviceProvider) @@ -54,7 +89,7 @@ private void CreateScopeAndConfigure(IServiceProvider serviceProvider) scope.ServiceProvider.GetRequiredService().Database.EnsureCreated(); - Configure(scope.ServiceProvider.GetService(), serviceProvider); + Configure(scope.ServiceProvider.GetService(), scope.ServiceProvider); PreExecute(scope.ServiceProvider); } diff --git a/samples/Examples/src/Silverback.Examples.Main/nlog.config b/samples/Examples/src/Silverback.Examples.Main/nlog.config index 606db4f2b..6a2fe66f8 100644 --- a/samples/Examples/src/Silverback.Examples.Main/nlog.config +++ b/samples/Examples/src/Silverback.Examples.Main/nlog.config @@ -8,7 +8,7 @@ + layout="* ${date:format=HH\:mm\:ss.fff}|${level:uppercase=true}|${message}|${exception:format=ToString,StackTrace}|${logger}|${all-event-properties}" /> diff --git a/samples/PerformanceTester/src/Silverback.PerformanceTester/Silverback.PerformanceTester.csproj b/samples/PerformanceTester/src/Silverback.PerformanceTester/Silverback.PerformanceTester.csproj index f00c9ea34..396b9448a 100644 --- a/samples/PerformanceTester/src/Silverback.PerformanceTester/Silverback.PerformanceTester.csproj +++ b/samples/PerformanceTester/src/Silverback.PerformanceTester/Silverback.PerformanceTester.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp2.1 + netcoreapp2.2 diff --git a/samples/SilverbackShop/src/Baskets.Domain/Baskets.Domain.csproj b/samples/SilverbackShop/src/Baskets.Domain/Baskets.Domain.csproj index 2c27a0c55..7d5212c03 100644 --- a/samples/SilverbackShop/src/Baskets.Domain/Baskets.Domain.csproj +++ b/samples/SilverbackShop/src/Baskets.Domain/Baskets.Domain.csproj @@ -4,9 +4,6 @@ netstandard2.0 SilverbackShop.Baskets.Domain SilverbackShop.Baskets.Domain - - - latest diff --git a/samples/SilverbackShop/src/Baskets.Infrastructure/Baskets.Infrastructure.csproj b/samples/SilverbackShop/src/Baskets.Infrastructure/Baskets.Infrastructure.csproj index bf4edbdf2..0d58a7446 100644 --- a/samples/SilverbackShop/src/Baskets.Infrastructure/Baskets.Infrastructure.csproj +++ b/samples/SilverbackShop/src/Baskets.Infrastructure/Baskets.Infrastructure.csproj @@ -4,9 +4,6 @@ netstandard2.0 SilverbackShop.Baskets.Infrastructure SilverbackShop.Baskets.Infrastructure - - - latest diff --git a/samples/SilverbackShop/src/Baskets.Integration/Baskets.Integration.csproj b/samples/SilverbackShop/src/Baskets.Integration/Baskets.Integration.csproj index 99b555596..d60f552ed 100644 --- a/samples/SilverbackShop/src/Baskets.Integration/Baskets.Integration.csproj +++ b/samples/SilverbackShop/src/Baskets.Integration/Baskets.Integration.csproj @@ -4,9 +4,6 @@ netstandard2.0 SilverbackShop.Baskets.Integration SilverbackShop.Baskets.Integration - - - latest diff --git a/samples/SilverbackShop/src/Baskets.Service/Baskets.Service.csproj b/samples/SilverbackShop/src/Baskets.Service/Baskets.Service.csproj index 6535c43c0..ff9f1c03e 100644 --- a/samples/SilverbackShop/src/Baskets.Service/Baskets.Service.csproj +++ b/samples/SilverbackShop/src/Baskets.Service/Baskets.Service.csproj @@ -1,14 +1,11 @@  - netcoreapp2.1 + netcoreapp2.2 SilverbackShop.Baskets.Service SilverbackShop.Baskets.Service ..\docker-compose.dcproj Linux - - - latest diff --git a/samples/SilverbackShop/src/Catalog.Domain/Catalog.Domain.csproj b/samples/SilverbackShop/src/Catalog.Domain/Catalog.Domain.csproj index 07a8b6c6a..6014c2819 100644 --- a/samples/SilverbackShop/src/Catalog.Domain/Catalog.Domain.csproj +++ b/samples/SilverbackShop/src/Catalog.Domain/Catalog.Domain.csproj @@ -4,9 +4,6 @@ netstandard2.0 SilverbackShop.Catalog.Domain SilverbackShop.Catalog.Domain - - - latest diff --git a/samples/SilverbackShop/src/Catalog.Infrastructure/Catalog.Infrastructure.csproj b/samples/SilverbackShop/src/Catalog.Infrastructure/Catalog.Infrastructure.csproj index 7daf38bb9..ea101f216 100644 --- a/samples/SilverbackShop/src/Catalog.Infrastructure/Catalog.Infrastructure.csproj +++ b/samples/SilverbackShop/src/Catalog.Infrastructure/Catalog.Infrastructure.csproj @@ -4,9 +4,6 @@ netstandard2.0 SilverbackShop.Catalog.Infrastructure SilverbackShop.Catalog.Infrastructure - - - latest diff --git a/samples/SilverbackShop/src/Catalog.Integration/Catalog.Integration.csproj b/samples/SilverbackShop/src/Catalog.Integration/Catalog.Integration.csproj index 9edc5291b..9ffefc425 100644 --- a/samples/SilverbackShop/src/Catalog.Integration/Catalog.Integration.csproj +++ b/samples/SilverbackShop/src/Catalog.Integration/Catalog.Integration.csproj @@ -4,9 +4,6 @@ netstandard2.0 SilverbackShop.Catalog.Integration SilverbackShop.Catalog.Integration - - - latest diff --git a/samples/SilverbackShop/src/Catalog.Service/Catalog.Service.csproj b/samples/SilverbackShop/src/Catalog.Service/Catalog.Service.csproj index 02b8c2671..fa21b134f 100644 --- a/samples/SilverbackShop/src/Catalog.Service/Catalog.Service.csproj +++ b/samples/SilverbackShop/src/Catalog.Service/Catalog.Service.csproj @@ -1,14 +1,11 @@  - netcoreapp2.1 + netcoreapp2.2 SilverbackShop.Catalog.Service SilverbackShop.Catalog.Service ..\docker-compose.dcproj Linux - - - latest diff --git a/samples/SilverbackShop/src/Common.Api/Common.Api.csproj b/samples/SilverbackShop/src/Common.Api/Common.Api.csproj index 07e9baf1b..3a7711c61 100644 --- a/samples/SilverbackShop/src/Common.Api/Common.Api.csproj +++ b/samples/SilverbackShop/src/Common.Api/Common.Api.csproj @@ -1,7 +1,7 @@ - netcoreapp2.1 + netcoreapp2.2 diff --git a/samples/SilverbackShop/src/Common.Data/Common.Data.csproj b/samples/SilverbackShop/src/Common.Data/Common.Data.csproj index 45f4a8b1e..eabfacae0 100644 --- a/samples/SilverbackShop/src/Common.Data/Common.Data.csproj +++ b/samples/SilverbackShop/src/Common.Data/Common.Data.csproj @@ -4,9 +4,6 @@ netstandard2.0 SilverbackShop.Common.Data SilverbackShop.Common.Data - - - latest diff --git a/samples/SilverbackShop/src/Common.Domain/Common.Domain.csproj b/samples/SilverbackShop/src/Common.Domain/Common.Domain.csproj index d0f3572c6..30f490f73 100644 --- a/samples/SilverbackShop/src/Common.Domain/Common.Domain.csproj +++ b/samples/SilverbackShop/src/Common.Domain/Common.Domain.csproj @@ -3,9 +3,6 @@ netstandard2.0 SilverbackShop.Common.Domain - - - latest diff --git a/samples/SilverbackShop/src/Common.Infrastructure/Common.Infrastructure.csproj b/samples/SilverbackShop/src/Common.Infrastructure/Common.Infrastructure.csproj index 934c96948..5b3eb0dc5 100644 --- a/samples/SilverbackShop/src/Common.Infrastructure/Common.Infrastructure.csproj +++ b/samples/SilverbackShop/src/Common.Infrastructure/Common.Infrastructure.csproj @@ -4,9 +4,6 @@ netstandard2.0 SilverbackShop.Common.Infrastructure SilverbackShop.Common.Infrastructure - - - latest diff --git a/src/Silverback.Core.EntityFrameworkCore/Background/Configuration/DependencyInjectionExtensions.cs b/src/Silverback.Core.EntityFrameworkCore/Background/Configuration/DependencyInjectionExtensions.cs new file mode 100644 index 000000000..6a54675f2 --- /dev/null +++ b/src/Silverback.Core.EntityFrameworkCore/Background/Configuration/DependencyInjectionExtensions.cs @@ -0,0 +1,22 @@ +// Copyright (c) 2018-2019 Sergio Aquilini +// This code is licensed under MIT license (see LICENSE file for details) + +using Microsoft.EntityFrameworkCore; +using Silverback.Background; + +// ReSharper disable once CheckNamespace +namespace Microsoft.Extensions.DependencyInjection +{ + public static partial class DependencyInjectionExtensions + { + /// + /// Adds the implementation and uses the specified DbContext to + /// handle the distributed locks. + /// + public static IServiceCollection AddDbDistributedLockManager(this IServiceCollection services) + where TDbContext : DbContext + { + return services.AddSingleton>(); + } + } +} \ No newline at end of file diff --git a/src/Silverback.Core.EntityFrameworkCore/Background/DbContextDistributedLockManager.cs b/src/Silverback.Core.EntityFrameworkCore/Background/DbContextDistributedLockManager.cs new file mode 100644 index 000000000..20ba52372 --- /dev/null +++ b/src/Silverback.Core.EntityFrameworkCore/Background/DbContextDistributedLockManager.cs @@ -0,0 +1,157 @@ +// Copyright (c) 2018-2019 Sergio Aquilini +// This code is licensed under MIT license (see LICENSE file for details) + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Silverback.Background.Model; + +namespace Silverback.Background +{ + public class DbContextDistributedLockManager : IDistributedLockManager + where TDbContext : DbContext + { + private readonly IServiceProvider _serviceProvider; + private readonly ILogger _logger; + + public DbContextDistributedLockManager(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + _logger = serviceProvider.GetRequiredService>>(); + } + + public Task Acquire(DistributedLockSettings settings, CancellationToken cancellationToken = default) => + Acquire(settings.ResourceName, settings.AcquireTimeout, settings.AcquireRetryInterval, settings.HeartbeatTimeout, cancellationToken); + + public async Task Acquire(string resourceName, TimeSpan? acquireTimeout = null, TimeSpan? acquireRetryInterval = null, TimeSpan? heartbeatTimeout = null, CancellationToken cancellationToken = default) + { + var start = DateTime.Now; + while (acquireTimeout == null || DateTime.Now - start < acquireTimeout) + { + if (await TryAcquireLock(resourceName, heartbeatTimeout)) + return new DistributedLock(resourceName, this); + + await Task.Delay(acquireRetryInterval?.Milliseconds ?? 500, cancellationToken); + + if (cancellationToken.IsCancellationRequested) + break; + } + + throw new TimeoutException($"Timeout waiting to get the required lock '{resourceName}'."); + } + + public async Task SendHeartbeat(string resourceName) + { + try + { + using (var scope = _serviceProvider.CreateScope()) + { + await SendHeartbeat(resourceName, scope.ServiceProvider); + } + } + catch (Exception ex) + { + _logger.LogError(ex, + "Failed to send heartbeat for lock '{lockName}'. See inner exception for details.", + resourceName); + } + } + + public async Task Release(string resourceName) + { + try + { + using (var scope = _serviceProvider.CreateScope()) + { + await Release(resourceName, scope.ServiceProvider); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to release lock '{lockName}'. See inner exception for details.", resourceName); + } + } + + private async Task TryAcquireLock(string resourceName, TimeSpan? heartbeatTimeout = null) + { + try + { + using (var scope = _serviceProvider.CreateScope()) + { + return await AcquireLock(resourceName, heartbeatTimeout, scope.ServiceProvider); + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to acquire lock '{lockName}'. See inner exception for details.", + resourceName); + } + + return false; + } + + private async Task AcquireLock(string resourceName, TimeSpan? heartbeatTimeout, IServiceProvider serviceProvider) + { + var heartbeatThreshold = DateTime.UtcNow.Subtract(heartbeatTimeout ?? TimeSpan.FromSeconds(10)); + + var (dbSet, dbContext) = GetDbSet(serviceProvider); + + if (await dbSet.AnyAsync(l => l.Name == resourceName && l.Heartbeat >= heartbeatThreshold)) + return false; + + await WriteLock(resourceName, dbSet, dbContext); + + return true; + } + + private async Task WriteLock(string resourceName, DbSet dbSet, TDbContext dbContext) + { + var entity = await dbSet.FirstOrDefaultAsync(e => e.Name == resourceName) + ?? dbSet.Add(new Lock { Name = resourceName }).Entity; + + entity.Heartbeat = entity.Created = DateTime.UtcNow; + + await dbContext.SaveChangesAsync(); + } + + private async Task SendHeartbeat(string resourceName, IServiceProvider serviceProvider) + { + var (dbSet, dbContext) = GetDbSet(serviceProvider); + + var lockRecord = await dbSet.FirstOrDefaultAsync(l => l.Name == resourceName); + + if (lockRecord == null) + return; + + lockRecord.Heartbeat = DateTime.UtcNow; + + await dbContext.SaveChangesAsync(); + } + + private async Task Release(string resourceName, IServiceProvider serviceProvider) + { + var (dbSet, dbContext) = GetDbSet(serviceProvider); + + var lockRecord = await dbSet.FirstOrDefaultAsync(l => l.Name == resourceName); + + if (lockRecord == null) + return; + + dbSet.Remove(lockRecord); + + await dbContext.SaveChangesAsync(); + } + + private (DbSet dbSet, TDbContext dbContext) GetDbSet(IServiceProvider serviceProvider) + { + var dbContext = serviceProvider.GetRequiredService(); + var dbSet = serviceProvider.GetRequiredService().Set() + ?? throw new SilverbackException($"The DbContext doesn't contain a DbSet<{typeof(Lock).FullName}>."); + + return (dbSet, dbContext); + } + } +} diff --git a/src/Silverback.Core.EntityFrameworkCore/Background/Model/Lock.cs b/src/Silverback.Core.EntityFrameworkCore/Background/Model/Lock.cs new file mode 100644 index 000000000..c25d63a33 --- /dev/null +++ b/src/Silverback.Core.EntityFrameworkCore/Background/Model/Lock.cs @@ -0,0 +1,21 @@ +// Copyright (c) 2018-2019 Sergio Aquilini +// This code is licensed under MIT license (see LICENSE file for details) + +using System; +using System.ComponentModel.DataAnnotations; + +namespace Silverback.Background.Model +{ + public class Lock + { + [Key, MaxLength(500)] + public string Name { get; set; } + + public DateTime Created { get; set; } + + public DateTime Heartbeat { get; set; } + + [Timestamp] + public byte[] Timestamp { get; set; } + } +} \ No newline at end of file diff --git a/src/Silverback.Core.EntityFrameworkCore/DbContextEventsPublisher.cs b/src/Silverback.Core.EntityFrameworkCore/EntityFrameworkCore/DbContextEventsPublisher.cs similarity index 80% rename from src/Silverback.Core.EntityFrameworkCore/DbContextEventsPublisher.cs rename to src/Silverback.Core.EntityFrameworkCore/EntityFrameworkCore/DbContextEventsPublisher.cs index 3b0eb7d2b..d01d04f37 100644 --- a/src/Silverback.Core.EntityFrameworkCore/DbContextEventsPublisher.cs +++ b/src/Silverback.Core.EntityFrameworkCore/EntityFrameworkCore/DbContextEventsPublisher.cs @@ -14,15 +14,23 @@ namespace Silverback.EntityFrameworkCore /// /// Exposes some methods to handle domain events as part of the SaveChanges transaction. /// - public class DbContextEventsPublisher where TDomainEntity : class + public class DbContextEventsPublisher { - private readonly Func> _eventsSelector; - private readonly Action _clearEventsAction; + private readonly Func> _eventsSelector; + private readonly Action _clearEventsAction; private readonly IPublisher _publisher; private readonly DbContext _dbContext; - public DbContextEventsPublisher(Func> eventsSelector, - Action clearEventsAction, IPublisher publisher, DbContext dbContext) + public DbContextEventsPublisher(IPublisher publisher, DbContext dbContext) + : this( + e => (e as IMessagesSource)?.GetMessages(), + e => (e as IMessagesSource)?.ClearMessages(), + publisher, dbContext) + { + } + + public DbContextEventsPublisher(Func> eventsSelector, + Action clearEventsAction, IPublisher publisher, DbContext dbContext) { _eventsSelector = eventsSelector ?? throw new ArgumentNullException(nameof(eventsSelector)); _clearEventsAction = clearEventsAction ?? throw new ArgumentNullException(nameof(clearEventsAction)); @@ -86,15 +94,15 @@ private async Task PublishDomainEvents(bool async) private List GetDomainEvents() => _dbContext - .ChangeTracker.Entries() + .ChangeTracker.Entries() .SelectMany(e => { - var selected = _eventsSelector(e.Entity).ToList(); + var selected = _eventsSelector(e.Entity)?.ToList(); // Clear all events to avoid firing the same event multiple times during the recursion _clearEventsAction(e.Entity); - return selected; + return selected ?? Enumerable.Empty(); }).ToList(); private async Task PublishEvent(bool async) diff --git a/src/Silverback.Core.EntityFrameworkCore/Silverback.Core.EntityFrameworkCore.csproj b/src/Silverback.Core.EntityFrameworkCore/Silverback.Core.EntityFrameworkCore.csproj index 3e3cf4be2..c50578566 100644 --- a/src/Silverback.Core.EntityFrameworkCore/Silverback.Core.EntityFrameworkCore.csproj +++ b/src/Silverback.Core.EntityFrameworkCore/Silverback.Core.EntityFrameworkCore.csproj @@ -5,18 +5,18 @@ true BEagle1984 - Silverback is a simple framework to build reactive, event-driven, microservices. This package adds the ability to fire the domain events as part of the EF's SaveChanges transaction. + Silverback is a simple framework to build reactive, event-driven, microservices. This package adds the ability to fire the domain events as part of the EntityFramework's SaveChanges transaction. https://github.com/BEagle1984/silverback/ - https://github.com/BEagle1984/silverback/blob/master/LICENSE - 0.6.1.0 - Silverback.EntityFrameworkCore + MIT + 0.10.0.0 + Silverback https://github.com/BEagle1984/silverback/raw/develop/graphics/Icon.png + latest + 1701;1702;CS1591 bin\Debug\netstandard2.0\Silverback.Core.EntityFrameworkCore.xml - latest - 1701;1702;CS1591 @@ -25,7 +25,7 @@ - + diff --git a/src/Silverback.Core.Model/Domain/DomainEntity.cs b/src/Silverback.Core.Model/Domain/DomainEntity.cs index 3fa1021f3..37f3818bb 100644 --- a/src/Silverback.Core.Model/Domain/DomainEntity.cs +++ b/src/Silverback.Core.Model/Domain/DomainEntity.cs @@ -4,39 +4,14 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; using System.Linq; +using Silverback.Messaging.Messages; namespace Silverback.Domain { - /// - /// A sample implementation of . - /// - public abstract class DomainEntity : IDomainEntity + public abstract class DomainEntity : MessagesSource { - private List> _domainEvents; - [NotMapped] - public IEnumerable> DomainEvents => - _domainEvents?.AsReadOnly() ?? Enumerable.Empty>(); - - public void ClearEvents() => _domainEvents?.Clear(); - - protected void AddEvent(IDomainEvent domainEvent) - { - _domainEvents = _domainEvents ?? new List>(); - - ((IDomainEvent)domainEvent).Source = this; - - _domainEvents.Add(domainEvent); - } - - protected TEvent AddEvent() - where TEvent : IDomainEvent, new() - { - var evnt = new TEvent(); - AddEvent(evnt); - return evnt; - } - - protected void RemoveEvent(IDomainEvent domainEvent) => _domainEvents?.Remove(domainEvent); + public IEnumerable DomainEvents => + GetMessages()?.Cast() ?? Enumerable.Empty(); } } diff --git a/src/Silverback.Core.Model/Domain/DomainEntityEventsAccessor.cs b/src/Silverback.Core.Model/Domain/DomainEntityEventsAccessor.cs deleted file mode 100644 index dcedc1e25..000000000 --- a/src/Silverback.Core.Model/Domain/DomainEntityEventsAccessor.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) 2018-2019 Sergio Aquilini -// This code is licensed under MIT license (see LICENSE file for details) - -using System; -using System.Collections.Generic; - -namespace Silverback.Domain -{ - public static class DomainEntityEventsAccessor - { - public static Func> EventsSelector = e => e.DomainEvents; - - public static Action ClearEventsAction = e => e.ClearEvents(); - } -} diff --git a/src/Silverback.Core.Model/Domain/DomainEvent.cs b/src/Silverback.Core.Model/Domain/DomainEvent.cs index e14b17c6a..967832dc0 100644 --- a/src/Silverback.Core.Model/Domain/DomainEvent.cs +++ b/src/Silverback.Core.Model/Domain/DomainEvent.cs @@ -1,22 +1,19 @@ // Copyright (c) 2018-2019 Sergio Aquilini // This code is licensed under MIT license (see LICENSE file for details) + +using Silverback.Messaging.Messages; + namespace Silverback.Domain { public abstract class DomainEvent : IDomainEvent - where TEntity : IDomainEntity { public TEntity Source { get; set; } - protected DomainEvent(TEntity source) - { - Source = source; - } - protected DomainEvent() { } - IDomainEntity IDomainEvent.Source + object IMessageWithSource.Source { get => Source; set => Source = (TEntity) value; diff --git a/src/Silverback.Core.Model/Domain/IAggregateRoot.cs b/src/Silverback.Core.Model/Domain/IAggregateRoot.cs index 2142de879..99d0b2b49 100644 --- a/src/Silverback.Core.Model/Domain/IAggregateRoot.cs +++ b/src/Silverback.Core.Model/Domain/IAggregateRoot.cs @@ -1,5 +1,6 @@ // Copyright (c) 2018-2019 Sergio Aquilini // This code is licensed under MIT license (see LICENSE file for details) + namespace Silverback.Domain { /// diff --git a/src/Silverback.Core.Model/Domain/IDomainEntity.cs b/src/Silverback.Core.Model/Domain/IDomainEntity.cs deleted file mode 100644 index 4a553c5c5..000000000 --- a/src/Silverback.Core.Model/Domain/IDomainEntity.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) 2018-2019 Sergio Aquilini -// This code is licensed under MIT license (see LICENSE file for details) - -using System.Collections.Generic; - -namespace Silverback.Domain -{ - /// - /// Exposes the methods to retrieve the collection related to - /// an entity. - /// See for a sample implementation of this interface. - /// - public interface IDomainEntity - { - IEnumerable> DomainEvents { get; } - - void ClearEvents(); - } -} \ No newline at end of file diff --git a/src/Silverback.Core.Model/Domain/IDomainEvent.cs b/src/Silverback.Core.Model/Domain/IDomainEvent.cs index 2ace5373d..54a825446 100644 --- a/src/Silverback.Core.Model/Domain/IDomainEvent.cs +++ b/src/Silverback.Core.Model/Domain/IDomainEvent.cs @@ -5,13 +5,11 @@ namespace Silverback.Domain { - public interface IDomainEvent : IEvent + public interface IDomainEvent : IMessageWithSource, IEvent { - IDomainEntity Source { get; set; } } public interface IDomainEvent : IDomainEvent - where TEntity : IDomainEntity { new TEntity Source { get; } } diff --git a/src/Silverback.Core.Model/Messaging/Messages/ICommand.cs b/src/Silverback.Core.Model/Messaging/Messages/ICommand.cs index 811e8dbc8..30c42ff38 100644 --- a/src/Silverback.Core.Model/Messaging/Messages/ICommand.cs +++ b/src/Silverback.Core.Model/Messaging/Messages/ICommand.cs @@ -1,5 +1,6 @@ // Copyright (c) 2018-2019 Sergio Aquilini // This code is licensed under MIT license (see LICENSE file for details) + namespace Silverback.Messaging.Messages { public interface ICommand : IMessage diff --git a/src/Silverback.Core.Model/Messaging/Messages/IEvent.cs b/src/Silverback.Core.Model/Messaging/Messages/IEvent.cs index b4d81f43a..cbb8b2dc7 100644 --- a/src/Silverback.Core.Model/Messaging/Messages/IEvent.cs +++ b/src/Silverback.Core.Model/Messaging/Messages/IEvent.cs @@ -1,5 +1,6 @@ // Copyright (c) 2018-2019 Sergio Aquilini // This code is licensed under MIT license (see LICENSE file for details) + namespace Silverback.Messaging.Messages { public interface IEvent : IMessage diff --git a/src/Silverback.Core.Model/Messaging/Messages/IIntegrationCommand.cs b/src/Silverback.Core.Model/Messaging/Messages/IIntegrationCommand.cs index 5e6392eb4..ea4c72d7e 100644 --- a/src/Silverback.Core.Model/Messaging/Messages/IIntegrationCommand.cs +++ b/src/Silverback.Core.Model/Messaging/Messages/IIntegrationCommand.cs @@ -1,5 +1,6 @@ // Copyright (c) 2018-2019 Sergio Aquilini // This code is licensed under MIT license (see LICENSE file for details) + namespace Silverback.Messaging.Messages { public interface IIntegrationCommand : ICommand, IIntegrationMessage diff --git a/src/Silverback.Core.Model/Messaging/Messages/IIntegrationEvent.cs b/src/Silverback.Core.Model/Messaging/Messages/IIntegrationEvent.cs index 3b2b72711..ec0aa143e 100644 --- a/src/Silverback.Core.Model/Messaging/Messages/IIntegrationEvent.cs +++ b/src/Silverback.Core.Model/Messaging/Messages/IIntegrationEvent.cs @@ -1,5 +1,6 @@ // Copyright (c) 2018-2019 Sergio Aquilini // This code is licensed under MIT license (see LICENSE file for details) + namespace Silverback.Messaging.Messages { public interface IIntegrationEvent : IEvent, IIntegrationMessage diff --git a/src/Silverback.Core.Model/Messaging/Messages/IQuery.cs b/src/Silverback.Core.Model/Messaging/Messages/IQuery.cs index 6492783e1..0b5d76b59 100644 --- a/src/Silverback.Core.Model/Messaging/Messages/IQuery.cs +++ b/src/Silverback.Core.Model/Messaging/Messages/IQuery.cs @@ -1,5 +1,6 @@ // Copyright (c) 2018-2019 Sergio Aquilini // This code is licensed under MIT license (see LICENSE file for details) + namespace Silverback.Messaging.Messages { public interface IQuery : IRequest diff --git a/src/Silverback.Core.Model/Messaging/Messages/IRequest.cs b/src/Silverback.Core.Model/Messaging/Messages/IRequest.cs index fa026075f..7ff2f773e 100644 --- a/src/Silverback.Core.Model/Messaging/Messages/IRequest.cs +++ b/src/Silverback.Core.Model/Messaging/Messages/IRequest.cs @@ -1,5 +1,6 @@ // Copyright (c) 2018-2019 Sergio Aquilini // This code is licensed under MIT license (see LICENSE file for details) + namespace Silverback.Messaging.Messages { public interface IRequest : IMessage diff --git a/src/Silverback.Core.Model/Silverback.Core.Model.csproj b/src/Silverback.Core.Model/Silverback.Core.Model.csproj index 2090ead21..7c9cf22a1 100644 --- a/src/Silverback.Core.Model/Silverback.Core.Model.csproj +++ b/src/Silverback.Core.Model/Silverback.Core.Model.csproj @@ -4,17 +4,27 @@ netstandard2.0 Silverback true - 0.6.1.0 + 0.10.0.0 BEagle1984 BEagle1984 Silverback is a simple framework to build reactive, event-driven, microservices. This package contains the default messages interfaces and domain entity implementation. - https://github.com/BEagle1984/silverback/blob/master/LICENSE + MIT https://github.com/BEagle1984/silverback/ https://github.com/BEagle1984/silverback/raw/develop/graphics/Icon.png + latest + 1701;1702;CS1591 + + + + bin\Debug\netstandard2.0\Silverback.Core.Model.xml + + + + bin\Release\netstandard2.0\Silverback.Core.Model.xml - + diff --git a/src/Silverback.Core.Rx/Configuration/BusPluginOptionsExtensions.cs b/src/Silverback.Core.Rx/Configuration/BusPluginOptionsExtensions.cs index 9d63e9a8d..59c1061e8 100644 --- a/src/Silverback.Core.Rx/Configuration/BusPluginOptionsExtensions.cs +++ b/src/Silverback.Core.Rx/Configuration/BusPluginOptionsExtensions.cs @@ -14,8 +14,8 @@ public static class BusPluginOptionsExtensions public static BusPluginOptions Observable(this BusPluginOptions options) { options.Services - .AddSingleton() - .AddSingleton() + .AddScoped() + .AddScoped() .AddSingleton() .AddSingleton() .AddSingleton(typeof(IMessageObservable<>), typeof(MessageObservable<>)); diff --git a/src/Silverback.Core.Rx/Silverback.Core.Rx.csproj b/src/Silverback.Core.Rx/Silverback.Core.Rx.csproj index 294ada557..7432834be 100644 --- a/src/Silverback.Core.Rx/Silverback.Core.Rx.csproj +++ b/src/Silverback.Core.Rx/Silverback.Core.Rx.csproj @@ -4,18 +4,28 @@ netstandard2.0 Silverback true - 0.6.1.0 + 0.10.0.0 BEagle1984 BEagle1984 Silverback is a simple framework to build reactive, event-driven, microservices. This package adds the ability to create an Observable from the internal bus messages stream. - https://github.com/BEagle1984/silverback/blob/master/LICENSE + MIT https://github.com/BEagle1984/silverback/ https://github.com/BEagle1984/silverback/raw/develop/graphics/Icon.png + latest + 1701;1702;CS1591 + + + + bin\Debug\netstandard2.0\Silverback.Core.Rx.xml + + + + bin\Release\netstandard2.0\Silverback.Core.Rx.xml - - + + diff --git a/src/Silverback.Core/AssemblyInfo.cs b/src/Silverback.Core/AssemblyInfo.cs deleted file mode 100644 index e92dcd69b..000000000 --- a/src/Silverback.Core/AssemblyInfo.cs +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) 2018-2019 Sergio Aquilini -// This code is licensed under MIT license (see LICENSE file for details) - -using System.Runtime.CompilerServices; - -[assembly: InternalsVisibleTo("Silverback.Core.EntityFrameworkCore")] -[assembly: InternalsVisibleTo("Silverback.Core.Model")] -[assembly: InternalsVisibleTo("Silverback.Core.Rx")] -[assembly: InternalsVisibleTo("Silverback.Integration")] -[assembly: InternalsVisibleTo("Silverback.Integration.EntityFrameworkCore")] -[assembly: InternalsVisibleTo("Silverback.Integration.Kafka")] \ No newline at end of file diff --git a/src/Silverback.Core/Background/Configuration/DependencyInjectionExtensions.cs b/src/Silverback.Core/Background/Configuration/DependencyInjectionExtensions.cs new file mode 100644 index 000000000..b0a4d1320 --- /dev/null +++ b/src/Silverback.Core/Background/Configuration/DependencyInjectionExtensions.cs @@ -0,0 +1,2 @@ +// Copyright (c) 2018-2019 Sergio Aquilini +// This code is licensed under MIT license (see LICENSE file for details) diff --git a/src/Silverback.Core/Background/DistributedBackgroundService.cs b/src/Silverback.Core/Background/DistributedBackgroundService.cs new file mode 100644 index 000000000..5fe2e4603 --- /dev/null +++ b/src/Silverback.Core/Background/DistributedBackgroundService.cs @@ -0,0 +1,65 @@ +// Copyright (c) 2018-2019 Sergio Aquilini +// This code is licensed under MIT license (see LICENSE file for details) + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace Silverback.Background +{ + /// + /// Extends the adding a distributed lock mechanism to prevent + /// concurrent executions. + /// + /// + public abstract class DistributedBackgroundService : BackgroundService + { + private readonly DistributedLockSettings _distributedLockSettings; + private readonly IDistributedLockManager _distributedLockManager; + private readonly ILogger _logger; + private DistributedLock _acquiredLock; + + protected DistributedBackgroundService(DistributedLockSettings distributedLockSettings, IDistributedLockManager distributedLockManager, ILogger logger) + { + _distributedLockSettings = distributedLockSettings ?? throw new ArgumentNullException(nameof(distributedLockSettings)); + _distributedLockManager = distributedLockManager ?? throw new ArgumentNullException(nameof(distributedLockManager)); + _logger = logger; + } + + protected override Task ExecuteAsync(CancellationToken stoppingToken) + { + _logger.LogInformation( + $"Starting background service {GetType().FullName}. Waiting for lock '{_distributedLockSettings.ResourceName}'..."); + + // Run another task to avoid deadlocks + return Task.Run(async () => + { + try + { + _acquiredLock = await _distributedLockManager.Acquire(_distributedLockSettings, stoppingToken); + + _logger.LogInformation($"Lock acquired, executing background service {GetType().FullName}."); + + await ExecuteLockedAsync(stoppingToken); + } + catch (TaskCanceledException) + { + // Don't log exception that is fired by the cancellation token. + } + catch (Exception ex) + { + _logger.LogError(ex, $"Background service '{GetType().FullName}' failed."); + } + finally + { + if (_acquiredLock != null) + await _acquiredLock.Release(); + } + }); + } + + protected abstract Task ExecuteLockedAsync(CancellationToken stoppingToken); + } +} diff --git a/src/Silverback.Core/Background/DistributedLock.cs b/src/Silverback.Core/Background/DistributedLock.cs new file mode 100644 index 000000000..e7d899ec2 --- /dev/null +++ b/src/Silverback.Core/Background/DistributedLock.cs @@ -0,0 +1,40 @@ +// Copyright (c) 2018-2019 Sergio Aquilini +// This code is licensed under MIT license (see LICENSE file for details) + +using System.Threading.Tasks; + +namespace Silverback.Background +{ + public class DistributedLock + { + private readonly string _name; + private readonly IDistributedLockManager _lockManager; + private readonly int _heartbeatIntervalInMilliseconds; + private bool _released; + + public DistributedLock(string name, IDistributedLockManager lockManager, int heartbeatIntervalInMilliseconds = 1000) + { + _name = name; + _lockManager = lockManager; + _heartbeatIntervalInMilliseconds = heartbeatIntervalInMilliseconds; + + Task.Run(SendHeartbeats); + } + + private async Task SendHeartbeats() + { + while (!_released) + { + await _lockManager.SendHeartbeat(_name); + + await Task.Delay(_heartbeatIntervalInMilliseconds); + } + } + + public async Task Release() + { + _released = true; + await _lockManager.Release(_name); + } + } +} \ No newline at end of file diff --git a/src/Silverback.Core/Background/DistributedLockSettings.cs b/src/Silverback.Core/Background/DistributedLockSettings.cs new file mode 100644 index 000000000..bedd0e0ea --- /dev/null +++ b/src/Silverback.Core/Background/DistributedLockSettings.cs @@ -0,0 +1,26 @@ +// Copyright (c) 2018-2019 Sergio Aquilini +// This code is licensed under MIT license (see LICENSE file for details) + +using System; + +namespace Silverback.Background +{ + public class DistributedLockSettings + { + public DistributedLockSettings(string resourceName = null, TimeSpan? acquireTimeout = null, TimeSpan? acquireRetryInterval = null, TimeSpan? heartbeatTimeout = null) + { + ResourceName = resourceName; + AcquireTimeout = acquireTimeout; + AcquireRetryInterval = acquireRetryInterval; + HeartbeatTimeout = heartbeatTimeout; + } + + public string ResourceName { get; set; } + + public TimeSpan? AcquireTimeout { get; set; } + + public TimeSpan? AcquireRetryInterval { get; set; } + + public TimeSpan? HeartbeatTimeout { get; set; } + } +} \ No newline at end of file diff --git a/src/Silverback.Core/Background/IDistributedLockManager.cs b/src/Silverback.Core/Background/IDistributedLockManager.cs new file mode 100644 index 000000000..f9cfa00b0 --- /dev/null +++ b/src/Silverback.Core/Background/IDistributedLockManager.cs @@ -0,0 +1,20 @@ +// Copyright (c) 2018-2019 Sergio Aquilini +// This code is licensed under MIT license (see LICENSE file for details) + +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Silverback.Background +{ + public interface IDistributedLockManager + { + Task Acquire(DistributedLockSettings settings, CancellationToken cancellationToken = default); + + Task Acquire(string resourceName, TimeSpan? acquireTimeout = null, TimeSpan? acquireRetryInterval = null, TimeSpan? heartbeatTimeout = null, CancellationToken cancellationToken = default); + + Task SendHeartbeat(string resourceName); + + Task Release(string resourceName); + } +} \ No newline at end of file diff --git a/src/Silverback.Core/Background/NullLockManager.cs b/src/Silverback.Core/Background/NullLockManager.cs new file mode 100644 index 000000000..36f3509ee --- /dev/null +++ b/src/Silverback.Core/Background/NullLockManager.cs @@ -0,0 +1,22 @@ +// Copyright (c) 2018-2019 Sergio Aquilini +// This code is licensed under MIT license (see LICENSE file for details) + +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Silverback.Background +{ + public class NullLockManager : IDistributedLockManager + { + public Task Acquire(DistributedLockSettings settings, CancellationToken cancellationToken = default) => + Task.FromResult(null); + + public Task Acquire(string resourceName, TimeSpan? acquireTimeout = null, TimeSpan? acquireRetryInterval = null, TimeSpan? heartbeatTimeout = null, CancellationToken cancellationToken = default) => + Task.FromResult(null); + + public Task SendHeartbeat(string resourceName) => Task.CompletedTask; + + public Task Release(string resourceName) => Task.CompletedTask; + } +} \ No newline at end of file diff --git a/src/Silverback.Core/Background/RecurringDistributedBackgroundService.cs b/src/Silverback.Core/Background/RecurringDistributedBackgroundService.cs new file mode 100644 index 000000000..af6ab52b8 --- /dev/null +++ b/src/Silverback.Core/Background/RecurringDistributedBackgroundService.cs @@ -0,0 +1,50 @@ +// Copyright (c) 2018-2019 Sergio Aquilini +// This code is licensed under MIT license (see LICENSE file for details) + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace Silverback.Background +{ + public abstract class RecurringDistributedBackgroundService : DistributedBackgroundService + { + private readonly TimeSpan _interval; + private readonly ILogger _logger; + + protected RecurringDistributedBackgroundService(TimeSpan interval, DistributedLockSettings distributedLockSettings, IDistributedLockManager distributedLockManager, ILogger logger) + : base(distributedLockSettings, distributedLockManager, logger) + { + _interval = interval; + _logger = logger; + } + + protected override async Task ExecuteLockedAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + await ExecuteRecurringAsync(stoppingToken); + + if (stoppingToken.IsCancellationRequested) + break; + + await Sleep(stoppingToken); + } + + _logger.LogInformation("OutboundQueueWorker processing stopped."); + } + + protected abstract Task ExecuteRecurringAsync(CancellationToken stoppingToken); + + private async Task Sleep(CancellationToken stoppingToken) + { + if (_interval <= TimeSpan.Zero) + return; + + _logger.LogTrace($"Sleeping for {_interval.TotalMilliseconds} milliseconds."); + + await Task.Delay(_interval, stoppingToken); + } + } +} \ No newline at end of file diff --git a/src/Silverback.Core/Messaging/Configuration/DependencyInjectionExtensions.cs b/src/Silverback.Core/Messaging/Configuration/DependencyInjectionExtensions.cs index b2f9feac1..3ad48f9e4 100644 --- a/src/Silverback.Core/Messaging/Configuration/DependencyInjectionExtensions.cs +++ b/src/Silverback.Core/Messaging/Configuration/DependencyInjectionExtensions.cs @@ -11,7 +11,7 @@ // ReSharper disable once CheckNamespace namespace Microsoft.Extensions.DependencyInjection { - public static class DependencyInjectionExtensions + public static partial class DependencyInjectionExtensions { public static IServiceCollection AddBus(this IServiceCollection services, Action optionsAction = null) { diff --git a/src/Silverback.Core/Messaging/Messages/IMessage.cs b/src/Silverback.Core/Messaging/Messages/IMessage.cs index c943201c1..071503ead 100644 --- a/src/Silverback.Core/Messaging/Messages/IMessage.cs +++ b/src/Silverback.Core/Messaging/Messages/IMessage.cs @@ -1,5 +1,6 @@ // Copyright (c) 2018-2019 Sergio Aquilini // This code is licensed under MIT license (see LICENSE file for details) + namespace Silverback.Messaging.Messages { public interface IMessage diff --git a/src/Silverback.Core/Messaging/Messages/IMessageWithSource.cs b/src/Silverback.Core/Messaging/Messages/IMessageWithSource.cs new file mode 100644 index 000000000..b561d581b --- /dev/null +++ b/src/Silverback.Core/Messaging/Messages/IMessageWithSource.cs @@ -0,0 +1,10 @@ +// Copyright (c) 2018-2019 Sergio Aquilini +// This code is licensed under MIT license (see LICENSE file for details) + +namespace Silverback.Messaging.Messages +{ + public interface IMessageWithSource + { + object Source { get; set; } + } +} \ No newline at end of file diff --git a/src/Silverback.Core/Messaging/Messages/IMessagesSource.cs b/src/Silverback.Core/Messaging/Messages/IMessagesSource.cs new file mode 100644 index 000000000..33b2258a0 --- /dev/null +++ b/src/Silverback.Core/Messaging/Messages/IMessagesSource.cs @@ -0,0 +1,14 @@ +// Copyright (c) 2018-2019 Sergio Aquilini +// This code is licensed under MIT license (see LICENSE file for details) + +using System.Collections.Generic; + +namespace Silverback.Messaging.Messages +{ + public interface IMessagesSource + { + IEnumerable GetMessages(); + + void ClearMessages(); + } +} diff --git a/src/Silverback.Core/Messaging/Messages/ISilverbackEvent.cs b/src/Silverback.Core/Messaging/Messages/ISilverbackEvent.cs index ecc5423e5..017e585f4 100644 --- a/src/Silverback.Core/Messaging/Messages/ISilverbackEvent.cs +++ b/src/Silverback.Core/Messaging/Messages/ISilverbackEvent.cs @@ -1,5 +1,6 @@ // Copyright (c) 2018-2019 Sergio Aquilini // This code is licensed under MIT license (see LICENSE file for details) + namespace Silverback.Messaging.Messages { public interface ISilverbackEvent : IMessage diff --git a/src/Silverback.Core/Messaging/Messages/MessagesSource.cs b/src/Silverback.Core/Messaging/Messages/MessagesSource.cs new file mode 100644 index 000000000..f346f53c2 --- /dev/null +++ b/src/Silverback.Core/Messaging/Messages/MessagesSource.cs @@ -0,0 +1,55 @@ +// Copyright (c) 2018-2019 Sergio Aquilini +// This code is licensed under MIT license (see LICENSE file for details) + +using System.Collections.Generic; +using System.Linq; + +namespace Silverback.Messaging.Messages +{ + public abstract class MessagesSource : MessagesSource + { + } + + public abstract class MessagesSource : IMessagesSource + { + private List _events; + + #region IMessagesSource + + public IEnumerable GetMessages() => _events?.Cast(); + + public void ClearMessages() => _events?.Clear(); + + #endregion + + protected virtual void AddEvent(TBaseEvent @event) + { + _events = _events ?? new List(); + + if (@event is IMessageWithSource messageWithSource) + messageWithSource.Source = this; + + _events.Add(@event); + } + + /// + /// Adds an event of the specified type to this entity. The event will be fired when the entity + /// is saved to the underlying database. + /// + /// The type of the event. + /// if set to false only one instance of the specified type TEvent will be added. + /// + protected TEvent AddEvent(bool allowMultiple = true) + where TEvent : TBaseEvent, new() + { + if (!allowMultiple && _events != null && _events.OfType().Any()) + return _events.OfType().First(); + + var @event = new TEvent(); + AddEvent(@event); + return @event; + } + + protected void RemoveEvent(TBaseEvent @event) => _events?.Remove(@event); + } +} \ No newline at end of file diff --git a/src/Silverback.Core/Messaging/Messages/TransactionAbortedEvent.cs b/src/Silverback.Core/Messaging/Messages/TransactionAbortedEvent.cs index 7a82ac995..4edffc296 100644 --- a/src/Silverback.Core/Messaging/Messages/TransactionAbortedEvent.cs +++ b/src/Silverback.Core/Messaging/Messages/TransactionAbortedEvent.cs @@ -1,5 +1,6 @@ // Copyright (c) 2018-2019 Sergio Aquilini // This code is licensed under MIT license (see LICENSE file for details) + namespace Silverback.Messaging.Messages { /// diff --git a/src/Silverback.Core/Messaging/Messages/TransactionCompletedEvent.cs b/src/Silverback.Core/Messaging/Messages/TransactionCompletedEvent.cs index 49ea8bfcd..361c05d70 100644 --- a/src/Silverback.Core/Messaging/Messages/TransactionCompletedEvent.cs +++ b/src/Silverback.Core/Messaging/Messages/TransactionCompletedEvent.cs @@ -1,5 +1,6 @@ // Copyright (c) 2018-2019 Sergio Aquilini // This code is licensed under MIT license (see LICENSE file for details) + namespace Silverback.Messaging.Messages { /// diff --git a/src/Silverback.Core/Messaging/Messages/TransactionStartedEvent.cs b/src/Silverback.Core/Messaging/Messages/TransactionStartedEvent.cs index bc4582546..4446a98f0 100644 --- a/src/Silverback.Core/Messaging/Messages/TransactionStartedEvent.cs +++ b/src/Silverback.Core/Messaging/Messages/TransactionStartedEvent.cs @@ -1,5 +1,6 @@ // Copyright (c) 2018-2019 Sergio Aquilini // This code is licensed under MIT license (see LICENSE file for details) + namespace Silverback.Messaging.Messages { /// diff --git a/src/Silverback.Core/Messaging/Publishing/Publisher.cs b/src/Silverback.Core/Messaging/Publishing/Publisher.cs index f429cbb93..43d4f227e 100644 --- a/src/Silverback.Core/Messaging/Publishing/Publisher.cs +++ b/src/Silverback.Core/Messaging/Publishing/Publisher.cs @@ -36,35 +36,40 @@ public Publisher(BusOptions options, IServiceProvider serviceProvider, ILogger

Publish(new[] { message }); - public Task PublishAsync(object message) => + public Task PublishAsync(object message) => PublishAsync(new[] { message }); - + public IEnumerable Publish(object message) => Publish(new[] { message }); - public Task> PublishAsync(object message) => - PublishAsync(new[] { message }); + public async Task> PublishAsync(object message) => + await PublishAsync(new[] { message }); public void Publish(IEnumerable messages) => Publish(messages, false).Wait(); - public Task PublishAsync(IEnumerable messages) => - Publish(messages, true); + public Task PublishAsync(IEnumerable messages) => Publish(messages, true); - public IEnumerable Publish(IEnumerable messages) => - Publish(messages, false).Result.Cast().ToList(); + public IEnumerable Publish(IEnumerable messages) => + CastResults(Publish(messages, false).Result); + + public async Task> PublishAsync(IEnumerable messages) => + CastResults(await Publish(messages, true)); - public async Task> PublishAsync(IEnumerable messages) + private IEnumerable CastResults(IEnumerable results) { - var results = await Publish(messages, true); - - try - { - return results.Cast().ToList(); - } - catch (InvalidCastException ex) + foreach (var result in results) { - throw new SilverbackException("One or more subscribers returned a result that is not compatible with the expected response type.", ex); + if (result is TResult castResult) + { + yield return castResult; + } + else + { + _logger.LogTrace( + $"Discarding result of type {result.GetType().FullName} because it doesn't match " + + $"the expected return type {typeof(TResult).FullName}."); + } } } diff --git a/src/Silverback.Core/Messaging/Subscribers/ArgumentResolvers/ISingleMessageArgumentResolver.cs b/src/Silverback.Core/Messaging/Subscribers/ArgumentResolvers/ISingleMessageArgumentResolver.cs index be12d6143..666e1f36a 100644 --- a/src/Silverback.Core/Messaging/Subscribers/ArgumentResolvers/ISingleMessageArgumentResolver.cs +++ b/src/Silverback.Core/Messaging/Subscribers/ArgumentResolvers/ISingleMessageArgumentResolver.cs @@ -1,5 +1,6 @@ // Copyright (c) 2018-2019 Sergio Aquilini // This code is licensed under MIT license (see LICENSE file for details) + namespace Silverback.Messaging.Subscribers.ArgumentResolvers { public interface ISingleMessageArgumentResolver : IMessageArgumentResolver diff --git a/src/Silverback.Core/Messaging/Subscribers/ISubscriber.cs b/src/Silverback.Core/Messaging/Subscribers/ISubscriber.cs index 7310d13f8..cfb1f80c5 100644 --- a/src/Silverback.Core/Messaging/Subscribers/ISubscriber.cs +++ b/src/Silverback.Core/Messaging/Subscribers/ISubscriber.cs @@ -1,5 +1,6 @@ // Copyright (c) 2018-2019 Sergio Aquilini // This code is licensed under MIT license (see LICENSE file for details) + namespace Silverback.Messaging.Subscribers { public interface ISubscriber diff --git a/src/Silverback.Core/Messaging/Subscribers/SubscribedMethod.cs b/src/Silverback.Core/Messaging/Subscribers/SubscribedMethod.cs index 62be876b9..d5b3d9a28 100644 --- a/src/Silverback.Core/Messaging/Subscribers/SubscribedMethod.cs +++ b/src/Silverback.Core/Messaging/Subscribers/SubscribedMethod.cs @@ -1,5 +1,6 @@ // Copyright (c) 2018-2019 Sergio Aquilini // This code is licensed under MIT license (see LICENSE file for details) + namespace Silverback.Messaging.Subscribers { public class SubscribedMethod diff --git a/src/Silverback.Core/Messaging/Subscribers/SubscribedMethodInvoker.cs b/src/Silverback.Core/Messaging/Subscribers/SubscribedMethodInvoker.cs index afcc985c9..e054fab04 100644 --- a/src/Silverback.Core/Messaging/Subscribers/SubscribedMethodInvoker.cs +++ b/src/Silverback.Core/Messaging/Subscribers/SubscribedMethodInvoker.cs @@ -74,19 +74,19 @@ private Task Invoke(SubscribedMethod method, object[] parameters, bool e private object InvokeSync(SubscribedMethod method, object[] parameters) { - if (!method.Info.MethodInfo.IsAsync()) - return method.Info.MethodInfo.Invoke(method.Target, parameters ); + if (!method.Info.MethodInfo.ReturnsTask()) + return method.Info.MethodInfo.Invoke(method.Target, parameters); return AsyncHelper.RunSynchronously(() => { var result = (Task)method.Info.MethodInfo.Invoke(method.Target, parameters); - return ((Task)result).GetReturnValue(); + return result.GetReturnValue(); }); } private Task InvokeAsync(SubscribedMethod method, object[] parameters) { - if (!method.Info.MethodInfo.IsAsync()) + if (!method.Info.MethodInfo.ReturnsTask()) return Task.Run(() => method.Info.MethodInfo.Invoke(method.Target, parameters)); var result = method.Info.MethodInfo.Invoke(method.Target, parameters); diff --git a/src/Silverback.Core/Properties/AssemblyInfo.cs b/src/Silverback.Core/Properties/AssemblyInfo.cs index a3a93df7d..41deab2e9 100644 --- a/src/Silverback.Core/Properties/AssemblyInfo.cs +++ b/src/Silverback.Core/Properties/AssemblyInfo.cs @@ -3,7 +3,15 @@ using System.Runtime.CompilerServices; -[assembly: InternalsVisibleTo("Silverback.Integration")] [assembly: InternalsVisibleTo("Silverback.Core.EntityFrameworkCore")] +[assembly: InternalsVisibleTo("Silverback.Core.Model")] +[assembly: InternalsVisibleTo("Silverback.Core.Rx")] +[assembly: InternalsVisibleTo("Silverback.EventSourcing")] +[assembly: InternalsVisibleTo("Silverback.EventSourcing.EntityFrameworkCore")] +[assembly: InternalsVisibleTo("Silverback.Integration")] +[assembly: InternalsVisibleTo("Silverback.Integration.Configuration")] +[assembly: InternalsVisibleTo("Silverback.Integration.EntityFrameworkCore")] +[assembly: InternalsVisibleTo("Silverback.Integration.InMemory")] +[assembly: InternalsVisibleTo("Silverback.Integration.Kafka")] [assembly: InternalsVisibleTo("Silverback.Core.Tests")] diff --git a/src/Silverback.Core/Silverback.Core.csproj b/src/Silverback.Core/Silverback.Core.csproj index d4b45f3d9..8a3281a7f 100644 --- a/src/Silverback.Core/Silverback.Core.csproj +++ b/src/Silverback.Core/Silverback.Core.csproj @@ -7,23 +7,23 @@ Silverback is a simple framework to build reactive, event-driven, microservices. This core package provides a very simple in-memory message bus. - https://github.com/BEagle1984/silverback/blob/master/LICENSE + MIT https://github.com/BEagle1984/silverback/ true - 0.6.1.0 + 0.10.0.0 false https://github.com/BEagle1984/silverback/raw/develop/graphics/Icon.png + latest + 1701;1702;CS1591 bin\Debug\netstandard2.0\Silverback.Core.xml Off - latest - 1701;1702;CS1591 @@ -33,6 +33,7 @@ + diff --git a/src/Silverback.Core/SilverbackConcurrencyException.cs b/src/Silverback.Core/SilverbackConcurrencyException.cs new file mode 100644 index 000000000..f7f465071 --- /dev/null +++ b/src/Silverback.Core/SilverbackConcurrencyException.cs @@ -0,0 +1,27 @@ +// Copyright (c) 2018-2019 Sergio Aquilini +// This code is licensed under MIT license (see LICENSE file for details) + +using System; +using System.Runtime.Serialization; + +namespace Silverback +{ + public class SilverbackConcurrencyException : Exception + { + public SilverbackConcurrencyException() + { + } + + public SilverbackConcurrencyException(string message) : base(message) + { + } + + public SilverbackConcurrencyException(string message, Exception innerException) : base(message, innerException) + { + } + + protected SilverbackConcurrencyException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + } +} \ No newline at end of file diff --git a/src/Silverback.Core/Util/ReflectionHelper.cs b/src/Silverback.Core/Util/ReflectionHelper.cs index dd0f6e0d1..964cf04a0 100644 --- a/src/Silverback.Core/Util/ReflectionHelper.cs +++ b/src/Silverback.Core/Util/ReflectionHelper.cs @@ -6,10 +6,9 @@ namespace Silverback.Util { - // TODO: Test internal static class ReflectionHelper { - public static bool IsAsync(this MethodInfo methodInfo) => + public static bool ReturnsTask(this MethodInfo methodInfo) => typeof(Task).IsAssignableFrom(methodInfo.ReturnType); } } \ No newline at end of file diff --git a/src/Silverback.EventSourcing.EntityFrameworkCore/EventStore/DbContextEventStoreRepository.cs b/src/Silverback.EventSourcing.EntityFrameworkCore/EventStore/DbContextEventStoreRepository.cs new file mode 100644 index 000000000..03422cac5 --- /dev/null +++ b/src/Silverback.EventSourcing.EntityFrameworkCore/EventStore/DbContextEventStoreRepository.cs @@ -0,0 +1,101 @@ +// Copyright (c) 2018-2019 Sergio Aquilini +// This code is licensed under MIT license (see LICENSE file for details) + +using System; +using System.Linq; +using System.Linq.Expressions; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; + +namespace Silverback.EventStore +{ + public abstract class DbContextEventStoreRepository + : EventStoreRepository + where TAggregateEntity : IEventSourcingAggregate + where TEventStoreEntity : EventStoreEntity + where TEventEntity : IEventEntity, new() + { + private readonly DbSet _dbSet; + + protected DbContextEventStoreRepository(DbContext dbContext) + { + _dbSet = dbContext.Set(); + } + + protected IQueryable EventStores => _dbSet.Include(s => s.Events); + + /// + /// Gets the aggregate entity folding the events from the specified event store. + /// + /// The predicate applied to get the desired event store. + /// + public TAggregateEntity Get(Expression> predicate) => + GetAggregateEntity(EventStores.AsNoTracking().FirstOrDefault(predicate)); + + /// + /// Gets the aggregate entity folding the events from the specified event store. + /// + /// The predicate applied to get the desired event store. + /// + public async Task GetAsync(Expression> predicate) => + GetAggregateEntity(await EventStores.AsNoTracking().FirstOrDefaultAsync(predicate)); + + /// + /// Gets the aggregate entity folding the events from the specified event store. + /// Only the events until the specified date and time are applied, giving + /// a snapshot of a past state of the entity. + /// + /// The predicate applied to get the desired event store. + /// The snapshot date and time. + /// + public TAggregateEntity GetSnapshot(Expression> predicate, DateTime snapshot) => + GetAggregateEntity(EventStores.AsNoTracking().FirstOrDefault(predicate), snapshot); + + /// + /// Gets the aggregate entity folding the events from the specified event store. + /// Only the events until the specified date and time are applied, giving + /// a snapshot of a past state of the entity. + /// + /// The predicate applied to get the desired event store. + /// The snapshot date and time. + /// + public async Task GetSnapshotAsync(Expression> predicate, DateTime snapshot) => + GetAggregateEntity(await EventStores.AsNoTracking().FirstOrDefaultAsync(predicate), snapshot); + + protected override TEventStoreEntity GetEventStoreEntity(TAggregateEntity aggregateEntity, bool addIfNotFound) + { + var eventStore = _dbSet.Find(aggregateEntity.Id); + + if (eventStore == null && addIfNotFound) + { + eventStore = GetNewEventStoreEntity(aggregateEntity); + _dbSet.Add(eventStore); + } + + return eventStore; + } + + protected override async Task GetEventStoreEntityAsync(TAggregateEntity aggregateEntity, bool addIfNotFound) + { + var eventStore = await _dbSet.FindAsync(aggregateEntity.Id); + + if (eventStore == null && addIfNotFound) + { + eventStore = GetNewEventStoreEntity(aggregateEntity); + _dbSet.Add(eventStore); + } + + return eventStore; + } + + protected override TEventStoreEntity Remove(TAggregateEntity aggregateEntity, TEventStoreEntity eventStore) + { + if (eventStore == null) + return default; + + return _dbSet.Remove(eventStore)?.Entity; + } + + protected abstract TEventStoreEntity GetNewEventStoreEntity(TAggregateEntity aggregateEntity); + } +} diff --git a/src/Silverback.EventSourcing.EntityFrameworkCore/Silverback.EventSourcing.EntityFrameworkCore.csproj b/src/Silverback.EventSourcing.EntityFrameworkCore/Silverback.EventSourcing.EntityFrameworkCore.csproj new file mode 100644 index 000000000..5b58c8bcc --- /dev/null +++ b/src/Silverback.EventSourcing.EntityFrameworkCore/Silverback.EventSourcing.EntityFrameworkCore.csproj @@ -0,0 +1,31 @@ + + + + netstandard2.0 + Silverback + true + 0.10.0.0 + BEagle1984 + BEagle1984 + Silverback is a simple framework to build reactive, event-driven, microservices. This package contains the implementation of the EventStore for EntityFramework. + MIT + https://github.com/BEagle1984/silverback/ + https://github.com/BEagle1984/silverback/raw/develop/graphics/Icon.png + latest + 1701;1702;CS1591 + + + + bin\Debug\netstandard2.0\Silverback.EventSourcing.EntityFrameworkCore.xml + + + + bin\Release\netstandard2.0\Silverback.EventSourcing.EntityFrameworkCore.xml + + + + + + + + diff --git a/src/Silverback.EventSourcing/Domain/EntityEvent.cs b/src/Silverback.EventSourcing/Domain/EntityEvent.cs new file mode 100644 index 000000000..d066aefec --- /dev/null +++ b/src/Silverback.EventSourcing/Domain/EntityEvent.cs @@ -0,0 +1,17 @@ +// Copyright (c) 2018-2019 Sergio Aquilini +// This code is licensed under MIT license (see LICENSE file for details) + +using System; +using Newtonsoft.Json; + +namespace Silverback.Domain +{ + public abstract class EntityEvent : IEntityEvent + { + [JsonIgnore] + public DateTime Timestamp { get; set; } = DateTime.UtcNow; + + [JsonIgnore] + public int Sequence { get; set; } = 0; + } +} \ No newline at end of file diff --git a/src/Silverback.EventSourcing/Domain/EventSourcingDomainEntity.cs b/src/Silverback.EventSourcing/Domain/EventSourcingDomainEntity.cs new file mode 100644 index 000000000..70c5259a5 --- /dev/null +++ b/src/Silverback.EventSourcing/Domain/EventSourcingDomainEntity.cs @@ -0,0 +1,76 @@ +// Copyright (c) 2018-2019 Sergio Aquilini +// This code is licensed under MIT license (see LICENSE file for details) + +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; +using System.Linq; +using Silverback.Domain.Util; +using Silverback.EventStore; +using Silverback.Messaging.Messages; +using Silverback.Util; + +namespace Silverback.Domain +{ + public abstract class EventSourcingDomainEntity : EventSourcingDomainEntity, IEventSourcingAggregate + { + protected EventSourcingDomainEntity() + { + } + + protected EventSourcingDomainEntity(IEnumerable events) : base(events) + { + } + } + + public abstract class EventSourcingDomainEntity : MessagesSource, IEventSourcingAggregate + { + private readonly List _storedEvents; + private List _newEvents; + + protected EventSourcingDomainEntity() + { + } + + protected EventSourcingDomainEntity(IEnumerable events) + { + events = events.OrderBy(e => e.Timestamp).ThenBy(e => e.Sequence).ToList().AsReadOnly(); + + events.ForEach(e => EventsApplier.Apply(e, this, true)); + + _storedEvents = events.ToList(); + + ClearMessages(); + } + + [NotMapped] + public IEnumerable DomainEvents => + GetMessages()?.Cast() ?? Enumerable.Empty(); + + [NotMapped] + public IEnumerable Events => + (_storedEvents ?? Enumerable.Empty()).Union(_newEvents ?? Enumerable.Empty()).ToList().AsReadOnly(); + + public TKey Id { get; protected set; } + + protected virtual IEntityEvent AddAndApplyEvent(IEntityEvent @event) + { + EventsApplier.Apply(@event, this); + + _newEvents = _newEvents ?? new List(); + _newEvents.Add(@event); + + if (@event.Timestamp == default(DateTime)) + @event.Timestamp = DateTime.UtcNow; + + if (@event.Sequence <= 0) + @event.Sequence = (_storedEvents?.Count ?? 0) + _newEvents.Count; + + return @event; + } + + public int GetVersion() => Events.Count(); + + public IEnumerable GetNewEvents() => _newEvents?.AsReadOnly() ?? Enumerable.Empty(); + } +} \ No newline at end of file diff --git a/src/Silverback.EventSourcing/Domain/IEntityEvent.cs b/src/Silverback.EventSourcing/Domain/IEntityEvent.cs new file mode 100644 index 000000000..f5b94b432 --- /dev/null +++ b/src/Silverback.EventSourcing/Domain/IEntityEvent.cs @@ -0,0 +1,14 @@ +// Copyright (c) 2018-2019 Sergio Aquilini +// This code is licensed under MIT license (see LICENSE file for details) + +using System; + +namespace Silverback.Domain +{ + public interface IEntityEvent + { + DateTime Timestamp { get; set; } + + int Sequence { get; set; } + } +} \ No newline at end of file diff --git a/src/Silverback.EventSourcing/Domain/Util/EntityActivator.cs b/src/Silverback.EventSourcing/Domain/Util/EntityActivator.cs new file mode 100644 index 000000000..f410b620b --- /dev/null +++ b/src/Silverback.EventSourcing/Domain/Util/EntityActivator.cs @@ -0,0 +1,29 @@ +// Copyright (c) 2018-2019 Sergio Aquilini +// This code is licensed under MIT license (see LICENSE file for details) + +using System; +using System.Collections.Generic; + +namespace Silverback.Domain.Util +{ + internal static class EntityActivator + { + public static TEntity CreateInstance(IEnumerable events, object eventStoreEntity) + { + try + { + var entity = (TEntity) Activator.CreateInstance(typeof(TEntity), new object[] {events}); + + PropertiesMapper.Map(eventStoreEntity, entity); + + return entity; + } + catch (MissingMethodException ex) + { + throw new SilverbackException( + $"The type {typeof(TEntity).Name} doesn't have a public constructor " + + $"with a single parameter of type IEnumerable.", ex); + } + } + } +} diff --git a/src/Silverback.EventSourcing/Domain/Util/EventsApplier.cs b/src/Silverback.EventSourcing/Domain/Util/EventsApplier.cs new file mode 100644 index 000000000..2e95f2540 --- /dev/null +++ b/src/Silverback.EventSourcing/Domain/Util/EventsApplier.cs @@ -0,0 +1,61 @@ +// Copyright (c) 2018-2019 Sergio Aquilini +// This code is licensed under MIT license (see LICENSE file for details) + +using System; +using System.Linq; +using System.Reflection; + +namespace Silverback.Domain.Util +{ + // TODO: Cache something? + internal static class EventsApplier + { + private const string ApplyMethodPrefix = "apply"; + + public static void Apply(IEntityEvent @event, object entity, bool isReplaying = false) + { + var methods = entity.GetType() + .GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) + .Where(m => m.Name.ToLowerInvariant().StartsWith(ApplyMethodPrefix) && + m.GetParameters().Any() && + m.GetParameters().First().ParameterType.IsInstanceOfType(@event)) + .ToList(); + + if (!methods.Any()) + { + throw new SilverbackException( + $"No method found to apply event of type {@event.GetType().Name} " + + $"in entity {entity.GetType().Name}."); + } + + methods.ForEach(m => InvokeApplyMethod(m, @event, entity, isReplaying)); + } + + private static void InvokeApplyMethod(MethodInfo methodInfo, IEntityEvent @event, object entity, + bool isReplaying) + { + try + { + var parametersCount = methodInfo.GetParameters().Count(); + + switch (parametersCount) + { + case 1: + methodInfo.Invoke(entity, new object[] {@event}); + break; + case 2: + methodInfo.Invoke(entity, new object[] {@event, isReplaying}); + break; + default: + throw new ArgumentException("Invalid parameters count."); + } + } + catch (ArgumentException ex) + { + throw new SilverbackException( + $"The apply method for the event of type {@event.GetType().Name} " + + $"in entity {entity.GetType().Name} has an invalid signature.", ex); + } + } + } +} \ No newline at end of file diff --git a/src/Silverback.EventSourcing/Domain/Util/PropertiesMapper.cs b/src/Silverback.EventSourcing/Domain/Util/PropertiesMapper.cs new file mode 100644 index 000000000..0fe6bd980 --- /dev/null +++ b/src/Silverback.EventSourcing/Domain/Util/PropertiesMapper.cs @@ -0,0 +1,58 @@ +// Copyright (c) 2018-2019 Sergio Aquilini +// This code is licensed under MIT license (see LICENSE file for details) + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Silverback.Domain.Util +{ + internal static class PropertiesMapper + { + public static void Map(object source, object destination) + { + var sourceProperties = source.GetType().GetProperties(); + var destProperties = destination.GetType().GetProperties(); + var destTypeName = destination.GetType().Name; + + foreach (var sourceProperty in sourceProperties) + { + if (TryMapProperty(source, destination, sourceProperty, destProperties, sourceProperty.Name)) + continue; + + if (sourceProperty.Name.StartsWith(destTypeName)) + if (TryMapProperty(source, destination, sourceProperty, destProperties, sourceProperty.Name.Substring(destTypeName.Length))) + continue; + + if (sourceProperty.Name.StartsWith("Entity")) + TryMapProperty(source, destination, sourceProperty, destProperties, sourceProperty.Name.Substring("Entity".Length)); + } + } + + private static bool TryMapProperty(object source, object destination, PropertyInfo sourcePropertyInfo, + IEnumerable destPropertiesInfo, string destPropertyName) + { + var destPropertyInfo = destPropertiesInfo.SingleOrDefault(p => p.Name == destPropertyName); + if (destPropertyInfo == null) + return false; + + var setterMethod = destPropertyInfo.GetSetMethod(true); + if (setterMethod == null) + return false; + + try + { + setterMethod.Invoke(destination, new[] {sourcePropertyInfo.GetValue(source)}); + return true; + } + catch (Exception ex) + { + throw new SilverbackException( + $"Couldn't map property {sourcePropertyInfo.DeclaringType.Name}.{sourcePropertyInfo.Name} " + + $"to {destPropertyInfo.DeclaringType.Name}.{destPropertyInfo.Name}." + + $"See inner exception for details.", ex); + } + } + } +} \ No newline at end of file diff --git a/src/Silverback.EventSourcing/EventStore/EventEntity.cs b/src/Silverback.EventSourcing/EventStore/EventEntity.cs new file mode 100644 index 000000000..e92cfeb01 --- /dev/null +++ b/src/Silverback.EventSourcing/EventStore/EventEntity.cs @@ -0,0 +1,16 @@ +// Copyright (c) 2018-2019 Sergio Aquilini +// This code is licensed under MIT license (see LICENSE file for details) + +using System; + +namespace Silverback.EventStore +{ + public abstract class EventEntity : IEventEntity + { + public DateTime Timestamp { get; set; } + + public int Sequence { get; set; } + + public string SerializedEvent { get; set; } + } +} \ No newline at end of file diff --git a/src/Silverback.EventSourcing/EventStore/EventSerializer.cs b/src/Silverback.EventSourcing/EventStore/EventSerializer.cs new file mode 100644 index 000000000..6392a89ac --- /dev/null +++ b/src/Silverback.EventSourcing/EventStore/EventSerializer.cs @@ -0,0 +1,24 @@ +// Copyright (c) 2018-2019 Sergio Aquilini +// This code is licensed under MIT license (see LICENSE file for details) + +using Newtonsoft.Json; +using Silverback.Domain; + +namespace Silverback.EventStore +{ + internal static class EventSerializer + { + private static readonly JsonSerializerSettings SerializerSettings = new JsonSerializerSettings + { + Formatting = Formatting.None, + DateFormatHandling = DateFormatHandling.IsoDateFormat, + NullValueHandling = NullValueHandling.Ignore, + DefaultValueHandling = DefaultValueHandling.Ignore, + TypeNameHandling = TypeNameHandling.Auto + }; + + public static string Serialize(IEntityEvent @event) => JsonConvert.SerializeObject(@event, typeof(IEntityEvent), SerializerSettings); + + public static IEntityEvent Deserialize(string json) => JsonConvert.DeserializeObject(json, SerializerSettings); + } +} \ No newline at end of file diff --git a/src/Silverback.EventSourcing/EventStore/EventStoreEntity.cs b/src/Silverback.EventSourcing/EventStore/EventStoreEntity.cs new file mode 100644 index 000000000..bd4f1ca5e --- /dev/null +++ b/src/Silverback.EventSourcing/EventStore/EventStoreEntity.cs @@ -0,0 +1,19 @@ +// Copyright (c) 2018-2019 Sergio Aquilini +// This code is licensed under MIT license (see LICENSE file for details) + +using System.Collections.Generic; +using Silverback.Messaging.Messages; +using Silverback.Util; + +namespace Silverback.EventStore +{ + public class EventStoreEntity : MessagesSource, IEventStoreEntity + where TEventEntity : IEventEntity + { + public ICollection Events { get; } = new HashSet(); // TODO: HashSet? + + public int EntityVersion { get; set; } + + public void AddDomainEvents(IEnumerable events) => events?.ForEach(AddEvent); + } +} diff --git a/src/Silverback.EventSourcing/EventStore/EventStoreRepository.cs b/src/Silverback.EventSourcing/EventStore/EventStoreRepository.cs new file mode 100644 index 000000000..72235849c --- /dev/null +++ b/src/Silverback.EventSourcing/EventStore/EventStoreRepository.cs @@ -0,0 +1,95 @@ +// Copyright (c) 2018-2019 Sergio Aquilini +// This code is licensed under MIT license (see LICENSE file for details) + +using System; +using System.Linq; +using System.Threading.Tasks; +using Silverback.Domain; +using Silverback.Domain.Util; +using Silverback.Messaging.Messages; +using Silverback.Util; + +namespace Silverback.EventStore +{ + public abstract class EventStoreRepository + where TAggregateEntity : IEventSourcingAggregate + where TEventStoreEntity : IEventStoreEntity + where TEventEntity : IEventEntity, new() + { + public TEventStoreEntity Store(TAggregateEntity aggregateEntity) => + Store(aggregateEntity, GetEventStoreEntity(aggregateEntity, true)); + + public async Task StoreAsync(TAggregateEntity aggregateEntity) => + Store(aggregateEntity, await GetEventStoreEntityAsync(aggregateEntity, true)); + + public TEventStoreEntity Remove(TAggregateEntity aggregateEntity) => + Remove(aggregateEntity, GetEventStoreEntity(aggregateEntity, false)); + + public async Task RemoveAsync(TAggregateEntity aggregateEntity) => + Remove(aggregateEntity, await GetEventStoreEntityAsync(aggregateEntity, false)); + + protected abstract TEventStoreEntity GetEventStoreEntity(TAggregateEntity aggregateEntity, bool addIfNotFound); + + protected abstract Task GetEventStoreEntityAsync(TAggregateEntity aggregateEntity, bool addIfNotFound); + + protected virtual TAggregateEntity GetAggregateEntity(TEventStoreEntity eventStore, DateTime? snapshot = null) + { + if (eventStore == null) + return default; + + var events = snapshot != null + ? eventStore.Events.Where(e => e.Timestamp <= snapshot) + : eventStore.Events; + + return EntityActivator.CreateInstance( + events + .OrderBy(e => e.Timestamp).ThenBy(e => e.Sequence) + .Select(GetEvent), + eventStore); + } + + protected virtual TEventEntity GetEventEntity(IEntityEvent @event) => + new TEventEntity + { + SerializedEvent = EventSerializer.Serialize(@event), + Timestamp = @event.Timestamp, + Sequence = @event.Sequence + }; + + protected virtual IEntityEvent GetEvent(TEventEntity e) + { + var @event = EventSerializer.Deserialize(e.SerializedEvent); + @event.Sequence = e.Sequence; + @event.Timestamp = e.Timestamp; + return @event; + } + + protected abstract TEventStoreEntity Remove(TAggregateEntity aggregateEntity, TEventStoreEntity eventStore); + + private TEventStoreEntity Store(TAggregateEntity aggregateEntity, TEventStoreEntity eventStore) + { + var newEvents = aggregateEntity.GetNewEvents(); + + eventStore.EntityVersion = eventStore.EntityVersion + newEvents.Count(); + + if (eventStore.EntityVersion != aggregateEntity.GetVersion()) + throw new SilverbackConcurrencyException( + $"Expected to save version {aggregateEntity.GetVersion()} but new version was {eventStore.EntityVersion}. " + + "Refresh the aggregate entity and reapply the new events."); + + newEvents.ForEach(@event => eventStore.Events.Add( + GetEventEntity(@event))); + + // Move the domain events from the aggregate to the event store entity + // being persisted, in order for Silverback to automatically publish them + // (even if the aggregate itself is not stored). + if (aggregateEntity is IMessagesSource messagesSource) + { + eventStore.AddDomainEvents(messagesSource.GetMessages()); + messagesSource.ClearMessages(); + } + + return eventStore; + } + } +} \ No newline at end of file diff --git a/src/Silverback.EventSourcing/EventStore/IEventEntity.cs b/src/Silverback.EventSourcing/EventStore/IEventEntity.cs new file mode 100644 index 000000000..6ae2af9fd --- /dev/null +++ b/src/Silverback.EventSourcing/EventStore/IEventEntity.cs @@ -0,0 +1,16 @@ +// Copyright (c) 2018-2019 Sergio Aquilini +// This code is licensed under MIT license (see LICENSE file for details) + +using System; + +namespace Silverback.EventStore +{ + public interface IEventEntity + { + DateTime Timestamp { get; set; } + + int Sequence { get; set; } + + string SerializedEvent { get; set; } + } +} \ No newline at end of file diff --git a/src/Silverback.EventSourcing/EventStore/IEventSourcingAggregate.cs b/src/Silverback.EventSourcing/EventStore/IEventSourcingAggregate.cs new file mode 100644 index 000000000..3bfe298de --- /dev/null +++ b/src/Silverback.EventSourcing/EventStore/IEventSourcingAggregate.cs @@ -0,0 +1,20 @@ +// Copyright (c) 2018-2019 Sergio Aquilini +// This code is licensed under MIT license (see LICENSE file for details) + +using System.Collections.Generic; +using Silverback.Domain; + +namespace Silverback.EventStore +{ + public interface IEventSourcingAggregate + { + int GetVersion(); + + IEnumerable GetNewEvents(); + } + + public interface IEventSourcingAggregate : IEventSourcingAggregate + { + TKey Id { get; } + } +} \ No newline at end of file diff --git a/src/Silverback.EventSourcing/EventStore/IEventStoreEntity.cs b/src/Silverback.EventSourcing/EventStore/IEventStoreEntity.cs new file mode 100644 index 000000000..db1256119 --- /dev/null +++ b/src/Silverback.EventSourcing/EventStore/IEventStoreEntity.cs @@ -0,0 +1,17 @@ +// Copyright (c) 2018-2019 Sergio Aquilini +// This code is licensed under MIT license (see LICENSE file for details) + +using System.Collections.Generic; + +namespace Silverback.EventStore +{ + public interface IEventStoreEntity + where TEventEntity : IEventEntity + { + ICollection Events { get; } + + int EntityVersion { get; set; } + + void AddDomainEvents(IEnumerable events); + } +} \ No newline at end of file diff --git a/src/Silverback.EventSourcing/Properties/AssemblyInfo.cs b/src/Silverback.EventSourcing/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..5307e496e --- /dev/null +++ b/src/Silverback.EventSourcing/Properties/AssemblyInfo.cs @@ -0,0 +1,6 @@ +// Copyright (c) 2018-2019 Sergio Aquilini +// This code is licensed under MIT license (see LICENSE file for details) + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Silverback.EventSourcing.Tests")] \ No newline at end of file diff --git a/src/Silverback.EventSourcing/Silverback.EventSourcing.csproj b/src/Silverback.EventSourcing/Silverback.EventSourcing.csproj new file mode 100644 index 000000000..13354bf4e --- /dev/null +++ b/src/Silverback.EventSourcing/Silverback.EventSourcing.csproj @@ -0,0 +1,30 @@ + + + + netstandard2.0 + Silverback + true + 0.10.0.0 + BEagle1984 + BEagle1984 + Silverback is a simple framework to build reactive, event-driven, microservices. This package contains the event store abstraction. + MIT + https://github.com/BEagle1984/silverback/ + https://github.com/BEagle1984/silverback/raw/develop/graphics/Icon.png + latest + 1701;1702;CS1591 + + + + bin\Debug\netstandard2.0\Silverback.EventSourcing.xml + + + + bin\Release\netstandard2.0\Silverback.EventSourcing.xml + + + + + + + diff --git a/src/Silverback.Integration.Configuration/Silverback.Integration.Configuration.csproj b/src/Silverback.Integration.Configuration/Silverback.Integration.Configuration.csproj index 688130fa5..6590ccb37 100644 --- a/src/Silverback.Integration.Configuration/Silverback.Integration.Configuration.csproj +++ b/src/Silverback.Integration.Configuration/Silverback.Integration.Configuration.csproj @@ -4,19 +4,29 @@ netstandard2.0 Silverback true - 0.6.1.0 + 0.10.0.0 BEagle1984 Silverback is a simple framework to build reactive, event-driven, microservices. This package contains the logic to read the broker endpoints configuration from the IConfiguration (see Microsoft.Extensions.Configuration). - https://github.com/BEagle1984/silverback/blob/master/LICENSE + MIT https://github.com/BEagle1984/silverback/ https://github.com/BEagle1984/silverback/raw/develop/graphics/Icon.png + latest + 1701;1702;CS1591 + + + + bin\Debug\netstandard2.0\Silverback.Integration.Configuration.xml + + + + bin\Release\netstandard2.0\Silverback.Integration.Configuration.xml - + diff --git a/src/Silverback.Integration.EntityFrameworkCore/Infrastructure/DefaultSerializer.cs b/src/Silverback.Integration.EntityFrameworkCore/Infrastructure/DefaultSerializer.cs index fd14461b4..76b605d62 100644 --- a/src/Silverback.Integration.EntityFrameworkCore/Infrastructure/DefaultSerializer.cs +++ b/src/Silverback.Integration.EntityFrameworkCore/Infrastructure/DefaultSerializer.cs @@ -5,7 +5,7 @@ namespace Silverback.Infrastructure { - public static class DefaultSerializer + internal static class DefaultSerializer { private static readonly JsonSerializerSettings SerializerSettings = new JsonSerializerSettings { diff --git a/src/Silverback.Integration.EntityFrameworkCore/Messaging/Configuration/BrokerOptionsBuilderExtensions.cs b/src/Silverback.Integration.EntityFrameworkCore/Messaging/Configuration/BrokerOptionsBuilderExtensions.cs index 4014d27df..70b34738d 100644 --- a/src/Silverback.Integration.EntityFrameworkCore/Messaging/Configuration/BrokerOptionsBuilderExtensions.cs +++ b/src/Silverback.Integration.EntityFrameworkCore/Messaging/Configuration/BrokerOptionsBuilderExtensions.cs @@ -1,9 +1,11 @@ // Copyright (c) 2018-2019 Sergio Aquilini // This code is licensed under MIT license (see LICENSE file for details) +using System; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Silverback.Background; using Silverback.Messaging.Connectors; using Silverback.Messaging.Connectors.Repositories; using Silverback.Messaging.LargeMessages; @@ -58,15 +60,42 @@ public static BrokerOptionsBuilder AddDbOutboundConnector(this Broke /// /// Adds an to publish the queued messages to the configured broker. /// + /// + /// The interval between each run (default is 500ms). /// if set to true the message order will be preserved (no message will be skipped). /// The number of messages to be loaded from the queue at once. /// if set to true the messages will be removed from the database immediately after being produced. public static BrokerOptionsBuilder AddDbOutboundWorker(this BrokerOptionsBuilder builder, + TimeSpan? interval = null, bool enforceMessageOrder = true, int readPackageSize = 100, + bool removeProduced = true) + where TDbContext : DbContext + { + return builder.AddDbOutboundWorker(new DistributedLockSettings(), interval, + enforceMessageOrder, readPackageSize); + } + + /// + /// Adds an to publish the queued messages to the configured broker. + /// + /// + /// The settings for the locking mechanism. + /// The interval between each run (default is 500ms). + /// if set to true the message order will be preserved (no message will be skipped). + /// The number of messages to be loaded from the queue at once. + /// if set to true the messages will be removed from the database immediately after being produced. + public static BrokerOptionsBuilder AddDbOutboundWorker(this BrokerOptionsBuilder builder, + DistributedLockSettings distributedLockSettings, TimeSpan? interval = null, bool enforceMessageOrder = true, int readPackageSize = 100, bool removeProduced = true) where TDbContext : DbContext { - builder.AddOutboundWorker(s => - new DbContextOutboundQueueConsumer(s.GetRequiredService(), removeProduced), + if (distributedLockSettings == null) throw new ArgumentNullException(nameof(distributedLockSettings)); + + if (string.IsNullOrEmpty(distributedLockSettings.ResourceName)) + distributedLockSettings.ResourceName = $"OutboundQueueWorker[{typeof(TDbContext).Name}]"; + + builder.AddOutboundWorker( + s => new DbContextOutboundQueueConsumer(s.GetRequiredService(), removeProduced), + distributedLockSettings, interval, enforceMessageOrder, readPackageSize); return builder; diff --git a/src/Silverback.Integration.EntityFrameworkCore/Messaging/Connectors/Repositories/DbContextInboundLog.cs b/src/Silverback.Integration.EntityFrameworkCore/Messaging/Connectors/Repositories/DbContextInboundLog.cs index e615f3f34..bd08c0d90 100644 --- a/src/Silverback.Integration.EntityFrameworkCore/Messaging/Connectors/Repositories/DbContextInboundLog.cs +++ b/src/Silverback.Integration.EntityFrameworkCore/Messaging/Connectors/Repositories/DbContextInboundLog.cs @@ -5,8 +5,8 @@ using System.Linq; using Microsoft.EntityFrameworkCore; using Silverback.Infrastructure; -using Silverback.Messaging.Connectors.Model; using Silverback.Messaging.Messages; +using InboundMessage = Silverback.Messaging.Connectors.Model.InboundMessage; namespace Silverback.Messaging.Connectors.Repositories { diff --git a/src/Silverback.Integration.EntityFrameworkCore/Messaging/Connectors/Repositories/DbContextOutboundQueueConsumer.cs b/src/Silverback.Integration.EntityFrameworkCore/Messaging/Connectors/Repositories/DbContextOutboundQueueConsumer.cs index 0ca658535..01ca4e06b 100644 --- a/src/Silverback.Integration.EntityFrameworkCore/Messaging/Connectors/Repositories/DbContextOutboundQueueConsumer.cs +++ b/src/Silverback.Integration.EntityFrameworkCore/Messaging/Connectors/Repositories/DbContextOutboundQueueConsumer.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Silverback.Infrastructure; using Silverback.Messaging.Connectors.Model; @@ -21,33 +22,35 @@ public DbContextOutboundQueueConsumer(DbContext dbContext, bool removeProduced) _removeProduced = removeProduced; } - public IEnumerable Dequeue(int count) => DbSet - .Where(m => m.Produced == null) - .OrderBy(m => m.Id) - .Take(count) - .ToList() + public async Task> Dequeue(int count) => + (await DbSet + .Where(m => m.Produced == null) + .OrderBy(m => m.Id) + .Take(count) + .ToListAsync()) .Select(message => new DbQueuedMessage( - message.Id, + message.Id, DefaultSerializer.Deserialize(message.Message))); - public void Retry(QueuedMessage queuedMessage) + public Task Retry(QueuedMessage queuedMessage) { // Nothing to do, the message is retried if not marked as produced + return Task.CompletedTask; } - public void Acknowledge(QueuedMessage queuedMessage) + public async Task Acknowledge(QueuedMessage queuedMessage) { if (!(queuedMessage is DbQueuedMessage dbQueuedMessage)) throw new InvalidOperationException("A DbQueuedMessage is expected."); - var entity = DbSet.Find(dbQueuedMessage.Id); + var entity = await DbSet.FindAsync(dbQueuedMessage.Id); if (_removeProduced) DbSet.Remove(entity); else entity.Produced = DateTime.UtcNow; - DbContext.SaveChanges(); + await DbContext.SaveChangesAsync(); } public int Length => DbSet.Count(m => m.Produced == null); diff --git a/src/Silverback.Integration.EntityFrameworkCore/Messaging/Connectors/Repositories/QueuedOutboundMessage.cs b/src/Silverback.Integration.EntityFrameworkCore/Messaging/Connectors/Repositories/QueuedOutboundMessage.cs index 9f318ce3e..97047db33 100644 --- a/src/Silverback.Integration.EntityFrameworkCore/Messaging/Connectors/Repositories/QueuedOutboundMessage.cs +++ b/src/Silverback.Integration.EntityFrameworkCore/Messaging/Connectors/Repositories/QueuedOutboundMessage.cs @@ -1,5 +1,6 @@ // Copyright (c) 2018-2019 Sergio Aquilini // This code is licensed under MIT license (see LICENSE file for details) + namespace Silverback.Messaging.Connectors.Repositories { class QueuedOutboundMessage diff --git a/src/Silverback.Integration.EntityFrameworkCore/Silverback.Integration.EntityFrameworkCore.csproj b/src/Silverback.Integration.EntityFrameworkCore/Silverback.Integration.EntityFrameworkCore.csproj index 1e4ec58a4..f0063231a 100644 --- a/src/Silverback.Integration.EntityFrameworkCore/Silverback.Integration.EntityFrameworkCore.csproj +++ b/src/Silverback.Integration.EntityFrameworkCore/Silverback.Integration.EntityFrameworkCore.csproj @@ -2,23 +2,32 @@ netstandard2.0 - 0.6.1.0 + 0.10.0.0 Silverback true - 7.1 BEagle1984 Silverback is a simple framework to build reactive, event-driven, microservices. This package enables the creation of the inbound and outbound messages DbSet and/or the temporary storage of message chunks in the database using Entity Framework Core. - https://github.com/BEagle1984/silverback/blob/master/LICENSE + MIT https://github.com/BEagle1984/silverback/ https://github.com/BEagle1984/silverback/raw/develop/graphics/Icon.png + latest + 1701;1702;CS1591 + + + + bin\Debug\netstandard2.0\Silverback.Integration.EntityFrameworkCore.xml + + + + bin\Release\netstandard2.0\Silverback.Integration.EntityFrameworkCore.xml - + - - + + diff --git a/src/Silverback.Integration.InMemory/Messaging/Broker/InMemoryBroker.cs b/src/Silverback.Integration.InMemory/Messaging/Broker/InMemoryBroker.cs index 4d3a74353..5652057e0 100644 --- a/src/Silverback.Integration.InMemory/Messaging/Broker/InMemoryBroker.cs +++ b/src/Silverback.Integration.InMemory/Messaging/Broker/InMemoryBroker.cs @@ -1,9 +1,9 @@ -using System; +// Copyright (c) 2018-2019 Sergio Aquilini +// This code is licensed under MIT license (see LICENSE file for details) + using System.Collections.Concurrent; using System.Collections.Generic; using Microsoft.Extensions.Logging; -using Silverback.Messaging; -using Silverback.Messaging.Broker; using Silverback.Messaging.Messages; namespace Silverback.Messaging.Broker @@ -28,7 +28,7 @@ protected override Producer InstantiateProducer(IEndpoint endpoint) => protected override Consumer InstantiateConsumer(IEndpoint endpoint) => GetTopic(endpoint.Name).Subscribe( - new InMemoryConsumer(this, endpoint, LoggerFactory.CreateLogger(), _messageLogger)); + new InMemoryConsumer(this, endpoint)); protected override void Connect(IEnumerable consumers) { diff --git a/src/Silverback.Integration.InMemory/Messaging/Broker/InMemoryConsumer.cs b/src/Silverback.Integration.InMemory/Messaging/Broker/InMemoryConsumer.cs index 68cd02d27..fecb7caf1 100644 --- a/src/Silverback.Integration.InMemory/Messaging/Broker/InMemoryConsumer.cs +++ b/src/Silverback.Integration.InMemory/Messaging/Broker/InMemoryConsumer.cs @@ -9,7 +9,7 @@ namespace Silverback.Messaging.Broker { public class InMemoryConsumer : Consumer { - public InMemoryConsumer(IBroker broker, IEndpoint endpoint, ILogger logger, MessageLogger messageLogger) : base(broker, endpoint, logger, messageLogger) + public InMemoryConsumer(IBroker broker, IEndpoint endpoint) : base(broker, endpoint) { } diff --git a/src/Silverback.Integration.InMemory/Messaging/Broker/InMemoryProducer.cs b/src/Silverback.Integration.InMemory/Messaging/Broker/InMemoryProducer.cs index 5f965be19..2db479af5 100644 --- a/src/Silverback.Integration.InMemory/Messaging/Broker/InMemoryProducer.cs +++ b/src/Silverback.Integration.InMemory/Messaging/Broker/InMemoryProducer.cs @@ -16,13 +16,16 @@ public InMemoryProducer(IBroker broker, IEndpoint endpoint, MessageKeyProvider m { } - protected override void Produce(object message, byte[] serializedMessage, IEnumerable headers) => + protected override IOffset Produce(object message, byte[] serializedMessage, IEnumerable headers) + { Broker.GetTopic(Endpoint.Name).Publish(serializedMessage, headers); + return null; + } - protected override Task ProduceAsync(object message, byte[] serializedMessage, IEnumerable headers) + protected override Task ProduceAsync(object message, byte[] serializedMessage, IEnumerable headers) { Produce(message, serializedMessage, headers); - return Task.CompletedTask; + return Task.FromResult(null); } } } \ No newline at end of file diff --git a/src/Silverback.Integration.InMemory/Silverback.Integration.InMemory.csproj b/src/Silverback.Integration.InMemory/Silverback.Integration.InMemory.csproj index 236dc41ea..ff14b1aba 100644 --- a/src/Silverback.Integration.InMemory/Silverback.Integration.InMemory.csproj +++ b/src/Silverback.Integration.InMemory/Silverback.Integration.InMemory.csproj @@ -4,17 +4,27 @@ netstandard2.0 Silverback true - 0.6.1.0 + 0.10.0.0 BEagle1984 BEagle1984 Silverback is a simple framework to build reactive, event-driven, microservices. This package includes a mocked message broker to be used for testing only. - https://github.com/BEagle1984/silverback/blob/master/LICENSE + MIT https://github.com/BEagle1984/silverback/ https://github.com/BEagle1984/silverback/raw/develop/graphics/Icon.png + latest + 1701;1702;CS1591 + + + + bin\Debug\netstandard2.0\Silverback.Integration.InMemory.xml + + + + bin\Release\netstandard2.0\Silverback.Integration.InMemory.xml - + diff --git a/src/Silverback.Integration.Kafka/Messaging/Broker/KafkaBroker.cs b/src/Silverback.Integration.Kafka/Messaging/Broker/KafkaBroker.cs index d32ec270b..eb671702f 100644 --- a/src/Silverback.Integration.Kafka/Messaging/Broker/KafkaBroker.cs +++ b/src/Silverback.Integration.Kafka/Messaging/Broker/KafkaBroker.cs @@ -29,7 +29,7 @@ protected override Producer InstantiateProducer(IEndpoint endpoint) => new KafkaProducer(this, (KafkaProducerEndpoint) endpoint, _messageKeyProvider, _loggerFactory.CreateLogger(), _messageLogger); protected override Consumer InstantiateConsumer(IEndpoint endpoint) => - new KafkaConsumer(this, (KafkaConsumerEndpoint) endpoint, _loggerFactory.CreateLogger(), _messageLogger); + new KafkaConsumer(this, (KafkaConsumerEndpoint) endpoint, _loggerFactory.CreateLogger()); protected override void Connect(IEnumerable consumers) => consumers.Cast().ToList().ForEach(c => c.Connect()); diff --git a/src/Silverback.Integration.Kafka/Messaging/Broker/KafkaConsumer.cs b/src/Silverback.Integration.Kafka/Messaging/Broker/KafkaConsumer.cs index a9aa950da..b8472c6ed 100644 --- a/src/Silverback.Integration.Kafka/Messaging/Broker/KafkaConsumer.cs +++ b/src/Silverback.Integration.Kafka/Messaging/Broker/KafkaConsumer.cs @@ -18,9 +18,8 @@ public class KafkaConsumer : Consumer, IDisp private InnerConsumerWrapper _innerConsumer; private int _messagesSinceCommit = 0; - public KafkaConsumer(IBroker broker, KafkaConsumerEndpoint endpoint, ILogger logger, - MessageLogger messageLogger) - : base(broker, endpoint, logger, messageLogger) + public KafkaConsumer(IBroker broker, KafkaConsumerEndpoint endpoint, ILogger logger) + : base(broker, endpoint) { _logger = logger; diff --git a/src/Silverback.Integration.Kafka/Messaging/Broker/KafkaProducer.cs b/src/Silverback.Integration.Kafka/Messaging/Broker/KafkaProducer.cs index ec2a8dea4..afbc51dd9 100644 --- a/src/Silverback.Integration.Kafka/Messaging/Broker/KafkaProducer.cs +++ b/src/Silverback.Integration.Kafka/Messaging/Broker/KafkaProducer.cs @@ -29,10 +29,10 @@ public KafkaProducer(KafkaBroker broker, KafkaProducerEndpoint endpoint, Message Endpoint.Validate(); } - protected override void Produce(object message, byte[] serializedMessage, IEnumerable headers) => - Task.Run(() => ProduceAsync(message, serializedMessage, headers)).Wait(); + protected override IOffset Produce(object message, byte[] serializedMessage, IEnumerable headers) => + AsyncHelper.RunSynchronously(() => ProduceAsync(message, serializedMessage, headers)); - protected override async Task ProduceAsync(object message, byte[] serializedMessage, IEnumerable headers) + protected override async Task ProduceAsync(object message, byte[] serializedMessage, IEnumerable headers) { var kafkaMessage = new Confluent.Kafka.Message { @@ -47,9 +47,8 @@ protected override async Task ProduceAsync(object message, byte[] serializedMess } var deliveryReport = await GetInnerProducer().ProduceAsync(Endpoint.Name, kafkaMessage); - _logger.LogTrace( - "Successfully produced: {topic} {partition} @{offset}.", - deliveryReport.Topic, deliveryReport.Partition, deliveryReport.Offset); + + return new KafkaOffset(deliveryReport.TopicPartitionOffset); } private Confluent.Kafka.IProducer GetInnerProducer() => diff --git a/src/Silverback.Integration.Kafka/Messaging/Broker/MessageReceivedHandler.cs b/src/Silverback.Integration.Kafka/Messaging/Broker/MessageReceivedHandler.cs index ed4ca1cbe..19991cf63 100644 --- a/src/Silverback.Integration.Kafka/Messaging/Broker/MessageReceivedHandler.cs +++ b/src/Silverback.Integration.Kafka/Messaging/Broker/MessageReceivedHandler.cs @@ -1,5 +1,6 @@ // Copyright (c) 2018-2019 Sergio Aquilini // This code is licensed under MIT license (see LICENSE file for details) + namespace Silverback.Messaging.Broker { internal delegate void MessageReceivedHandler(Confluent.Kafka.Message message, Confluent.Kafka.TopicPartitionOffset tpo); diff --git a/src/Silverback.Integration.Kafka/Messaging/Proxies/ConfluentConsumerConfigProxy.cs b/src/Silverback.Integration.Kafka/Messaging/Proxies/ConfluentConsumerConfigProxy.cs index 78a3ce938..5d2c7401c 100644 --- a/src/Silverback.Integration.Kafka/Messaging/Proxies/ConfluentConsumerConfigProxy.cs +++ b/src/Silverback.Integration.Kafka/Messaging/Proxies/ConfluentConsumerConfigProxy.cs @@ -1,228 +1,235 @@ // Copyright (c) 2018-2019 Sergio Aquilini // This code is licensed under MIT license (see LICENSE file for details) + namespace Silverback.Messaging.Proxies { public class ConfluentConsumerConfigProxy { internal Confluent.Kafka.ConsumerConfig ConfluentConfig { get; } = new Confluent.Kafka.ConsumerConfig(); - /// A comma separated list of fields that may be optionally set in objects returned by the method. Disabling fields that you do not require will improve throughput and reduce memory consumption. Allowed values: headers, timestamp, topic, all, none default: all - public string ConsumeResultFields { set => ConfluentConfig.ConsumeResultFields = value; } + /// A comma separated list of fields that may be optionally set in objects returned by the method. Disabling fields that you do not require will improve throughput and reduce memory consumption. Allowed values: headers, timestamp, topic, all, none default: all importance: low + public string ConsumeResultFields {set => ConfluentConfig.ConsumeResultFields = value; } + + /// Action to take when there is no initial offset in offset store or the desired offset is out of range: 'smallest','earliest' - automatically reset the offset to the smallest offset, 'largest','latest' - automatically reset the offset to the largest offset, 'error' - trigger an error which is retrieved by consuming messages and checking 'message->err'. default: largest importance: high + public Confluent.Kafka.AutoOffsetReset? AutoOffsetReset { get => ConfluentConfig.AutoOffsetReset; set => ConfluentConfig.AutoOffsetReset = value; } + /// Client group id string. All clients sharing the same group.id belong to the same group. default: '' importance: high public string GroupId { get => ConfluentConfig.GroupId; set => ConfluentConfig.GroupId = value; } + /// Name of partition assignment strategy to use when elected group leader assigns partitions to group members. default: range,roundrobin importance: medium public Confluent.Kafka.PartitionAssignmentStrategy? PartitionAssignmentStrategy { get => ConfluentConfig.PartitionAssignmentStrategy; set => ConfluentConfig.PartitionAssignmentStrategy = value; } + /// Client group session and failure detection timeout. The consumer sends periodic heartbeats (heartbeat.interval.ms) to indicate its liveness to the broker. If no hearts are received by the broker for a group member within the session timeout, the broker will remove the consumer from the group and trigger a rebalance. The allowed range is configured with the **broker** configuration properties `group.min.session.timeout.ms` and `group.max.session.timeout.ms`. Also see `max.poll.interval.ms`. default: 10000 importance: high public int? SessionTimeoutMs { get => ConfluentConfig.SessionTimeoutMs; set => ConfluentConfig.SessionTimeoutMs = value; } + /// Group session keepalive heartbeat interval. default: 3000 importance: low public int? HeartbeatIntervalMs { get => ConfluentConfig.HeartbeatIntervalMs; set => ConfluentConfig.HeartbeatIntervalMs = value; } + /// Group protocol type default: consumer importance: low public string GroupProtocolType { get => ConfluentConfig.GroupProtocolType; set => ConfluentConfig.GroupProtocolType = value; } + /// How often to query for the current client group coordinator. If the currently assigned coordinator is down the configured query interval will be divided by ten to more quickly recover in case of coordinator reassignment. default: 600000 importance: low public int? CoordinatorQueryIntervalMs { get => ConfluentConfig.CoordinatorQueryIntervalMs; set => ConfluentConfig.CoordinatorQueryIntervalMs = value; } + /// Maximum allowed time between calls to consume messages (e.g., rd_kafka_consumer_poll()) for high-level consumers. If this interval is exceeded the consumer is considered failed and the group will rebalance in order to reassign the partitions to another consumer group member. Warning: Offset commits may be not possible at this point. Note: It is recommended to set `enable.auto.offset.store=false` for long-time processing applications and then explicitly store offsets (using offsets_store()) *after* message processing, to make sure offsets are not auto-committed prior to processing has finished. The interval is checked two times per second. See KIP-62 for more information. default: 300000 importance: high public int? MaxPollIntervalMs { get => ConfluentConfig.MaxPollIntervalMs; set => ConfluentConfig.MaxPollIntervalMs = value; } - /// Automatically and periodically commit offsets in the background. Note: setting this to false does not prevent the consumer from fetching previously committed start offsets. To circumvent this behaviour set specific start offsets per partition in the call to assign(). + /// Automatically and periodically commit offsets in the background. Note: setting this to false does not prevent the consumer from fetching previously committed start offsets. To circumvent this behaviour set specific start offsets per partition in the call to assign(). default: true importance: high public bool? EnableAutoCommit { get => ConfluentConfig.EnableAutoCommit; set => ConfluentConfig.EnableAutoCommit = value; } - /// The frequency in milliseconds that the consumer offsets are committed (written) to offset storage. (0 = disable). This setting is used by the high-level consumer. + /// The frequency in milliseconds that the consumer offsets are committed (written) to offset storage. (0 = disable). This setting is used by the high-level consumer. default: 5000 importance: medium public int? AutoCommitIntervalMs { get => ConfluentConfig.AutoCommitIntervalMs; set => ConfluentConfig.AutoCommitIntervalMs = value; } - /// Automatically store offset of last message provided to application. + /// Automatically store offset of last message provided to application. The offset store is an in-memory store of the next offset to (auto-)commit for each partition. default: true importance: high public bool? EnableAutoOffsetStore { get => ConfluentConfig.EnableAutoOffsetStore; set => ConfluentConfig.EnableAutoOffsetStore = value; } - /// Minimum number of messages per topic+partition librdkafka tries to maintain in the local consumer queue. + /// Minimum number of messages per topic+partition librdkafka tries to maintain in the local consumer queue. default: 100000 importance: medium public int? QueuedMinMessages { get => ConfluentConfig.QueuedMinMessages; set => ConfluentConfig.QueuedMinMessages = value; } - /// Maximum number of kilobytes per topic+partition in the local consumer queue. This value may be overshot by fetch.message.max.bytes. This property has higher priority than queued.min.messages. + /// Maximum number of kilobytes per topic+partition in the local consumer queue. This value may be overshot by fetch.message.max.bytes. This property has higher priority than queued.min.messages. default: 1048576 importance: medium public int? QueuedMaxMessagesKbytes { get => ConfluentConfig.QueuedMaxMessagesKbytes; set => ConfluentConfig.QueuedMaxMessagesKbytes = value; } - /// Maximum time the broker may wait to fill the response with fetch.min.bytes. + /// Maximum time the broker may wait to fill the response with fetch.min.bytes. default: 100 importance: low public int? FetchWaitMaxMs { get => ConfluentConfig.FetchWaitMaxMs; set => ConfluentConfig.FetchWaitMaxMs = value; } - /// Initial maximum number of bytes per topic+partition to request when fetching messages from the broker. If the client encounters a message larger than this value it will gradually try to increase it until the entire message can be fetched. + /// Initial maximum number of bytes per topic+partition to request when fetching messages from the broker. If the client encounters a message larger than this value it will gradually try to increase it until the entire message can be fetched. default: 1048576 importance: medium public int? MaxPartitionFetchBytes { get => ConfluentConfig.MaxPartitionFetchBytes; set => ConfluentConfig.MaxPartitionFetchBytes = value; } - /// Maximum amount of data the broker shall return for a Fetch request. Messages are fetched in batches by the consumer and if the first message batch in the first non-empty partition of the Fetch request is larger than this value, then the message batch will still be returned to ensure the consumer can make progress. The maximum message batch size accepted by the broker is defined via `message.max.bytes` (broker config) or `max.message.bytes` (broker topic config). `fetch.max.bytes` is automatically adjusted upwards to be at least `message.max.bytes` (consumer config). + /// Maximum amount of data the broker shall return for a Fetch request. Messages are fetched in batches by the consumer and if the first message batch in the first non-empty partition of the Fetch request is larger than this value, then the message batch will still be returned to ensure the consumer can make progress. The maximum message batch size accepted by the broker is defined via `message.max.bytes` (broker config) or `max.message.bytes` (broker topic config). `fetch.max.bytes` is automatically adjusted upwards to be at least `message.max.bytes` (consumer config). default: 52428800 importance: medium public int? FetchMaxBytes { get => ConfluentConfig.FetchMaxBytes; set => ConfluentConfig.FetchMaxBytes = value; } - /// Minimum number of bytes the broker responds with. If fetch.wait.max.ms expires the accumulated data will be sent to the client regardless of this setting. + /// Minimum number of bytes the broker responds with. If fetch.wait.max.ms expires the accumulated data will be sent to the client regardless of this setting. default: 1 importance: low public int? FetchMinBytes { get => ConfluentConfig.FetchMinBytes; set => ConfluentConfig.FetchMinBytes = value; } - /// How long to postpone the next fetch request for a topic+partition in case of a fetch error. + /// How long to postpone the next fetch request for a topic+partition in case of a fetch error. default: 500 importance: medium public int? FetchErrorBackoffMs { get => ConfluentConfig.FetchErrorBackoffMs; set => ConfluentConfig.FetchErrorBackoffMs = value; } - /// Emit RD_KAFKA_RESP_ERR__PARTITION_EOF event whenever the consumer reaches the end of a partition. + /// Emit RD_KAFKA_RESP_ERR__PARTITION_EOF event whenever the consumer reaches the end of a partition. default: false importance: low public bool? EnablePartitionEof { get => ConfluentConfig.EnablePartitionEof; set => ConfluentConfig.EnablePartitionEof = value; } - /// Verify CRC32 of consumed messages, ensuring no on-the-wire or on-disk corruption to the messages occurred. This check comes at slightly increased CPU usage. + /// Verify CRC32 of consumed messages, ensuring no on-the-wire or on-disk corruption to the messages occurred. This check comes at slightly increased CPU usage. default: false importance: medium public bool? CheckCrcs { get => ConfluentConfig.CheckCrcs; set => ConfluentConfig.CheckCrcs = value; } - /// Action to take when there is no initial offset in offset store or the desired offset is out of range: 'smallest','earliest' - automatically reset the offset to the smallest offset, 'largest','latest' - automatically reset the offset to the largest offset, 'error' - trigger an error which is retrieved by consuming messages and checking 'message->err'. - public Confluent.Kafka.AutoOffsetReset? AutoOffsetReset { get => ConfluentConfig.AutoOffsetReset; set => ConfluentConfig.AutoOffsetReset = value; } - /// SASL mechanism to use for authentication. Supported: GSSAPI, PLAIN, SCRAM-SHA-256, SCRAM-SHA-512. **NOTE**: Despite the name, you may not configure more than one mechanism. public Confluent.Kafka.SaslMechanism? SaslMechanism { get => ConfluentConfig.SaslMechanism; set => ConfluentConfig.SaslMechanism = value; } + /// This field indicates the number of acknowledgements the leader broker must receive from ISR brokers before responding to the request: Zero=Broker does not send any response/ack to client, One=The leader will write the record to its local log but will respond without awaiting full acknowledgement from all followers. All=Broker will block until message is committed by all in sync replicas (ISRs). If there are less than min.insync.replicas (broker configuration) in the ISR set the produce request will fail. public Confluent.Kafka.Acks? Acks { get => ConfluentConfig.Acks; set => ConfluentConfig.Acks = value; } - /// Client identifier. + /// Client identifier. default: rdkafka importance: low public string ClientId { get => ConfluentConfig.ClientId; set => ConfluentConfig.ClientId = value; } - /// Initial list of brokers as a CSV list of broker host or host:port. The application may also use `rd_kafka_brokers_add()` to add brokers during runtime. + /// Initial list of brokers as a CSV list of broker host or host:port. The application may also use `rd_kafka_brokers_add()` to add brokers during runtime. default: '' importance: high public string BootstrapServers { get => ConfluentConfig.BootstrapServers; set => ConfluentConfig.BootstrapServers = value; } - /// Maximum Kafka protocol request message size. + /// Maximum Kafka protocol request message size. default: 1000000 importance: medium public int? MessageMaxBytes { get => ConfluentConfig.MessageMaxBytes; set => ConfluentConfig.MessageMaxBytes = value; } - /// Maximum size for message to be copied to buffer. Messages larger than this will be passed by reference (zero-copy) at the expense of larger iovecs. + /// Maximum size for message to be copied to buffer. Messages larger than this will be passed by reference (zero-copy) at the expense of larger iovecs. default: 65535 importance: low public int? MessageCopyMaxBytes { get => ConfluentConfig.MessageCopyMaxBytes; set => ConfluentConfig.MessageCopyMaxBytes = value; } - /// Maximum Kafka protocol response message size. This serves as a safety precaution to avoid memory exhaustion in case of protocol hickups. This value must be at least `fetch.max.bytes` + 512 to allow for protocol overhead; the value is adjusted automatically unless the configuration property is explicitly set. + /// Maximum Kafka protocol response message size. This serves as a safety precaution to avoid memory exhaustion in case of protocol hickups. This value must be at least `fetch.max.bytes` + 512 to allow for protocol overhead; the value is adjusted automatically unless the configuration property is explicitly set. default: 100000000 importance: medium public int? ReceiveMessageMaxBytes { get => ConfluentConfig.ReceiveMessageMaxBytes; set => ConfluentConfig.ReceiveMessageMaxBytes = value; } - /// Maximum number of in-flight requests per broker connection. This is a generic property applied to all broker communication, however it is primarily relevant to produce requests. In particular, note that other mechanisms limit the number of outstanding consumer fetch request per broker to one. + /// Maximum number of in-flight requests per broker connection. This is a generic property applied to all broker communication, however it is primarily relevant to produce requests. In particular, note that other mechanisms limit the number of outstanding consumer fetch request per broker to one. default: 1000000 importance: low public int? MaxInFlight { get => ConfluentConfig.MaxInFlight; set => ConfluentConfig.MaxInFlight = value; } - /// Non-topic request timeout in milliseconds. This is for metadata requests, etc. + /// Non-topic request timeout in milliseconds. This is for metadata requests, etc. default: 60000 importance: low public int? MetadataRequestTimeoutMs { get => ConfluentConfig.MetadataRequestTimeoutMs; set => ConfluentConfig.MetadataRequestTimeoutMs = value; } - /// Topic metadata refresh interval in milliseconds. The metadata is automatically refreshed on error and connect. Use -1 to disable the intervalled refresh. + /// Topic metadata refresh interval in milliseconds. The metadata is automatically refreshed on error and connect. Use -1 to disable the intervalled refresh. default: 300000 importance: low public int? TopicMetadataRefreshIntervalMs { get => ConfluentConfig.TopicMetadataRefreshIntervalMs; set => ConfluentConfig.TopicMetadataRefreshIntervalMs = value; } - /// Metadata cache max age. Defaults to topic.metadata.refresh.interval.ms * 3 + /// Metadata cache max age. Defaults to topic.metadata.refresh.interval.ms * 3 default: 900000 importance: low public int? MetadataMaxAgeMs { get => ConfluentConfig.MetadataMaxAgeMs; set => ConfluentConfig.MetadataMaxAgeMs = value; } - /// When a topic loses its leader a new metadata request will be enqueued with this initial interval, exponentially increasing until the topic metadata has been refreshed. This is used to recover quickly from transitioning leader brokers. + /// When a topic loses its leader a new metadata request will be enqueued with this initial interval, exponentially increasing until the topic metadata has been refreshed. This is used to recover quickly from transitioning leader brokers. default: 250 importance: low public int? TopicMetadataRefreshFastIntervalMs { get => ConfluentConfig.TopicMetadataRefreshFastIntervalMs; set => ConfluentConfig.TopicMetadataRefreshFastIntervalMs = value; } - /// Sparse metadata requests (consumes less network bandwidth) + /// Sparse metadata requests (consumes less network bandwidth) default: true importance: low public bool? TopicMetadataRefreshSparse { get => ConfluentConfig.TopicMetadataRefreshSparse; set => ConfluentConfig.TopicMetadataRefreshSparse = value; } - /// Topic blacklist, a comma-separated list of regular expressions for matching topic names that should be ignored in broker metadata information as if the topics did not exist. + /// Topic blacklist, a comma-separated list of regular expressions for matching topic names that should be ignored in broker metadata information as if the topics did not exist. default: '' importance: low public string TopicBlacklist { get => ConfluentConfig.TopicBlacklist; set => ConfluentConfig.TopicBlacklist = value; } - /// A comma-separated list of debug contexts to enable. Detailed Producer debugging: broker,topic,msg. Consumer: consumer,cgrp,topic,fetch + /// A comma-separated list of debug contexts to enable. Detailed Producer debugging: broker,topic,msg. Consumer: consumer,cgrp,topic,fetch default: '' importance: medium public string Debug { get => ConfluentConfig.Debug; set => ConfluentConfig.Debug = value; } - /// Default timeout for network requests. Producer: ProduceRequests will use the lesser value of `socket.timeout.ms` and remaining `message.timeout.ms` for the first message in the batch. Consumer: FetchRequests will use `fetch.wait.max.ms` + `socket.timeout.ms`. Admin: Admin requests will use `socket.timeout.ms` or explicitly set `rd_kafka_AdminOptions_set_operation_timeout()` value. + /// Default timeout for network requests. Producer: ProduceRequests will use the lesser value of `socket.timeout.ms` and remaining `message.timeout.ms` for the first message in the batch. Consumer: FetchRequests will use `fetch.wait.max.ms` + `socket.timeout.ms`. Admin: Admin requests will use `socket.timeout.ms` or explicitly set `rd_kafka_AdminOptions_set_operation_timeout()` value. default: 60000 importance: low public int? SocketTimeoutMs { get => ConfluentConfig.SocketTimeoutMs; set => ConfluentConfig.SocketTimeoutMs = value; } - /// Broker socket send buffer size. System default is used if 0. + /// Broker socket send buffer size. System default is used if 0. default: 0 importance: low public int? SocketSendBufferBytes { get => ConfluentConfig.SocketSendBufferBytes; set => ConfluentConfig.SocketSendBufferBytes = value; } - /// Broker socket receive buffer size. System default is used if 0. + /// Broker socket receive buffer size. System default is used if 0. default: 0 importance: low public int? SocketReceiveBufferBytes { get => ConfluentConfig.SocketReceiveBufferBytes; set => ConfluentConfig.SocketReceiveBufferBytes = value; } - /// Enable TCP keep-alives (SO_KEEPALIVE) on broker sockets + /// Enable TCP keep-alives (SO_KEEPALIVE) on broker sockets default: false importance: low public bool? SocketKeepaliveEnable { get => ConfluentConfig.SocketKeepaliveEnable; set => ConfluentConfig.SocketKeepaliveEnable = value; } - /// Disable the Nagle algorithm (TCP_NODELAY) on broker sockets. + /// Disable the Nagle algorithm (TCP_NODELAY) on broker sockets. default: false importance: low public bool? SocketNagleDisable { get => ConfluentConfig.SocketNagleDisable; set => ConfluentConfig.SocketNagleDisable = value; } - /// Disconnect from broker when this number of send failures (e.g., timed out requests) is reached. Disable with 0. WARNING: It is highly recommended to leave this setting at its default value of 1 to avoid the client and broker to become desynchronized in case of request timeouts. NOTE: The connection is automatically re-established. + /// Disconnect from broker when this number of send failures (e.g., timed out requests) is reached. Disable with 0. WARNING: It is highly recommended to leave this setting at its default value of 1 to avoid the client and broker to become desynchronized in case of request timeouts. NOTE: The connection is automatically re-established. default: 1 importance: low public int? SocketMaxFails { get => ConfluentConfig.SocketMaxFails; set => ConfluentConfig.SocketMaxFails = value; } - /// How long to cache the broker address resolving results (milliseconds). + /// How long to cache the broker address resolving results (milliseconds). default: 1000 importance: low public int? BrokerAddressTtl { get => ConfluentConfig.BrokerAddressTtl; set => ConfluentConfig.BrokerAddressTtl = value; } - /// Allowed broker IP address families: any, v4, v6 + /// Allowed broker IP address families: any, v4, v6 default: any importance: low public Confluent.Kafka.BrokerAddressFamily? BrokerAddressFamily { get => ConfluentConfig.BrokerAddressFamily; set => ConfluentConfig.BrokerAddressFamily = value; } - /// When enabled the client will only connect to brokers it needs to communicate with. When disabled the client will maintain connections to all brokers in the cluster. - public bool? EnableSparseConnections { get => ConfluentConfig.EnableSparseConnections; set => ConfluentConfig.EnableSparseConnections = value; } - - /// The initial time to wait before reconnecting to a broker after the connection has been closed. The time is increased exponentially until `reconnect.backoff.max.ms` is reached. -25% to +50% jitter is applied to each reconnect backoff. A value of 0 disables the backoff and reconnects immediately. + /// The initial time to wait before reconnecting to a broker after the connection has been closed. The time is increased exponentially until `reconnect.backoff.max.ms` is reached. -25% to +50% jitter is applied to each reconnect backoff. A value of 0 disables the backoff and reconnects immediately. default: 100 importance: medium public int? ReconnectBackoffMs { get => ConfluentConfig.ReconnectBackoffMs; set => ConfluentConfig.ReconnectBackoffMs = value; } - /// The maximum time to wait before reconnecting to a broker after the connection has been closed. + /// The maximum time to wait before reconnecting to a broker after the connection has been closed. default: 10000 importance: medium public int? ReconnectBackoffMaxMs { get => ConfluentConfig.ReconnectBackoffMaxMs; set => ConfluentConfig.ReconnectBackoffMaxMs = value; } - /// librdkafka statistics emit interval. The application also needs to register a stats callback using `rd_kafka_conf_set_stats_cb()`. The granularity is 1000ms. A value of 0 disables statistics. + /// librdkafka statistics emit interval. The application also needs to register a stats callback using `rd_kafka_conf_set_stats_cb()`. The granularity is 1000ms. A value of 0 disables statistics. default: 0 importance: high public int? StatisticsIntervalMs { get => ConfluentConfig.StatisticsIntervalMs; set => ConfluentConfig.StatisticsIntervalMs = value; } - /// Disable spontaneous log_cb from internal librdkafka threads, instead enqueue log messages on queue set with `rd_kafka_set_log_queue()` and serve log callbacks or events through the standard poll APIs. **NOTE**: Log messages will linger in a temporary queue until the log queue has been set. + /// Disable spontaneous log_cb from internal librdkafka threads, instead enqueue log messages on queue set with `rd_kafka_set_log_queue()` and serve log callbacks or events through the standard poll APIs. **NOTE**: Log messages will linger in a temporary queue until the log queue has been set. default: false importance: low public bool? LogQueue { get => ConfluentConfig.LogQueue; set => ConfluentConfig.LogQueue = value; } - /// Print internal thread name in log messages (useful for debugging librdkafka internals) + /// Print internal thread name in log messages (useful for debugging librdkafka internals) default: true importance: low public bool? LogThreadName { get => ConfluentConfig.LogThreadName; set => ConfluentConfig.LogThreadName = value; } - /// Log broker disconnects. It might be useful to turn this off when interacting with 0.9 brokers with an aggressive `connection.max.idle.ms` value. + /// Log broker disconnects. It might be useful to turn this off when interacting with 0.9 brokers with an aggressive `connection.max.idle.ms` value. default: true importance: low public bool? LogConnectionClose { get => ConfluentConfig.LogConnectionClose; set => ConfluentConfig.LogConnectionClose = value; } - /// Signal that librdkafka will use to quickly terminate on rd_kafka_destroy(). If this signal is not set then there will be a delay before rd_kafka_wait_destroyed() returns true as internal threads are timing out their system calls. If this signal is set however the delay will be minimal. The application should mask this signal as an internal signal handler is installed. + /// Signal that librdkafka will use to quickly terminate on rd_kafka_destroy(). If this signal is not set then there will be a delay before rd_kafka_wait_destroyed() returns true as internal threads are timing out their system calls. If this signal is set however the delay will be minimal. The application should mask this signal as an internal signal handler is installed. default: 0 importance: low public int? InternalTerminationSignal { get => ConfluentConfig.InternalTerminationSignal; set => ConfluentConfig.InternalTerminationSignal = value; } - /// Request broker's supported API versions to adjust functionality to available protocol features. If set to false, or the ApiVersionRequest fails, the fallback version `broker.version.fallback` will be used. **NOTE**: Depends on broker version >=0.10.0. If the request is not supported by (an older) broker the `broker.version.fallback` fallback is used. + /// Request broker's supported API versions to adjust functionality to available protocol features. If set to false, or the ApiVersionRequest fails, the fallback version `broker.version.fallback` will be used. **NOTE**: Depends on broker version >=0.10.0. If the request is not supported by (an older) broker the `broker.version.fallback` fallback is used. default: true importance: high public bool? ApiVersionRequest { get => ConfluentConfig.ApiVersionRequest; set => ConfluentConfig.ApiVersionRequest = value; } - /// Timeout for broker API version requests. + /// Timeout for broker API version requests. default: 10000 importance: low public int? ApiVersionRequestTimeoutMs { get => ConfluentConfig.ApiVersionRequestTimeoutMs; set => ConfluentConfig.ApiVersionRequestTimeoutMs = value; } - /// Dictates how long the `broker.version.fallback` fallback is used in the case the ApiVersionRequest fails. **NOTE**: The ApiVersionRequest is only issued when a new connection to the broker is made (such as after an upgrade). + /// Dictates how long the `broker.version.fallback` fallback is used in the case the ApiVersionRequest fails. **NOTE**: The ApiVersionRequest is only issued when a new connection to the broker is made (such as after an upgrade). default: 0 importance: medium public int? ApiVersionFallbackMs { get => ConfluentConfig.ApiVersionFallbackMs; set => ConfluentConfig.ApiVersionFallbackMs = value; } - /// Older broker versions (before 0.10.0) provide no way for a client to query for supported protocol features (ApiVersionRequest, see `api.version.request`) making it impossible for the client to know what features it may use. As a workaround a user may set this property to the expected broker version and the client will automatically adjust its feature set accordingly if the ApiVersionRequest fails (or is disabled). The fallback broker version will be used for `api.version.fallback.ms`. Valid values are: 0.9.0, 0.8.2, 0.8.1, 0.8.0. Any other value, such as 0.10.2.1, enables ApiVersionRequests. + /// Older broker versions (before 0.10.0) provide no way for a client to query for supported protocol features (ApiVersionRequest, see `api.version.request`) making it impossible for the client to know what features it may use. As a workaround a user may set this property to the expected broker version and the client will automatically adjust its feature set accordingly if the ApiVersionRequest fails (or is disabled). The fallback broker version will be used for `api.version.fallback.ms`. Valid values are: 0.9.0, 0.8.2, 0.8.1, 0.8.0. Any other value >= 0.10, such as 0.10.2.1, enables ApiVersionRequests. default: 0.10.0 importance: medium public string BrokerVersionFallback { get => ConfluentConfig.BrokerVersionFallback; set => ConfluentConfig.BrokerVersionFallback = value; } - /// Protocol used to communicate with brokers. + /// Protocol used to communicate with brokers. default: plaintext importance: high public Confluent.Kafka.SecurityProtocol? SecurityProtocol { get => ConfluentConfig.SecurityProtocol; set => ConfluentConfig.SecurityProtocol = value; } - /// A cipher suite is a named combination of authentication, encryption, MAC and key exchange algorithm used to negotiate the security settings for a network connection using TLS or SSL network protocol. See manual page for `ciphers(1)` and `SSL_CTX_set_cipher_list(3). + /// A cipher suite is a named combination of authentication, encryption, MAC and key exchange algorithm used to negotiate the security settings for a network connection using TLS or SSL network protocol. See manual page for `ciphers(1)` and `SSL_CTX_set_cipher_list(3). default: '' importance: low public string SslCipherSuites { get => ConfluentConfig.SslCipherSuites; set => ConfluentConfig.SslCipherSuites = value; } - /// The supported-curves extension in the TLS ClientHello message specifies the curves (standard/named, or 'explicit' GF(2^k) or GF(p)) the client is willing to have the server use. See manual page for `SSL_CTX_set1_curves_list(3)`. OpenSSL >= 1.0.2 required. + /// The supported-curves extension in the TLS ClientHello message specifies the curves (standard/named, or 'explicit' GF(2^k) or GF(p)) the client is willing to have the server use. See manual page for `SSL_CTX_set1_curves_list(3)`. OpenSSL >= 1.0.2 required. default: '' importance: low public string SslCurvesList { get => ConfluentConfig.SslCurvesList; set => ConfluentConfig.SslCurvesList = value; } - /// The client uses the TLS ClientHello signature_algorithms extension to indicate to the server which signature/hash algorithm pairs may be used in digital signatures. See manual page for `SSL_CTX_set1_sigalgs_list(3)`. OpenSSL >= 1.0.2 required. + /// The client uses the TLS ClientHello signature_algorithms extension to indicate to the server which signature/hash algorithm pairs may be used in digital signatures. See manual page for `SSL_CTX_set1_sigalgs_list(3)`. OpenSSL >= 1.0.2 required. default: '' importance: low public string SslSigalgsList { get => ConfluentConfig.SslSigalgsList; set => ConfluentConfig.SslSigalgsList = value; } - /// Path to client's private key (PEM) used for authentication. + /// Path to client's private key (PEM) used for authentication. default: '' importance: low public string SslKeyLocation { get => ConfluentConfig.SslKeyLocation; set => ConfluentConfig.SslKeyLocation = value; } - /// Private key passphrase + /// Private key passphrase default: '' importance: low public string SslKeyPassword { get => ConfluentConfig.SslKeyPassword; set => ConfluentConfig.SslKeyPassword = value; } - /// Path to client's public key (PEM) used for authentication. + /// Path to client's public key (PEM) used for authentication. default: '' importance: low public string SslCertificateLocation { get => ConfluentConfig.SslCertificateLocation; set => ConfluentConfig.SslCertificateLocation = value; } - /// File or directory path to CA certificate(s) for verifying the broker's key. + /// File or directory path to CA certificate(s) for verifying the broker's key. default: '' importance: medium public string SslCaLocation { get => ConfluentConfig.SslCaLocation; set => ConfluentConfig.SslCaLocation = value; } - /// Path to CRL for verifying broker's certificate validity. + /// Path to CRL for verifying broker's certificate validity. default: '' importance: low public string SslCrlLocation { get => ConfluentConfig.SslCrlLocation; set => ConfluentConfig.SslCrlLocation = value; } - /// Path to client's keystore (PKCS#12) used for authentication. + /// Path to client's keystore (PKCS#12) used for authentication. default: '' importance: low public string SslKeystoreLocation { get => ConfluentConfig.SslKeystoreLocation; set => ConfluentConfig.SslKeystoreLocation = value; } - /// Client's keystore (PKCS#12) password. + /// Client's keystore (PKCS#12) password. default: '' importance: low public string SslKeystorePassword { get => ConfluentConfig.SslKeystorePassword; set => ConfluentConfig.SslKeystorePassword = value; } - /// Kerberos principal name that Kafka runs as, not including /hostname@REALM + /// Kerberos principal name that Kafka runs as, not including /hostname@REALM default: kafka importance: low public string SaslKerberosServiceName { get => ConfluentConfig.SaslKerberosServiceName; set => ConfluentConfig.SaslKerberosServiceName = value; } - /// This client's Kerberos principal name. (Not supported on Windows, will use the logon user's principal). + /// This client's Kerberos principal name. (Not supported on Windows, will use the logon user's principal). default: kafkaclient importance: low public string SaslKerberosPrincipal { get => ConfluentConfig.SaslKerberosPrincipal; set => ConfluentConfig.SaslKerberosPrincipal = value; } - /// Full kerberos kinit command string, %{config.prop.name} is replaced by corresponding config object value, %{broker.name} returns the broker's hostname. + /// Full kerberos kinit command string, %{config.prop.name} is replaced by corresponding config object value, %{broker.name} returns the broker's hostname. default: kinit -S "%{sasl.kerberos.service.name}/%{broker.name}" -k -t "%{sasl.kerberos.keytab}" %{sasl.kerberos.principal} importance: low public string SaslKerberosKinitCmd { get => ConfluentConfig.SaslKerberosKinitCmd; set => ConfluentConfig.SaslKerberosKinitCmd = value; } - /// Path to Kerberos keytab file. Uses system default if not set.**NOTE**: This is not automatically used but must be added to the template in sasl.kerberos.kinit.cmd as ` ... -t %{sasl.kerberos.keytab}`. + /// Path to Kerberos keytab file. Uses system default if not set.**NOTE**: This is not automatically used but must be added to the template in sasl.kerberos.kinit.cmd as ` ... -t %{sasl.kerberos.keytab}`. default: '' importance: low public string SaslKerberosKeytab { get => ConfluentConfig.SaslKerberosKeytab; set => ConfluentConfig.SaslKerberosKeytab = value; } - /// Minimum time in milliseconds between key refresh attempts. + /// Minimum time in milliseconds between key refresh attempts. default: 60000 importance: low public int? SaslKerberosMinTimeBeforeRelogin { get => ConfluentConfig.SaslKerberosMinTimeBeforeRelogin; set => ConfluentConfig.SaslKerberosMinTimeBeforeRelogin = value; } - /// SASL username for use with the PLAIN and SASL-SCRAM-.. mechanisms + /// SASL username for use with the PLAIN and SASL-SCRAM-.. mechanisms default: '' importance: high public string SaslUsername { get => ConfluentConfig.SaslUsername; set => ConfluentConfig.SaslUsername = value; } - /// SASL password for use with the PLAIN and SASL-SCRAM-.. mechanism + /// SASL password for use with the PLAIN and SASL-SCRAM-.. mechanism default: '' importance: high public string SaslPassword { get => ConfluentConfig.SaslPassword; set => ConfluentConfig.SaslPassword = value; } - /// List of plugin libraries to load (; separated). The library search path is platform dependent (see dlopen(3) for Unix and LoadLibrary() for Windows). If no filename extension is specified the platform-specific extension (such as .dll or .so) will be appended automatically. + /// List of plugin libraries to load (; separated). The library search path is platform dependent (see dlopen(3) for Unix and LoadLibrary() for Windows). If no filename extension is specified the platform-specific extension (such as .dll or .so) will be appended automatically. default: '' importance: low public string PluginLibraryPaths { get => ConfluentConfig.PluginLibraryPaths; set => ConfluentConfig.PluginLibraryPaths = value; } + /// The maximum length of time (in milliseconds) before a cancellation request is acted on. Low values may result in measurably higher CPU usage. default: 100 range: 1 <= dotnet.cancellation.delay.max.ms <= 10000 importance: low public int CancellationDelayMaxMs { set => ConfluentConfig.CancellationDelayMaxMs = value; } } } \ No newline at end of file diff --git a/src/Silverback.Integration.Kafka/Messaging/Proxies/ConfluentProducerConfigProxy.cs b/src/Silverback.Integration.Kafka/Messaging/Proxies/ConfluentProducerConfigProxy.cs index c146db8f5..15504bf8f 100644 --- a/src/Silverback.Integration.Kafka/Messaging/Proxies/ConfluentProducerConfigProxy.cs +++ b/src/Silverback.Integration.Kafka/Messaging/Proxies/ConfluentProducerConfigProxy.cs @@ -1,219 +1,223 @@ // Copyright (c) 2018-2019 Sergio Aquilini // This code is licensed under MIT license (see LICENSE file for details) + namespace Silverback.Messaging.Proxies { public class ConfluentProducerConfigProxy { internal Confluent.Kafka.ProducerConfig ConfluentConfig { get; } = new Confluent.Kafka.ProducerConfig(); - /// Specifies whether or not the producer should start a background poll thread to receive delivery reports and event notifications. Generally, this should be set to true. If set to false, you will need to call the Poll function manually. default: true + /// Specifies whether or not the producer should start a background poll thread to receive delivery reports and event notifications. Generally, this should be set to true. If set to false, you will need to call the Poll function manually. default: true importance: low public bool? EnableBackgroundPoll { get => ConfluentConfig.EnableBackgroundPoll; set => ConfluentConfig.EnableBackgroundPoll = value; } - /// Specifies whether to enable notification of delivery reports. Typically you should set this parameter to true. Set it to false for "fire and forget" semantics and a small boost in performance. default: true + /// Specifies whether to enable notification of delivery reports. Typically you should set this parameter to true. Set it to false for "fire and forget" semantics and a small boost in performance. default: true importance: low public bool? EnableDeliveryReports { get => ConfluentConfig.EnableDeliveryReports; set => ConfluentConfig.EnableDeliveryReports = value; } - /// A comma separated list of fields that may be optionally set in delivery reports. Disabling delivery report fields that you do not require will improve maximum throughput and reduce memory usage. Allowed values: key, value, timestamp, headers, all, none. default: all + /// A comma separated list of fields that may be optionally set in delivery reports. Disabling delivery report fields that you do not require will improve maximum throughput and reduce memory usage. Allowed values: key, value, timestamp, headers, all, none. default: all importance: low public string DeliveryReportFields { get => ConfluentConfig.DeliveryReportFields; set => ConfluentConfig.DeliveryReportFields = value ?? ""; } + /// The ack timeout of the producer request in milliseconds. This value is only enforced by the broker and relies on `request.required.acks` being != 0. default: 5000 importance: medium + public int? RequestTimeoutMs { get => ConfluentConfig.RequestTimeoutMs; set => ConfluentConfig.RequestTimeoutMs = value; } + + /// Local message timeout. This value is only enforced locally and limits the time a produced message waits for successful delivery. A time of 0 is infinite. This is the maximum time librdkafka may use to deliver a message (including retries). Delivery error occurs when either the retry count or the message timeout are exceeded. default: 300000 importance: high + public int? MessageTimeoutMs { get => ConfluentConfig.MessageTimeoutMs; set => ConfluentConfig.MessageTimeoutMs = value; } + + /// Partitioner: `random` - random distribution, `consistent` - CRC32 hash of key (Empty and NULL keys are mapped to single partition), `consistent_random` - CRC32 hash of key (Empty and NULL keys are randomly partitioned), `murmur2` - Java Producer compatible Murmur2 hash of key (NULL keys are mapped to single partition), `murmur2_random` - Java Producer compatible Murmur2 hash of key (NULL keys are randomly partitioned. This is functionally equivalent to the default partitioner in the Java Producer.). default: consistent_random importance: high + public Confluent.Kafka.Partitioner? Partitioner { get => ConfluentConfig.Partitioner; set => ConfluentConfig.Partitioner = value; } + + /// Compression level parameter for algorithm selected by configuration property `compression.codec`. Higher values will result in better compression at the cost of more CPU usage. Usable range is algorithm-dependent: [0-9] for gzip; [0-12] for lz4; only 0 for snappy; -1 = codec-dependent default compression level. default: -1 importance: medium + public int? CompressionLevel { get => ConfluentConfig.CompressionLevel; set => ConfluentConfig.CompressionLevel = value; } + + /// When set to `true`, the producer will ensure that messages are successfully produced exactly once and in the original produce order. The following configuration properties are adjusted automatically (if not modified by the user) when idempotence is enabled: `max.in.flight.requests.per.connection=5` (must be less than or equal to 5), `retries=INT32_MAX` (must be greater than 0), `acks=all`, `queuing.strategy=fifo`. Producer instantation will fail if user-supplied configuration is incompatible. default: false importance: high public bool? EnableIdempotence { get => ConfluentConfig.EnableIdempotence; set => ConfluentConfig.EnableIdempotence = value; } - /// When set to `true`, any error that could result in a gap in the produced message series when a batch of messages fails, will raise a fatal error (ERR__GAPLESS_GUARANTEE) and stop the producer. Requires `enable.idempotence=true`. + /// **EXPERIMENTAL**: subject to change or removal. When set to `true`, any error that could result in a gap in the produced message series when a batch of messages fails, will raise a fatal error (ERR__GAPLESS_GUARANTEE) and stop the producer. Messages failing due to `message.timeout.ms` are not covered by this guarantee. Requires `enable.idempotence=true`. default: false importance: low public bool? EnableGaplessGuarantee { get => ConfluentConfig.EnableGaplessGuarantee; set => ConfluentConfig.EnableGaplessGuarantee = value; } - /// Maximum number of messages allowed on the producer queue. + /// Maximum number of messages allowed on the producer queue. This queue is shared by all topics and partitions. default: 100000 importance: high public int? QueueBufferingMaxMessages { get => ConfluentConfig.QueueBufferingMaxMessages; set => ConfluentConfig.QueueBufferingMaxMessages = value; } - /// Maximum total message size sum allowed on the producer queue. This property has higher priority than queue.buffering.max.messages. + /// Maximum total message size sum allowed on the producer queue. This queue is shared by all topics and partitions. This property has higher priority than queue.buffering.max.messages. default: 1048576 importance: high public int? QueueBufferingMaxKbytes { get => ConfluentConfig.QueueBufferingMaxKbytes; set => ConfluentConfig.QueueBufferingMaxKbytes = value; } - /// Delay in milliseconds to wait for messages in the producer queue to accumulate before constructing message batches (MessageSets) to transmit to brokers. A higher value allows larger and more effective (less overhead, improved compression) batches of messages to accumulate at the expense of increased message delivery latency. + /// Delay in milliseconds to wait for messages in the producer queue to accumulate before constructing message batches (MessageSets) to transmit to brokers. A higher value allows larger and more effective (less overhead, improved compression) batches of messages to accumulate at the expense of increased message delivery latency. default: 0 importance: high public int? LingerMs { get => ConfluentConfig.LingerMs; set => ConfluentConfig.LingerMs = value; } - /// How many times to retry sending a failing MessageSet. **Note:** retrying may cause reordering. + /// How many times to retry sending a failing Message. **Note:** retrying may cause reordering unless `enable.idempotence` is set to true. default: 2 importance: high public int? MessageSendMaxRetries { get => ConfluentConfig.MessageSendMaxRetries; set => ConfluentConfig.MessageSendMaxRetries = value; } - /// The backoff time in milliseconds before retrying a protocol request. + /// The backoff time in milliseconds before retrying a protocol request. default: 100 importance: medium public int? RetryBackoffMs { get => ConfluentConfig.RetryBackoffMs; set => ConfluentConfig.RetryBackoffMs = value; } - /// The threshold of outstanding not yet transmitted broker requests needed to backpressure the producer's message accumulator. If the number of not yet transmitted requests equals or exceeds this number, produce request creation that would have otherwise been triggered (for example, in accordance with linger.ms) will be delayed. A lower number yields larger and more effective batches. A higher value can improve latency when using compression on slow machines. + /// The threshold of outstanding not yet transmitted broker requests needed to backpressure the producer's message accumulator. If the number of not yet transmitted requests equals or exceeds this number, produce request creation that would have otherwise been triggered (for example, in accordance with linger.ms) will be delayed. A lower number yields larger and more effective batches. A higher value can improve latency when using compression on slow machines. default: 1 importance: low public int? QueueBufferingBackpressureThreshold { get => ConfluentConfig.QueueBufferingBackpressureThreshold; set => ConfluentConfig.QueueBufferingBackpressureThreshold = value; } - /// Maximum number of messages batched in one MessageSet. The total MessageSet size is also limited by message.max.bytes. - public int? BatchNumMessages { get => ConfluentConfig.BatchNumMessages; set => ConfluentConfig.BatchNumMessages = value; } + /// compression codec to use for compressing message sets. This is the default value for all topics, may be overridden by the topic configuration property `compression.codec`. default: none importance: medium + public Confluent.Kafka.CompressionType? CompressionType { get => ConfluentConfig.CompressionType; set => ConfluentConfig.CompressionType = value; } - /// The ack timeout of the producer request in milliseconds. This value is only enforced by the broker and relies on `request.required.acks` being != 0. - public int? RequestTimeoutMs { get => ConfluentConfig.RequestTimeoutMs; set => ConfluentConfig.RequestTimeoutMs = value; } - - /// Local message timeout. This value is only enforced locally and limits the time a produced message waits for successful delivery. A time of 0 is infinite. This is the maximum time librdkafka may use to deliver a message (including retries). Delivery error occurs when either the retry count or the message timeout are exceeded. - public int? MessageTimeoutMs { get => ConfluentConfig.MessageTimeoutMs; set => ConfluentConfig.MessageTimeoutMs = value; } - - /// Partitioner: `random` - random distribution, `consistent` - CRC32 hash of key (Empty and NULL keys are mapped to single partition), `consistent_random` - CRC32 hash of key (Empty and NULL keys are randomly partitioned), `murmur2` - Java Producer compatible Murmur2 hash of key (NULL keys are mapped to single partition), `murmur2_random` - Java Producer compatible Murmur2 hash of key (NULL keys are randomly partitioned. This is functionally equivalent to the default partitioner in the Java Producer.). - public Confluent.Kafka.Partitioner? Partitioner { get => ConfluentConfig.Partitioner; set => ConfluentConfig.Partitioner = value; } - - /// Compression level parameter for algorithm selected by configuration property `compression.codec`. Higher values will result in better compression at the cost of more CPU usage. Usable range is algorithm-dependent: [0-9] for gzip; [0-12] for lz4; only 0 for snappy; -1 = codec-dependent default compression level. - public int? CompressionLevel { get => ConfluentConfig.CompressionLevel; set => ConfluentConfig.CompressionLevel = value; } + /// Maximum number of messages batched in one MessageSet. The total MessageSet size is also limited by message.max.bytes. default: 10000 importance: medium + public int? BatchNumMessages { get => ConfluentConfig.BatchNumMessages; set => ConfluentConfig.BatchNumMessages = value; } /// SASL mechanism to use for authentication. Supported: GSSAPI, PLAIN, SCRAM-SHA-256, SCRAM-SHA-512. **NOTE**: Despite the name, you may not configure more than one mechanism. public Confluent.Kafka.SaslMechanism? SaslMechanism { get => ConfluentConfig.SaslMechanism; set => ConfluentConfig.SaslMechanism = value; } + /// This field indicates the number of acknowledgements the leader broker must receive from ISR brokers before responding to the request: Zero=Broker does not send any response/ack to client, One=The leader will write the record to its local log but will respond without awaiting full acknowledgement from all followers. All=Broker will block until message is committed by all in sync replicas (ISRs). If there are less than min.insync.replicas (broker configuration) in the ISR set the produce request will fail. public Confluent.Kafka.Acks? Acks { get => ConfluentConfig.Acks; set => ConfluentConfig.Acks = value; } - /// Client identifier. + /// Client identifier. default: rdkafka importance: low public string ClientId { get => ConfluentConfig.ClientId; set => ConfluentConfig.ClientId = value; } - /// Initial list of brokers as a CSV list of broker host or host:port. The application may also use `rd_kafka_brokers_add()` to add brokers during runtime. + /// Initial list of brokers as a CSV list of broker host or host:port. The application may also use `rd_kafka_brokers_add()` to add brokers during runtime. default: '' importance: high public string BootstrapServers { get => ConfluentConfig.BootstrapServers; set => ConfluentConfig.BootstrapServers = value; } - /// Maximum Kafka protocol request message size. + /// Maximum Kafka protocol request message size. default: 1000000 importance: medium public int? MessageMaxBytes { get => ConfluentConfig.MessageMaxBytes; set => ConfluentConfig.MessageMaxBytes = value; } - /// Maximum size for message to be copied to buffer. Messages larger than this will be passed by reference (zero-copy) at the expense of larger iovecs. + /// Maximum size for message to be copied to buffer. Messages larger than this will be passed by reference (zero-copy) at the expense of larger iovecs. default: 65535 importance: low public int? MessageCopyMaxBytes { get => ConfluentConfig.MessageCopyMaxBytes; set => ConfluentConfig.MessageCopyMaxBytes = value; } - /// Maximum Kafka protocol response message size. This serves as a safety precaution to avoid memory exhaustion in case of protocol hickups. This value must be at least `fetch.max.bytes` + 512 to allow for protocol overhead; the value is adjusted automatically unless the configuration property is explicitly set. + /// Maximum Kafka protocol response message size. This serves as a safety precaution to avoid memory exhaustion in case of protocol hickups. This value must be at least `fetch.max.bytes` + 512 to allow for protocol overhead; the value is adjusted automatically unless the configuration property is explicitly set. default: 100000000 importance: medium public int? ReceiveMessageMaxBytes { get => ConfluentConfig.ReceiveMessageMaxBytes; set => ConfluentConfig.ReceiveMessageMaxBytes = value; } - /// Maximum number of in-flight requests per broker connection. This is a generic property applied to all broker communication, however it is primarily relevant to produce requests. In particular, note that other mechanisms limit the number of outstanding consumer fetch request per broker to one. + /// Maximum number of in-flight requests per broker connection. This is a generic property applied to all broker communication, however it is primarily relevant to produce requests. In particular, note that other mechanisms limit the number of outstanding consumer fetch request per broker to one. default: 1000000 importance: low public int? MaxInFlight { get => ConfluentConfig.MaxInFlight; set => ConfluentConfig.MaxInFlight = value; } - /// Non-topic request timeout in milliseconds. This is for metadata requests, etc. + /// Non-topic request timeout in milliseconds. This is for metadata requests, etc. default: 60000 importance: low public int? MetadataRequestTimeoutMs { get => ConfluentConfig.MetadataRequestTimeoutMs; set => ConfluentConfig.MetadataRequestTimeoutMs = value; } - /// Topic metadata refresh interval in milliseconds. The metadata is automatically refreshed on error and connect. Use -1 to disable the intervalled refresh. + /// Topic metadata refresh interval in milliseconds. The metadata is automatically refreshed on error and connect. Use -1 to disable the intervalled refresh. default: 300000 importance: low public int? TopicMetadataRefreshIntervalMs { get => ConfluentConfig.TopicMetadataRefreshIntervalMs; set => ConfluentConfig.TopicMetadataRefreshIntervalMs = value; } - /// Metadata cache max age. Defaults to topic.metadata.refresh.interval.ms * 3 + /// Metadata cache max age. Defaults to topic.metadata.refresh.interval.ms * 3 default: 900000 importance: low public int? MetadataMaxAgeMs { get => ConfluentConfig.MetadataMaxAgeMs; set => ConfluentConfig.MetadataMaxAgeMs = value; } - /// When a topic loses its leader a new metadata request will be enqueued with this initial interval, exponentially increasing until the topic metadata has been refreshed. This is used to recover quickly from transitioning leader brokers. + /// When a topic loses its leader a new metadata request will be enqueued with this initial interval, exponentially increasing until the topic metadata has been refreshed. This is used to recover quickly from transitioning leader brokers. default: 250 importance: low public int? TopicMetadataRefreshFastIntervalMs { get => ConfluentConfig.TopicMetadataRefreshFastIntervalMs; set => ConfluentConfig.TopicMetadataRefreshFastIntervalMs = value; } - /// Sparse metadata requests (consumes less network bandwidth) + /// Sparse metadata requests (consumes less network bandwidth) default: true importance: low public bool? TopicMetadataRefreshSparse { get => ConfluentConfig.TopicMetadataRefreshSparse; set => ConfluentConfig.TopicMetadataRefreshSparse = value; } - /// Topic blacklist, a comma-separated list of regular expressions for matching topic names that should be ignored in broker metadata information as if the topics did not exist. + /// Topic blacklist, a comma-separated list of regular expressions for matching topic names that should be ignored in broker metadata information as if the topics did not exist. default: '' importance: low public string TopicBlacklist { get => ConfluentConfig.TopicBlacklist; set => ConfluentConfig.TopicBlacklist = value; } - /// A comma-separated list of debug contexts to enable. Detailed Producer debugging: broker,topic,msg. Consumer: consumer,cgrp,topic,fetch + /// A comma-separated list of debug contexts to enable. Detailed Producer debugging: broker,topic,msg. Consumer: consumer,cgrp,topic,fetch default: '' importance: medium public string Debug { get => ConfluentConfig.Debug; set => ConfluentConfig.Debug = value; } - /// Default timeout for network requests. Producer: ProduceRequests will use the lesser value of `socket.timeout.ms` and remaining `message.timeout.ms` for the first message in the batch. Consumer: FetchRequests will use `fetch.wait.max.ms` + `socket.timeout.ms`. Admin: Admin requests will use `socket.timeout.ms` or explicitly set `rd_kafka_AdminOptions_set_operation_timeout()` value. + /// Default timeout for network requests. Producer: ProduceRequests will use the lesser value of `socket.timeout.ms` and remaining `message.timeout.ms` for the first message in the batch. Consumer: FetchRequests will use `fetch.wait.max.ms` + `socket.timeout.ms`. Admin: Admin requests will use `socket.timeout.ms` or explicitly set `rd_kafka_AdminOptions_set_operation_timeout()` value. default: 60000 importance: low public int? SocketTimeoutMs { get => ConfluentConfig.SocketTimeoutMs; set => ConfluentConfig.SocketTimeoutMs = value; } - /// Broker socket send buffer size. System default is used if 0. + /// Broker socket send buffer size. System default is used if 0. default: 0 importance: low public int? SocketSendBufferBytes { get => ConfluentConfig.SocketSendBufferBytes; set => ConfluentConfig.SocketSendBufferBytes = value; } - /// Broker socket receive buffer size. System default is used if 0. + /// Broker socket receive buffer size. System default is used if 0. default: 0 importance: low public int? SocketReceiveBufferBytes { get => ConfluentConfig.SocketReceiveBufferBytes; set => ConfluentConfig.SocketReceiveBufferBytes = value; } - /// Enable TCP keep-alives (SO_KEEPALIVE) on broker sockets + /// Enable TCP keep-alives (SO_KEEPALIVE) on broker sockets default: false importance: low public bool? SocketKeepaliveEnable { get => ConfluentConfig.SocketKeepaliveEnable; set => ConfluentConfig.SocketKeepaliveEnable = value; } - /// Disable the Nagle algorithm (TCP_NODELAY) on broker sockets. + /// Disable the Nagle algorithm (TCP_NODELAY) on broker sockets. default: false importance: low public bool? SocketNagleDisable { get => ConfluentConfig.SocketNagleDisable; set => ConfluentConfig.SocketNagleDisable = value; } - /// Disconnect from broker when this number of send failures (e.g., timed out requests) is reached. Disable with 0. WARNING: It is highly recommended to leave this setting at its default value of 1 to avoid the client and broker to become desynchronized in case of request timeouts. NOTE: The connection is automatically re-established. + /// Disconnect from broker when this number of send failures (e.g., timed out requests) is reached. Disable with 0. WARNING: It is highly recommended to leave this setting at its default value of 1 to avoid the client and broker to become desynchronized in case of request timeouts. NOTE: The connection is automatically re-established. default: 1 importance: low public int? SocketMaxFails { get => ConfluentConfig.SocketMaxFails; set => ConfluentConfig.SocketMaxFails = value; } - /// How long to cache the broker address resolving results (milliseconds). + /// How long to cache the broker address resolving results (milliseconds). default: 1000 importance: low public int? BrokerAddressTtl { get => ConfluentConfig.BrokerAddressTtl; set => ConfluentConfig.BrokerAddressTtl = value; } - /// Allowed broker IP address families: any, v4, v6 + /// Allowed broker IP address families: any, v4, v6 default: any importance: low public Confluent.Kafka.BrokerAddressFamily? BrokerAddressFamily { get => ConfluentConfig.BrokerAddressFamily; set => ConfluentConfig.BrokerAddressFamily = value; } - /// When enabled the client will only connect to brokers it needs to communicate with. When disabled the client will maintain connections to all brokers in the cluster. - public bool? EnableSparseConnections { get => ConfluentConfig.EnableSparseConnections; set => ConfluentConfig.EnableSparseConnections = value; } - - /// The initial time to wait before reconnecting to a broker after the connection has been closed. The time is increased exponentially until `reconnect.backoff.max.ms` is reached. -25% to +50% jitter is applied to each reconnect backoff. A value of 0 disables the backoff and reconnects immediately. + /// The initial time to wait before reconnecting to a broker after the connection has been closed. The time is increased exponentially until `reconnect.backoff.max.ms` is reached. -25% to +50% jitter is applied to each reconnect backoff. A value of 0 disables the backoff and reconnects immediately. default: 100 importance: medium public int? ReconnectBackoffMs { get => ConfluentConfig.ReconnectBackoffMs; set => ConfluentConfig.ReconnectBackoffMs = value; } - /// The maximum time to wait before reconnecting to a broker after the connection has been closed. + /// The maximum time to wait before reconnecting to a broker after the connection has been closed. default: 10000 importance: medium public int? ReconnectBackoffMaxMs { get => ConfluentConfig.ReconnectBackoffMaxMs; set => ConfluentConfig.ReconnectBackoffMaxMs = value; } - /// librdkafka statistics emit interval. The application also needs to register a stats callback using `rd_kafka_conf_set_stats_cb()`. The granularity is 1000ms. A value of 0 disables statistics. + /// librdkafka statistics emit interval. The application also needs to register a stats callback using `rd_kafka_conf_set_stats_cb()`. The granularity is 1000ms. A value of 0 disables statistics. default: 0 importance: high public int? StatisticsIntervalMs { get => ConfluentConfig.StatisticsIntervalMs; set => ConfluentConfig.StatisticsIntervalMs = value; } - /// Disable spontaneous log_cb from internal librdkafka threads, instead enqueue log messages on queue set with `rd_kafka_set_log_queue()` and serve log callbacks or events through the standard poll APIs. **NOTE**: Log messages will linger in a temporary queue until the log queue has been set. + /// Disable spontaneous log_cb from internal librdkafka threads, instead enqueue log messages on queue set with `rd_kafka_set_log_queue()` and serve log callbacks or events through the standard poll APIs. **NOTE**: Log messages will linger in a temporary queue until the log queue has been set. default: false importance: low public bool? LogQueue { get => ConfluentConfig.LogQueue; set => ConfluentConfig.LogQueue = value; } - /// Print internal thread name in log messages (useful for debugging librdkafka internals) + /// Print internal thread name in log messages (useful for debugging librdkafka internals) default: true importance: low public bool? LogThreadName { get => ConfluentConfig.LogThreadName; set => ConfluentConfig.LogThreadName = value; } - /// Log broker disconnects. It might be useful to turn this off when interacting with 0.9 brokers with an aggressive `connection.max.idle.ms` value. + /// Log broker disconnects. It might be useful to turn this off when interacting with 0.9 brokers with an aggressive `connection.max.idle.ms` value. default: true importance: low public bool? LogConnectionClose { get => ConfluentConfig.LogConnectionClose; set => ConfluentConfig.LogConnectionClose = value; } - /// Signal that librdkafka will use to quickly terminate on rd_kafka_destroy(). If this signal is not set then there will be a delay before rd_kafka_wait_destroyed() returns true as internal threads are timing out their system calls. If this signal is set however the delay will be minimal. The application should mask this signal as an internal signal handler is installed. + /// Signal that librdkafka will use to quickly terminate on rd_kafka_destroy(). If this signal is not set then there will be a delay before rd_kafka_wait_destroyed() returns true as internal threads are timing out their system calls. If this signal is set however the delay will be minimal. The application should mask this signal as an internal signal handler is installed. default: 0 importance: low public int? InternalTerminationSignal { get => ConfluentConfig.InternalTerminationSignal; set => ConfluentConfig.InternalTerminationSignal = value; } - /// Request broker's supported API versions to adjust functionality to available protocol features. If set to false, or the ApiVersionRequest fails, the fallback version `broker.version.fallback` will be used. **NOTE**: Depends on broker version >=0.10.0. If the request is not supported by (an older) broker the `broker.version.fallback` fallback is used. + /// Request broker's supported API versions to adjust functionality to available protocol features. If set to false, or the ApiVersionRequest fails, the fallback version `broker.version.fallback` will be used. **NOTE**: Depends on broker version >=0.10.0. If the request is not supported by (an older) broker the `broker.version.fallback` fallback is used. default: true importance: high public bool? ApiVersionRequest { get => ConfluentConfig.ApiVersionRequest; set => ConfluentConfig.ApiVersionRequest = value; } - /// Timeout for broker API version requests. + /// Timeout for broker API version requests. default: 10000 importance: low public int? ApiVersionRequestTimeoutMs { get => ConfluentConfig.ApiVersionRequestTimeoutMs; set => ConfluentConfig.ApiVersionRequestTimeoutMs = value; } - /// Dictates how long the `broker.version.fallback` fallback is used in the case the ApiVersionRequest fails. **NOTE**: The ApiVersionRequest is only issued when a new connection to the broker is made (such as after an upgrade). + /// Dictates how long the `broker.version.fallback` fallback is used in the case the ApiVersionRequest fails. **NOTE**: The ApiVersionRequest is only issued when a new connection to the broker is made (such as after an upgrade). default: 0 importance: medium public int? ApiVersionFallbackMs { get => ConfluentConfig.ApiVersionFallbackMs; set => ConfluentConfig.ApiVersionFallbackMs = value; } - /// Older broker versions (before 0.10.0) provide no way for a client to query for supported protocol features (ApiVersionRequest, see `api.version.request`) making it impossible for the client to know what features it may use. As a workaround a user may set this property to the expected broker version and the client will automatically adjust its feature set accordingly if the ApiVersionRequest fails (or is disabled). The fallback broker version will be used for `api.version.fallback.ms`. Valid values are: 0.9.0, 0.8.2, 0.8.1, 0.8.0. Any other value, such as 0.10.2.1, enables ApiVersionRequests. + /// Older broker versions (before 0.10.0) provide no way for a client to query for supported protocol features (ApiVersionRequest, see `api.version.request`) making it impossible for the client to know what features it may use. As a workaround a user may set this property to the expected broker version and the client will automatically adjust its feature set accordingly if the ApiVersionRequest fails (or is disabled). The fallback broker version will be used for `api.version.fallback.ms`. Valid values are: 0.9.0, 0.8.2, 0.8.1, 0.8.0. Any other value >= 0.10, such as 0.10.2.1, enables ApiVersionRequests. default: 0.10.0 importance: medium public string BrokerVersionFallback { get => ConfluentConfig.BrokerVersionFallback; set => ConfluentConfig.BrokerVersionFallback = value; } - /// Protocol used to communicate with brokers. + /// Protocol used to communicate with brokers. default: plaintext importance: high public Confluent.Kafka.SecurityProtocol? SecurityProtocol { get => ConfluentConfig.SecurityProtocol; set => ConfluentConfig.SecurityProtocol = value; } - /// A cipher suite is a named combination of authentication, encryption, MAC and key exchange algorithm used to negotiate the security settings for a network connection using TLS or SSL network protocol. See manual page for `ciphers(1)` and `SSL_CTX_set_cipher_list(3). + /// A cipher suite is a named combination of authentication, encryption, MAC and key exchange algorithm used to negotiate the security settings for a network connection using TLS or SSL network protocol. See manual page for `ciphers(1)` and `SSL_CTX_set_cipher_list(3). default: '' importance: low public string SslCipherSuites { get => ConfluentConfig.SslCipherSuites; set => ConfluentConfig.SslCipherSuites = value; } - /// The supported-curves extension in the TLS ClientHello message specifies the curves (standard/named, or 'explicit' GF(2^k) or GF(p)) the client is willing to have the server use. See manual page for `SSL_CTX_set1_curves_list(3)`. OpenSSL >= 1.0.2 required. + /// The supported-curves extension in the TLS ClientHello message specifies the curves (standard/named, or 'explicit' GF(2^k) or GF(p)) the client is willing to have the server use. See manual page for `SSL_CTX_set1_curves_list(3)`. OpenSSL >= 1.0.2 required. default: '' importance: low public string SslCurvesList { get => ConfluentConfig.SslCurvesList; set => ConfluentConfig.SslCurvesList = value; } - /// The client uses the TLS ClientHello signature_algorithms extension to indicate to the server which signature/hash algorithm pairs may be used in digital signatures. See manual page for `SSL_CTX_set1_sigalgs_list(3)`. OpenSSL >= 1.0.2 required. + /// The client uses the TLS ClientHello signature_algorithms extension to indicate to the server which signature/hash algorithm pairs may be used in digital signatures. See manual page for `SSL_CTX_set1_sigalgs_list(3)`. OpenSSL >= 1.0.2 required. default: '' importance: low public string SslSigalgsList { get => ConfluentConfig.SslSigalgsList; set => ConfluentConfig.SslSigalgsList = value; } - /// Path to client's private key (PEM) used for authentication. + /// Path to client's private key (PEM) used for authentication. default: '' importance: low public string SslKeyLocation { get => ConfluentConfig.SslKeyLocation; set => ConfluentConfig.SslKeyLocation = value; } - /// Private key passphrase + /// Private key passphrase default: '' importance: low public string SslKeyPassword { get => ConfluentConfig.SslKeyPassword; set => ConfluentConfig.SslKeyPassword = value; } - /// Path to client's public key (PEM) used for authentication. + /// Path to client's public key (PEM) used for authentication. default: '' importance: low public string SslCertificateLocation { get => ConfluentConfig.SslCertificateLocation; set => ConfluentConfig.SslCertificateLocation = value; } - /// File or directory path to CA certificate(s) for verifying the broker's key. + /// File or directory path to CA certificate(s) for verifying the broker's key. default: '' importance: medium public string SslCaLocation { get => ConfluentConfig.SslCaLocation; set => ConfluentConfig.SslCaLocation = value; } - /// Path to CRL for verifying broker's certificate validity. + /// Path to CRL for verifying broker's certificate validity. default: '' importance: low public string SslCrlLocation { get => ConfluentConfig.SslCrlLocation; set => ConfluentConfig.SslCrlLocation = value; } - /// Path to client's keystore (PKCS#12) used for authentication. + /// Path to client's keystore (PKCS#12) used for authentication. default: '' importance: low public string SslKeystoreLocation { get => ConfluentConfig.SslKeystoreLocation; set => ConfluentConfig.SslKeystoreLocation = value; } - /// Client's keystore (PKCS#12) password. + /// Client's keystore (PKCS#12) password. default: '' importance: low public string SslKeystorePassword { get => ConfluentConfig.SslKeystorePassword; set => ConfluentConfig.SslKeystorePassword = value; } - /// Kerberos principal name that Kafka runs as, not including /hostname@REALM + /// Kerberos principal name that Kafka runs as, not including /hostname@REALM default: kafka importance: low public string SaslKerberosServiceName { get => ConfluentConfig.SaslKerberosServiceName; set => ConfluentConfig.SaslKerberosServiceName = value; } - /// This client's Kerberos principal name. (Not supported on Windows, will use the logon user's principal). + /// This client's Kerberos principal name. (Not supported on Windows, will use the logon user's principal). default: kafkaclient importance: low public string SaslKerberosPrincipal { get => ConfluentConfig.SaslKerberosPrincipal; set => ConfluentConfig.SaslKerberosPrincipal = value; } - /// Full kerberos kinit command string, %{config.prop.name} is replaced by corresponding config object value, %{broker.name} returns the broker's hostname. + /// Full kerberos kinit command string, %{config.prop.name} is replaced by corresponding config object value, %{broker.name} returns the broker's hostname. default: kinit -S "%{sasl.kerberos.service.name}/%{broker.name}" -k -t "%{sasl.kerberos.keytab}" %{sasl.kerberos.principal} importance: low public string SaslKerberosKinitCmd { get => ConfluentConfig.SaslKerberosKinitCmd; set => ConfluentConfig.SaslKerberosKinitCmd = value; } - /// Path to Kerberos keytab file. Uses system default if not set.**NOTE**: This is not automatically used but must be added to the template in sasl.kerberos.kinit.cmd as ` ... -t %{sasl.kerberos.keytab}`. + /// Path to Kerberos keytab file. Uses system default if not set.**NOTE**: This is not automatically used but must be added to the template in sasl.kerberos.kinit.cmd as ` ... -t %{sasl.kerberos.keytab}`. default: '' importance: low public string SaslKerberosKeytab { get => ConfluentConfig.SaslKerberosKeytab; set => ConfluentConfig.SaslKerberosKeytab = value; } - /// Minimum time in milliseconds between key refresh attempts. + /// Minimum time in milliseconds between key refresh attempts. default: 60000 importance: low public int? SaslKerberosMinTimeBeforeRelogin { get => ConfluentConfig.SaslKerberosMinTimeBeforeRelogin; set => ConfluentConfig.SaslKerberosMinTimeBeforeRelogin = value; } - /// SASL username for use with the PLAIN and SASL-SCRAM-.. mechanisms + /// SASL username for use with the PLAIN and SASL-SCRAM-.. mechanisms default: '' importance: high public string SaslUsername { get => ConfluentConfig.SaslUsername; set => ConfluentConfig.SaslUsername = value; } - /// SASL password for use with the PLAIN and SASL-SCRAM-.. mechanism + /// SASL password for use with the PLAIN and SASL-SCRAM-.. mechanism default: '' importance: high public string SaslPassword { get => ConfluentConfig.SaslPassword; set => ConfluentConfig.SaslPassword = value; } - /// List of plugin libraries to load (; separated). The library search path is platform dependent (see dlopen(3) for Unix and LoadLibrary() for Windows). If no filename extension is specified the platform-specific extension (such as .dll or .so) will be appended automatically. + /// List of plugin libraries to load (; separated). The library search path is platform dependent (see dlopen(3) for Unix and LoadLibrary() for Windows). If no filename extension is specified the platform-specific extension (such as .dll or .so) will be appended automatically. default: '' importance: low public string PluginLibraryPaths { get => ConfluentConfig.PluginLibraryPaths; set => ConfluentConfig.PluginLibraryPaths = value; } + /// The maximum length of time (in milliseconds) before a cancellation request is acted on. Low values may result in measurably higher CPU usage. default: 100 range: 1 <= dotnet.cancellation.delay.max.ms <= 10000 importance: low public int CancellationDelayMaxMs { set => ConfluentConfig.CancellationDelayMaxMs = value; } } } \ No newline at end of file diff --git a/src/Silverback.Integration.Kafka/Silverback.Integration.Kafka.csproj b/src/Silverback.Integration.Kafka/Silverback.Integration.Kafka.csproj index f5e350987..f94a6f033 100644 --- a/src/Silverback.Integration.Kafka/Silverback.Integration.Kafka.csproj +++ b/src/Silverback.Integration.Kafka/Silverback.Integration.Kafka.csproj @@ -6,19 +6,19 @@ 1.0.1.0 1.0.1.0 - 0.6.1.0 + 0.10.0.0 BEagle1984, ppx80 - https://github.com/BEagle1984/silverback/blob/master/LICENSE + MIT https://github.com/BEagle1984/silverback/ true Silverback is a simple framework to build reactive, event-driven, microservices. This package contains an implementation of Silverback.Integration for the popular Apache Kafka message broker. https://github.com/BEagle1984/silverback/raw/develop/graphics/Icon.png + latest + 1701;1702;CS1591 - latest bin\Debug\netstandard2.0\Silverback.Integration.Kafka.xml - 1701;1702;CS1591 @@ -26,8 +26,8 @@ - - + + diff --git a/src/Silverback.Integration.S3/Silverback.Integration.S3.csproj b/src/Silverback.Integration.S3/Silverback.Integration.S3.csproj index 66a30d96a..1ea184c11 100644 --- a/src/Silverback.Integration.S3/Silverback.Integration.S3.csproj +++ b/src/Silverback.Integration.S3/Silverback.Integration.S3.csproj @@ -2,20 +2,30 @@ netstandard2.0 - 0.5.0.0 + 0.10.0.0 Silverback true 7.1 BEagle1984 - Silverback is a simple framework to build reactive, event-driven, microservices. This package enables the creation of the inbound and outbound messages DbSet. - https://github.com/BEagle1984/silverback/blob/master/LICENSE + Silverback is a simple framework to build reactive, event-driven, microservices. This package enables the storage of the message chunks into S3. + MIT https://github.com/BEagle1984/silverback/ https://github.com/BEagle1984/silverback/raw/develop/graphics/Icon.png + latest + 1701;1702;CS1591 + + + + bin\Debug\netstandard2.0\Silverback.Integration.S3.xml + + + + bin\Release\netstandard2.0\Silverback.Integration.S3.xml - - + + diff --git a/src/Silverback.Integration/AssemblyInfo.cs b/src/Silverback.Integration/AssemblyInfo.cs index b24034fc1..cbe904cf7 100644 --- a/src/Silverback.Integration/AssemblyInfo.cs +++ b/src/Silverback.Integration/AssemblyInfo.cs @@ -3,5 +3,6 @@ using System.Runtime.CompilerServices; -[assembly: InternalsVisibleTo("Silverback.Integration.Tests")] [assembly: InternalsVisibleTo("Silverback.Integration.EntityFrameworkCore")] +[assembly: InternalsVisibleTo("Silverback.Integration.Tests")] +[assembly: InternalsVisibleTo("Silverback.Integration.Configuration.Tests")] diff --git a/src/Silverback.Integration/Messaging/Batch/MessageBatch.cs b/src/Silverback.Integration/Messaging/Batch/MessageBatch.cs index e3ad86fff..e208d2768 100644 --- a/src/Silverback.Integration/Messaging/Batch/MessageBatch.cs +++ b/src/Silverback.Integration/Messaging/Batch/MessageBatch.cs @@ -20,24 +20,24 @@ public class MessageBatch private readonly IEndpoint _endpoint; private readonly BatchSettings _settings; private readonly IErrorPolicy _errorPolicy; + private readonly InboundMessageProcessor _inboundMessageProcessor; - private readonly Action, IServiceProvider> _messagesHandler; + private readonly Action, IServiceProvider> _messagesHandler; private readonly Action, IServiceProvider> _commitHandler; private readonly Action _rollbackHandler; private readonly IServiceProvider _serviceProvider; - private readonly IPublisher _publisher; private readonly ILogger _logger; private readonly MessageLogger _messageLogger; - private readonly List _messages; + private readonly List _messages; private readonly Timer _waitTimer; private Exception _processingException; public MessageBatch(IEndpoint endpoint, BatchSettings settings, - Action, IServiceProvider> messagesHandler, + Action, IServiceProvider> messagesHandler, Action, IServiceProvider> commitHandler, Action rollbackHandler, IErrorPolicy errorPolicy, @@ -49,12 +49,13 @@ public MessageBatch(IEndpoint endpoint, _commitHandler = commitHandler ?? throw new ArgumentNullException(nameof(commitHandler)); _rollbackHandler = rollbackHandler ?? throw new ArgumentNullException(nameof(rollbackHandler)); - _errorPolicy = errorPolicy; - _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); + _inboundMessageProcessor = serviceProvider.GetRequiredService(); + + _errorPolicy = errorPolicy; _settings = settings; - _messages = new List(_settings.Size); + _messages = new List(_settings.Size); if (_settings.MaxWaitTime < TimeSpan.MaxValue) { @@ -62,7 +63,6 @@ public MessageBatch(IEndpoint endpoint, _waitTimer.Elapsed += OnWaitTimerElapsed; } - _publisher = serviceProvider.GetRequiredService(); _logger = serviceProvider.GetRequiredService>(); _messageLogger = serviceProvider.GetRequiredService(); } @@ -71,16 +71,17 @@ public MessageBatch(IEndpoint endpoint, public int CurrentSize => _messages.Count; - public void AddMessage(MessageReceivedEventArgs messageArgs) + public void AddMessage(IInboundMessage message) { + // TODO: Check this! if (_processingException != null) throw new SilverbackException("Cannot add to the batch because the processing of the previous batch failed. See inner exception for details.", _processingException); lock (_messages) { - _messages.Add(messageArgs); + _messages.Add(message); - _messageLogger.LogTrace(_logger, "Message added to batch.", messageArgs.Message, _endpoint, this, messageArgs.Offset); + _messageLogger.LogInformation(_logger, "Message added to batch.", message, CurrentBatchId, CurrentSize); if (_messages.Count == 1) { @@ -109,32 +110,33 @@ private void ProcessBatch() { try { - _logger.LogTrace("Processing batch '{batchId}' containing {batchSize} message(s).", CurrentBatchId, _messages.Count); - - _errorPolicy.TryProcess( - new BatchCompleteEvent(CurrentBatchId, _messages), - _ => ProcessEachMessageAndPublishEvents()); + _inboundMessageProcessor.TryDeserializeAndProcess( + new InboundBatch(CurrentBatchId, _messages, _endpoint), + _errorPolicy, + deserializedBatch => ProcessEachMessageAndPublishEvents((IInboundBatch) deserializedBatch)); _messages.Clear(); } catch (Exception ex) { - _logger.LogError(ex, "Failed to process batch '{batchId}' containing {batchSize} message(s).", CurrentBatchId, _messages.Count); - _processingException = ex; throw new SilverbackException("Failed to process batch. See inner exception for details.", ex); } } - private void ProcessEachMessageAndPublishEvents() + private void ProcessEachMessageAndPublishEvents(IInboundBatch deserializedBatch) { using (var scope = _serviceProvider.CreateScope()) { + var publisher = scope.ServiceProvider.GetRequiredService(); + + var unwrappedMessages = deserializedBatch.Messages.Select(m => m.Message).ToList(); + try { - _publisher.Publish(new BatchCompleteEvent(CurrentBatchId, _messages)); - _messagesHandler(_messages, scope.ServiceProvider); - _publisher.Publish(new BatchProcessedEvent(CurrentBatchId, _messages)); + publisher.Publish(new BatchCompleteEvent(CurrentBatchId, unwrappedMessages)); + _messagesHandler(deserializedBatch.Messages, scope.ServiceProvider); + publisher.Publish(new BatchProcessedEvent(CurrentBatchId, unwrappedMessages)); _commitHandler?.Invoke(_messages.Select(m => m.Offset).ToList(), scope.ServiceProvider); } @@ -142,7 +144,7 @@ private void ProcessEachMessageAndPublishEvents() { _rollbackHandler?.Invoke(scope.ServiceProvider); - _publisher.Publish(new BatchAbortedEvent(CurrentBatchId, _messages, ex)); + publisher.Publish(new BatchAbortedEvent(CurrentBatchId, unwrappedMessages, ex)); throw; } diff --git a/src/Silverback.Integration/Messaging/Broker/Broker.cs b/src/Silverback.Integration/Messaging/Broker/Broker.cs index ac500cf00..c02b5c5ce 100644 --- a/src/Silverback.Integration/Messaging/Broker/Broker.cs +++ b/src/Silverback.Integration/Messaging/Broker/Broker.cs @@ -32,7 +32,7 @@ public virtual IProducer GetProducer(IEndpoint endpoint) { return _producers.GetOrAdd(endpoint, _ => { - _logger?.LogInformation($"Creating new producer for endpoint '{endpoint.Name}'"); + _logger.LogInformation($"Creating new producer for endpoint '{endpoint.Name}'"); return InstantiateProducer(endpoint); }); } @@ -147,6 +147,5 @@ public static void ThrowIfWrongEndpointType(IEndpoint endpoint) if (!(endpoint is TEndpoint)) throw new ArgumentException($"An endpoint of type {typeof(TEndpoint).Name} is expected."); } - } } \ No newline at end of file diff --git a/src/Silverback.Integration/Messaging/Broker/Consumer.cs b/src/Silverback.Integration/Messaging/Broker/Consumer.cs index b1128d345..1b0074a18 100644 --- a/src/Silverback.Integration/Messaging/Broker/Consumer.cs +++ b/src/Silverback.Integration/Messaging/Broker/Consumer.cs @@ -10,14 +10,9 @@ namespace Silverback.Messaging.Broker { public abstract class Consumer : EndpointConnectedObject, IConsumer { - private readonly ILogger _logger; - private readonly MessageLogger _messageLogger; - - protected Consumer(IBroker broker, IEndpoint endpoint,ILogger logger, MessageLogger messageLogger) - : base(broker, endpoint) + protected Consumer(IBroker broker, IEndpoint endpoint) + : base(broker, endpoint) { - _logger = logger; - _messageLogger = messageLogger; } public event EventHandler Received; @@ -34,11 +29,7 @@ protected void HandleMessage(byte[] message, IEnumerable headers, if (Received == null) throw new InvalidOperationException("A message was received but no handler is configured, please attach to the Received event."); - var deserializedMessage = Endpoint.Serializer.Deserialize(message); - - _messageLogger.LogTrace(_logger, "Message received.", deserializedMessage, Endpoint); - - Received.Invoke(this, new MessageReceivedEventArgs(deserializedMessage, headers, offset)); + Received.Invoke(this, new MessageReceivedEventArgs(message, headers, offset, Endpoint)); } } @@ -46,9 +37,8 @@ public abstract class Consumer : Consumer where TBroker : class, IBroker where TEndpoint : class, IEndpoint { - protected Consumer(IBroker broker, IEndpoint endpoint, - ILogger logger, MessageLogger messageLogger) - : base(broker, endpoint, logger, messageLogger) + protected Consumer(IBroker broker, IEndpoint endpoint) + : base(broker, endpoint) { } diff --git a/src/Silverback.Integration/Messaging/Broker/IBroker.cs b/src/Silverback.Integration/Messaging/Broker/IBroker.cs index 6d1977336..0b8bf026d 100644 --- a/src/Silverback.Integration/Messaging/Broker/IBroker.cs +++ b/src/Silverback.Integration/Messaging/Broker/IBroker.cs @@ -1,5 +1,6 @@ // Copyright (c) 2018-2019 Sergio Aquilini // This code is licensed under MIT license (see LICENSE file for details) + namespace Silverback.Messaging.Broker { /// diff --git a/src/Silverback.Integration/Messaging/Broker/MessageReceivedEventArgs.cs b/src/Silverback.Integration/Messaging/Broker/MessageReceivedEventArgs.cs index cc09e245f..542f4bafd 100644 --- a/src/Silverback.Integration/Messaging/Broker/MessageReceivedEventArgs.cs +++ b/src/Silverback.Integration/Messaging/Broker/MessageReceivedEventArgs.cs @@ -9,17 +9,18 @@ namespace Silverback.Messaging.Broker { public class MessageReceivedEventArgs : EventArgs { - public MessageReceivedEventArgs(object message, IEnumerable headers, IOffset offset) + public MessageReceivedEventArgs(byte[] message, IEnumerable headers, IOffset offset, + IEndpoint endpoint) { Message = message; Headers = headers; Offset = offset; + Endpoint = endpoint; } - public object Message { get; set; } - - public IEnumerable Headers { get; set; } - - public IOffset Offset { get; set; } + public byte[] Message { get; } + public IEnumerable Headers { get; } + public IOffset Offset { get; } + public IEndpoint Endpoint { get; } } } \ No newline at end of file diff --git a/src/Silverback.Integration/Messaging/Broker/Producer.cs b/src/Silverback.Integration/Messaging/Broker/Producer.cs index d5127379a..d68b59497 100644 --- a/src/Silverback.Integration/Messaging/Broker/Producer.cs +++ b/src/Silverback.Integration/Messaging/Broker/Producer.cs @@ -27,16 +27,23 @@ protected Producer(IBroker broker, IEndpoint endpoint, MessageKeyProvider messag public void Produce(object message, IEnumerable headers = null) => GetMessageContentChunks(message) - .ForEach(x => Produce(x.message, x.serializedMessage, headers)); + .ForEach(x => + { + var offset = Produce(x.message, x.serializedMessage, headers); + Trace(message, offset); + }); public Task ProduceAsync(object message, IEnumerable headers = null) => GetMessageContentChunks(message) - .ForEachAsync(x => ProduceAsync(x.message, x.serializedMessage, headers)); + .ForEachAsync(async x => + { + var offset = await ProduceAsync(x.message, x.serializedMessage, headers); + Trace(message, offset); + }); private IEnumerable<(object message, byte[] serializedMessage)> GetMessageContentChunks(object message) { _messageKeyProvider.EnsureKeyIsInitialized(message); - Trace(message); return ChunkProducer.ChunkIfNeeded( _messageKeyProvider.GetKey(message, false), @@ -45,12 +52,12 @@ public Task ProduceAsync(object message, IEnumerable headers = nu Endpoint.Serializer); } - private void Trace(object message) => - _messageLogger.LogTrace(_logger, "Producing message.", message, Endpoint); + private void Trace(object message, IOffset offset) => + _messageLogger.LogInformation(_logger, "Message produced.", message, Endpoint, offset); - protected abstract void Produce(object message, byte[] serializedMessage, IEnumerable headers); + protected abstract IOffset Produce(object message, byte[] serializedMessage, IEnumerable headers); - protected abstract Task ProduceAsync(object message, byte[] serializedMessage, IEnumerable headers); + protected abstract Task ProduceAsync(object message, byte[] serializedMessage, IEnumerable headers); } public abstract class Producer : Producer diff --git a/src/Silverback.Integration/Messaging/Broker/RawMessage.cs b/src/Silverback.Integration/Messaging/Broker/RawMessage.cs new file mode 100644 index 000000000..9d8768a93 --- /dev/null +++ b/src/Silverback.Integration/Messaging/Broker/RawMessage.cs @@ -0,0 +1,21 @@ +// Copyright (c) 2018-2019 Sergio Aquilini +// This code is licensed under MIT license (see LICENSE file for details) + +using System.Collections.Generic; +using Silverback.Messaging.Messages; + +namespace Silverback.Messaging.Broker +{ + public class RawMessage + { + public RawMessage(byte[] message, IEnumerable headers) + { + Message = message; + Headers = headers; + } + + public byte[] Message { get; private set; } + + public IEnumerable Headers { get; private set; } + } +} \ No newline at end of file diff --git a/src/Silverback.Integration/Messaging/Configuration/BrokerOptionsBuilder.cs b/src/Silverback.Integration/Messaging/Configuration/BrokerOptionsBuilder.cs index 162db594a..80b575ac8 100644 --- a/src/Silverback.Integration/Messaging/Configuration/BrokerOptionsBuilder.cs +++ b/src/Silverback.Integration/Messaging/Configuration/BrokerOptionsBuilder.cs @@ -4,10 +4,13 @@ using System; using System.Linq; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using Silverback.Background; using Silverback.Messaging.Broker; using Silverback.Messaging.Connectors; using Silverback.Messaging.Connectors.Repositories; +using Silverback.Messaging.ErrorHandling; using Silverback.Messaging.LargeMessages; using Silverback.Messaging.Messages; using Silverback.Messaging.Subscribers; @@ -29,6 +32,13 @@ public BrokerOptionsBuilder(IServiceCollection services) public BrokerOptionsBuilder AddInboundConnector() where TConnector : class, IInboundConnector { Services.AddSingleton(); + + if (Services.All(s => s.ImplementationType != typeof(InboundMessageUnwrapper))) + Services.AddScoped(); + + if (Services.All(s => s.ImplementationType != typeof(InboundMessageProcessor))) + Services.AddSingleton(); + return this; } @@ -93,14 +103,21 @@ public BrokerOptionsBuilder AddDeferredOutboundConnector(Func new OutboundQueueWorker( - s.GetRequiredService(), - s.GetRequiredService(), - s.GetRequiredService>(), - s.GetRequiredService(), - enforceMessageOrder, readPackageSize)); + Services + .AddSingleton(s => new OutboundQueueWorker( + s.GetRequiredService(), + s.GetRequiredService(), + s.GetRequiredService>(), + s.GetRequiredService(), + enforceMessageOrder, readPackageSize)) + .AddSingleton(s => new OutboundQueueWorkerService( + interval, + s.GetRequiredService(), + distributedLockSettings, + s.GetService() ?? new NullLockManager(), + s.GetRequiredService>())); return this; } @@ -109,13 +126,41 @@ internal BrokerOptionsBuilder AddOutboundWorker(bool enforceMessageOrder, int re /// Adds an to publish the queued messages to the configured broker. /// /// - /// if set to true the message order will be preserved (no message will be skipped). + /// The interval between each run (default is 500ms). + /// If set to true the message order will be preserved (no message will be skipped). /// The number of messages to be loaded from the queue at once. // TODO: Test - public BrokerOptionsBuilder AddOutboundWorker(Func outboundQueueConsumerFactory, bool enforceMessageOrder = true, int readPackageSize = 100) + public BrokerOptionsBuilder AddOutboundWorker(Func outboundQueueConsumerFactory, TimeSpan? interval = null, bool enforceMessageOrder = true, int readPackageSize = 100) => + AddOutboundWorker(outboundQueueConsumerFactory, new DistributedLockSettings(), interval, enforceMessageOrder, readPackageSize); + + /// + /// Adds an to publish the queued messages to the configured broker. + /// + /// + /// The settings for the locking mechanism. + /// The interval between each run (default is 500ms). + /// If set to true the message order will be preserved (no message will be skipped). + /// The number of messages to be loaded from the queue at once. + // TODO: Test + public BrokerOptionsBuilder AddOutboundWorker( + Func outboundQueueConsumerFactory, + DistributedLockSettings distributedLockSettings, TimeSpan? interval = null, + bool enforceMessageOrder = true, int readPackageSize = 100) { - AddOutboundWorker(enforceMessageOrder, readPackageSize); - Services.AddScoped(outboundQueueConsumerFactory); + if (outboundQueueConsumerFactory == null) throw new ArgumentNullException(nameof(outboundQueueConsumerFactory)); + if (distributedLockSettings == null) throw new ArgumentNullException(nameof(distributedLockSettings)); + + if (string.IsNullOrEmpty(distributedLockSettings.ResourceName)) + distributedLockSettings.ResourceName = "OutboundQueueWorker"; + + AddOutboundWorker( + interval ?? TimeSpan.FromMilliseconds(500), + distributedLockSettings, + enforceMessageOrder, + readPackageSize); + + Services + .AddScoped(outboundQueueConsumerFactory); return this; } @@ -140,7 +185,7 @@ public BrokerOptionsBuilder AddChunkStore(Func ch internal void CompleteWithDefaults() => SetDefaults(); /// - /// Sets the default values for the options that have not been explicitely set + /// Sets the default values for the options that have not been explicitly set /// by the user. /// protected virtual void SetDefaults() diff --git a/src/Silverback.Integration/Messaging/Configuration/ErrorPolicyBuilder.cs b/src/Silverback.Integration/Messaging/Configuration/ErrorPolicyBuilder.cs index 37ef5b338..ee7728366 100644 --- a/src/Silverback.Integration/Messaging/Configuration/ErrorPolicyBuilder.cs +++ b/src/Silverback.Integration/Messaging/Configuration/ErrorPolicyBuilder.cs @@ -21,22 +21,50 @@ public ErrorPolicyBuilder(IServiceProvider serviceProvider, ILoggerFactory logge _loggerFactory = loggerFactory; } + /// + /// Creates a chain of multiple policies to be applied one after the other to handle the processing error. + /// + /// The policies to be sequentially applied. + /// public ErrorPolicyChain Chain(params ErrorPolicyBase[] policies) => - new ErrorPolicyChain(_loggerFactory.CreateLogger(), - _serviceProvider.GetRequiredService(), policies); + new ErrorPolicyChain( + _serviceProvider, + _loggerFactory.CreateLogger(), + _serviceProvider.GetRequiredService(), + policies); + /// + /// Creates a retry policy to simply try again the message processing in case of processing errors. + /// + /// The optional delay between each retry. + /// The optional increment to be added to the initial delay at each retry. + /// public RetryErrorPolicy Retry(TimeSpan? initialDelay = null, TimeSpan? delayIncrement = null) => - new RetryErrorPolicy(_loggerFactory.CreateLogger(), - _serviceProvider.GetRequiredService(), initialDelay, delayIncrement); + new RetryErrorPolicy( + _serviceProvider, + _loggerFactory.CreateLogger(), + _serviceProvider.GetRequiredService(), + initialDelay, delayIncrement); + /// + /// Creates a skip policy to discard the message whose processing failed. + /// + /// public SkipMessageErrorPolicy Skip() => new SkipMessageErrorPolicy( + _serviceProvider, _loggerFactory.CreateLogger(), _serviceProvider.GetRequiredService()); + /// + /// Creates a move policy to forward the message to another endpoint in case of processing errors. + /// + /// + /// public MoveMessageErrorPolicy Move(IEndpoint endpoint) => new MoveMessageErrorPolicy( _serviceProvider.GetRequiredService(), endpoint, + _serviceProvider, _loggerFactory.CreateLogger(), _serviceProvider.GetRequiredService()); } diff --git a/src/Silverback.Integration/Messaging/Connectors/ExactlyOnceInboundConnector.cs b/src/Silverback.Integration/Messaging/Connectors/ExactlyOnceInboundConnector.cs index cd8e2e764..7fb3e0e6a 100644 --- a/src/Silverback.Integration/Messaging/Connectors/ExactlyOnceInboundConnector.cs +++ b/src/Silverback.Integration/Messaging/Connectors/ExactlyOnceInboundConnector.cs @@ -24,29 +24,28 @@ protected ExactlyOnceInboundConnector(IBroker broker, IServiceProvider servicePr _messageLogger = messageLogger; } - protected override void RelayMessages(IEnumerable messagesArgs, IEndpoint endpoint, InboundConnectorSettings settings, IServiceProvider serviceProvider) + protected override void RelayMessages(IEnumerable messages, IServiceProvider serviceProvider) { - messagesArgs = EnsureExactlyOnce(messagesArgs, endpoint, serviceProvider); + messages = EnsureExactlyOnce(messages, serviceProvider); - base.RelayMessages(messagesArgs, endpoint, settings, serviceProvider); + base.RelayMessages(messages, serviceProvider); } - private IEnumerable EnsureExactlyOnce(IEnumerable messagesArgs, IEndpoint endpoint, IServiceProvider serviceProvider) + private IEnumerable EnsureExactlyOnce(IEnumerable messages, IServiceProvider serviceProvider) { - foreach (var messageArgs in messagesArgs) + foreach (var message in messages) { - if (MustProcess(messageArgs, endpoint, serviceProvider)) + if (MustProcess(message, serviceProvider)) { - yield return messageArgs; + yield return message; } else { - _messageLogger.LogTrace(Logger, "Message is being skipped since it was already processed.", messageArgs.Message, - endpoint, offset: messageArgs.Offset); + _messageLogger.LogTrace(Logger, "Message is being skipped since it was already processed.", message); } } } - protected abstract bool MustProcess(MessageReceivedEventArgs messageArgs, IEndpoint endpoint, IServiceProvider serviceProvider); + protected abstract bool MustProcess(IInboundMessage message, IServiceProvider serviceProvider); } } \ No newline at end of file diff --git a/src/Silverback.Integration/Messaging/Connectors/IOutboundQueueWorker.cs b/src/Silverback.Integration/Messaging/Connectors/IOutboundQueueWorker.cs new file mode 100644 index 000000000..15737de96 --- /dev/null +++ b/src/Silverback.Integration/Messaging/Connectors/IOutboundQueueWorker.cs @@ -0,0 +1,13 @@ +// Copyright (c) 2018-2019 Sergio Aquilini +// This code is licensed under MIT license (see LICENSE file for details) + +using System.Threading; +using System.Threading.Tasks; + +namespace Silverback.Messaging.Connectors +{ + public interface IOutboundQueueWorker + { + Task ProcessQueue(CancellationToken stoppingToken); + } +} \ No newline at end of file diff --git a/src/Silverback.Integration/Messaging/Connectors/InboundConnector.cs b/src/Silverback.Integration/Messaging/Connectors/InboundConnector.cs index 1ddeb063d..25115ae20 100644 --- a/src/Silverback.Integration/Messaging/Connectors/InboundConnector.cs +++ b/src/Silverback.Integration/Messaging/Connectors/InboundConnector.cs @@ -41,7 +41,7 @@ public virtual IInboundConnector Bind(IEndpoint endpoint, IErrorPolicy errorPoli _broker, endpoint, settings, - RelayMessages, + HandleMessages, Commit, Rollback, errorPolicy, @@ -53,67 +53,41 @@ public virtual IInboundConnector Bind(IEndpoint endpoint, IErrorPolicy errorPoli return this; } - protected virtual void RelayMessages(IEnumerable messagesArgs, IEndpoint endpoint, InboundConnectorSettings settings, IServiceProvider serviceProvider) + protected void HandleMessages(IEnumerable messages, IServiceProvider serviceProvider) { - var messages = messagesArgs - .Select(args => HandleChunkedMessage(args, endpoint, serviceProvider) ? args : null) - .Select(UnwrapFailedMessage) + messages = messages + .Select(message => HandleChunkedMessage(message, serviceProvider)) .Where(args => args != null) - .SelectMany(args => settings.UnwrapMessages - ? new[] {args.Message, WrapInboundMessage(args, endpoint)} - : new[] {WrapInboundMessage(args, endpoint)}) .ToList(); if (!messages.Any()) return; - serviceProvider.GetRequiredService().Publish(messages); + RelayMessages(messages, serviceProvider); } - private bool HandleChunkedMessage(MessageReceivedEventArgs args, IEndpoint endpoint, IServiceProvider serviceProvider) + private IInboundMessage HandleChunkedMessage(IInboundMessage message, IServiceProvider serviceProvider) { - if (args.Message is MessageChunk chunk) - { - var joined = serviceProvider.GetRequiredService().JoinIfComplete(chunk); + if (!(message.Message is MessageChunk chunk)) + return message; - if (joined == null) - return false; - - args.Message = endpoint.Serializer.Deserialize(joined); - } + var joinedMessage = serviceProvider.GetRequiredService().JoinIfComplete(chunk); - return true; - } + if (joinedMessage == null) + return null; - private MessageReceivedEventArgs UnwrapFailedMessage(MessageReceivedEventArgs args) - { - if (args?.Message is FailedMessage failedMessage) - args.Message = failedMessage.Message; + var deserializedJoinedMessage = message.Endpoint.Serializer.Deserialize(joinedMessage); - return args; + return InboundMessageHelper.CreateNewInboundMessage(deserializedJoinedMessage, message); } - private IInboundMessage WrapInboundMessage(MessageReceivedEventArgs args, IEndpoint endpoint) - { - var wrapper = (IInboundMessage) Activator.CreateInstance(typeof(InboundMessage<>).MakeGenericType(args.Message.GetType())); - - wrapper.Endpoint = endpoint; - wrapper.Message = args.Message; - - if (args.Headers != null) - wrapper.Headers.AddRange(args.Headers); - - return wrapper; - } + protected virtual void RelayMessages(IEnumerable messages, IServiceProvider serviceProvider) => + serviceProvider.GetRequiredService().Publish(messages); - protected virtual void Commit(IServiceProvider serviceProvider) - { + protected virtual void Commit(IServiceProvider serviceProvider) => serviceProvider.GetService()?.Commit(); - } - protected virtual void Rollback(IServiceProvider serviceProvider) - { + protected virtual void Rollback(IServiceProvider serviceProvider) => serviceProvider.GetService()?.Rollback(); - } } } \ No newline at end of file diff --git a/src/Silverback.Integration/Messaging/Connectors/InboundConsumer.cs b/src/Silverback.Integration/Messaging/Connectors/InboundConsumer.cs index 8eafde44f..e7df39a84 100644 --- a/src/Silverback.Integration/Messaging/Connectors/InboundConsumer.cs +++ b/src/Silverback.Integration/Messaging/Connectors/InboundConsumer.cs @@ -19,8 +19,9 @@ public class InboundConsumer private readonly IEndpoint _endpoint; private readonly InboundConnectorSettings _settings; private readonly IErrorPolicy _errorPolicy; + private readonly InboundMessageProcessor _inboundMessageProcessor; - private readonly Action, IEndpoint, InboundConnectorSettings, IServiceProvider> _messagesHandler; + private readonly Action, IServiceProvider> _messagesHandler; private readonly Action _commitHandler; private readonly Action _rollbackHandler; @@ -33,7 +34,7 @@ public class InboundConsumer public InboundConsumer(IBroker broker, IEndpoint endpoint, InboundConnectorSettings settings, - Action, IEndpoint, InboundConnectorSettings, IServiceProvider> messagesHandler, + Action, IServiceProvider> messagesHandler, Action commitHandler, Action rollbackHandler, IErrorPolicy errorPolicy, @@ -50,6 +51,7 @@ public InboundConsumer(IBroker broker, _serviceProvider = serviceProvider; _logger = serviceProvider.GetRequiredService>(); _messageLogger = serviceProvider.GetRequiredService(); + _inboundMessageProcessor = serviceProvider.GetRequiredService(); _consumer = broker.GetConsumer(_endpoint); @@ -67,43 +69,59 @@ private void Bind() var batch = new MessageBatch( _endpoint, _settings.Batch, - (messageArgs, serviceProvider) => _messagesHandler(messageArgs, _endpoint, _settings, serviceProvider), + _messagesHandler, Commit, _rollbackHandler, _errorPolicy, _serviceProvider); - _consumer.Received += (_, args) => batch.AddMessage(args); + _consumer.Received += (_, args) => batch.AddMessage(CreateInboundMessage(args)); } else { - _consumer.Received += (_, args) => ProcessSingleMessage(args); + _consumer.Received += (_, args) => ProcessSingleMessage(CreateInboundMessage(args)); } } - private void ProcessSingleMessage(MessageReceivedEventArgs messageArgs) + private IInboundMessage CreateInboundMessage(MessageReceivedEventArgs args) { - _messageLogger.LogTrace(_logger, "Processing message.", messageArgs.Message, _endpoint, offset: messageArgs.Offset); - - _errorPolicy.TryProcess(messageArgs.Message, _ => + var message = new InboundMessage { - using (var scope = _serviceProvider.CreateScope()) + Message = args.Message, + Endpoint = _endpoint, + Offset = args.Offset, + MustUnwrap = _settings.UnwrapMessages + }; + + if (args.Headers != null) + message.Headers.AddRange(args.Headers); + + return message; + } + + private void ProcessSingleMessage(IInboundMessage message) + { + _inboundMessageProcessor.TryDeserializeAndProcess( + message, + _errorPolicy, + deserializedMessage => { - RelayAndCommitSingleMessage(messageArgs, scope.ServiceProvider); - } - }); + using (var scope = _serviceProvider.CreateScope()) + { + RelayAndCommitSingleMessage(deserializedMessage, scope.ServiceProvider); + } + }); } - private void RelayAndCommitSingleMessage(MessageReceivedEventArgs messageArgs, IServiceProvider serviceProvider) + private void RelayAndCommitSingleMessage(IInboundMessage message, IServiceProvider serviceProvider) { try { - _messagesHandler(new[] { messageArgs }, _endpoint, _settings, serviceProvider); - Commit(new[] { messageArgs.Offset }, serviceProvider); + _messagesHandler(new[] { message }, serviceProvider); + Commit(new[] { message.Offset }, serviceProvider); } - catch (Exception ex) + catch (Exception) { - _messageLogger.LogWarning(_logger, ex, "Error occurred processing the message.", messageArgs.Message, _endpoint, offset: messageArgs.Offset); Rollback(serviceProvider); throw; } diff --git a/src/Silverback.Integration/Messaging/Connectors/InboundMessageUnwrapper.cs b/src/Silverback.Integration/Messaging/Connectors/InboundMessageUnwrapper.cs new file mode 100644 index 000000000..5771dffa6 --- /dev/null +++ b/src/Silverback.Integration/Messaging/Connectors/InboundMessageUnwrapper.cs @@ -0,0 +1,14 @@ +// Copyright (c) 2018-2019 Sergio Aquilini +// This code is licensed under MIT license (see LICENSE file for details) + +using Silverback.Messaging.Messages; +using Silverback.Messaging.Subscribers; + +namespace Silverback.Messaging.Connectors +{ + public class InboundMessageUnwrapper : ISubscriber + { + [Subscribe] + public object OnMessageReceived(IInboundMessage message) => message.MustUnwrap ? message.Message : null; + } +} diff --git a/src/Silverback.Integration/Messaging/Connectors/LoggedInboundConnector.cs b/src/Silverback.Integration/Messaging/Connectors/LoggedInboundConnector.cs index 9b71db4d6..781724b7a 100644 --- a/src/Silverback.Integration/Messaging/Connectors/LoggedInboundConnector.cs +++ b/src/Silverback.Integration/Messaging/Connectors/LoggedInboundConnector.cs @@ -22,14 +22,14 @@ public LoggedInboundConnector(IBroker broker, IServiceProvider serviceProvider, { } - protected override bool MustProcess(MessageReceivedEventArgs messageArgs, IEndpoint sourceEndpoint, IServiceProvider serviceProvider) + protected override bool MustProcess(IInboundMessage message, IServiceProvider serviceProvider) { var inboundLog = serviceProvider.GetRequiredService(); - if (inboundLog.Exists(messageArgs.Message, sourceEndpoint)) + if (inboundLog.Exists(message.Message, message.Endpoint)) return false; - inboundLog.Add(messageArgs.Message, sourceEndpoint); + inboundLog.Add(message.Message, message.Endpoint); return true; } diff --git a/src/Silverback.Integration/Messaging/Connectors/OffsetStoredInboundConnector.cs b/src/Silverback.Integration/Messaging/Connectors/OffsetStoredInboundConnector.cs index 0099fc459..26e2ed1bb 100644 --- a/src/Silverback.Integration/Messaging/Connectors/OffsetStoredInboundConnector.cs +++ b/src/Silverback.Integration/Messaging/Connectors/OffsetStoredInboundConnector.cs @@ -18,15 +18,15 @@ public OffsetStoredInboundConnector(IBroker broker, IServiceProvider serviceProv { } - protected override bool MustProcess(MessageReceivedEventArgs messageArgs, IEndpoint sourceEndpoint, IServiceProvider serviceProvider) + protected override bool MustProcess(IInboundMessage message, IServiceProvider serviceProvider) { var offsetStore = serviceProvider.GetRequiredService(); - var latest = offsetStore.GetLatestValue(messageArgs.Offset.Key); - if (latest != null && messageArgs.Offset.CompareTo(latest) <= 0) + var latest = offsetStore.GetLatestValue(message.Offset.Key); + if (latest != null && message.Offset.CompareTo(latest) <= 0) return false; - offsetStore.Store(messageArgs.Offset); + offsetStore.Store(message.Offset); return true; } diff --git a/src/Silverback.Integration/Messaging/Connectors/OutboundQueueWorker.cs b/src/Silverback.Integration/Messaging/Connectors/OutboundQueueWorker.cs index c7dedc1ed..039fbc260 100644 --- a/src/Silverback.Integration/Messaging/Connectors/OutboundQueueWorker.cs +++ b/src/Silverback.Integration/Messaging/Connectors/OutboundQueueWorker.cs @@ -2,6 +2,10 @@ // This code is licensed under MIT license (see LICENSE file for details) using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Silverback.Messaging.Broker; using Silverback.Messaging.Connectors.Repositories; @@ -9,23 +13,19 @@ namespace Silverback.Messaging.Connectors { - /// - /// Publishes the messages in the outbox queue to the configured message broker. - /// - public class OutboundQueueWorker + public class OutboundQueueWorker : IOutboundQueueWorker { - private readonly IOutboundQueueConsumer _queue; + private readonly IServiceProvider _serviceProvider; private readonly IBroker _broker; private readonly MessageLogger _messageLogger; private readonly ILogger _logger; private readonly int _readPackageSize; private readonly bool _enforceMessageOrder; - - public OutboundQueueWorker(IOutboundQueueConsumer queue, IBroker broker, ILogger logger, + public OutboundQueueWorker(IServiceProvider serviceProvider, IBroker broker, ILogger logger, MessageLogger messageLogger, bool enforceMessageOrder, int readPackageSize) { - _queue = queue; + _serviceProvider = serviceProvider; _broker = broker; _messageLogger = messageLogger; _logger = logger; @@ -33,35 +33,61 @@ public OutboundQueueWorker(IOutboundQueueConsumer queue, IBroker broker, ILogger _readPackageSize = readPackageSize; } - public void ProcessQueue() + public async Task ProcessQueue(CancellationToken stoppingToken) { - foreach (var message in _queue.Dequeue(_readPackageSize)) + try { - ProcessMessage(message); + using (var scope = _serviceProvider.CreateScope()) + { + await ProcessQueue(scope.ServiceProvider.GetRequiredService(), stoppingToken); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error occurred processing the outbound queue. See inner exception for details."); } } - private void ProcessMessage(QueuedMessage message) + private async Task ProcessQueue(IOutboundQueueConsumer queue, CancellationToken stoppingToken) + { + _logger.LogTrace($"Reading outbound messages from queue (limit: {_readPackageSize})."); + + var messages = (await queue.Dequeue(_readPackageSize)).ToList(); + + if (!messages.Any()) + _logger.LogTrace("The outbound queue is empty."); + + for (var i = 0; i < messages.Count; i++) + { + _logger.LogTrace($"Processing message {i + 1} of {messages.Count}."); + await ProcessMessage(messages[i], queue); + + if (stoppingToken.IsCancellationRequested) + break; + } + } + + private async Task ProcessMessage(QueuedMessage message, IOutboundQueueConsumer queue) { try { - ProduceMessage(message.Message); + await ProduceMessage(message.Message); - _queue.Acknowledge(message); + await queue.Acknowledge(message); } catch (Exception ex) { _messageLogger.LogError(_logger, ex, "Failed to publish queued message.", message?.Message.Message, message?.Message.Endpoint); - _queue.Retry(message); + await queue.Retry(message); // Rethrow if message order has to be preserved, otherwise go ahead with next message in the queue if (_enforceMessageOrder) throw; } } - - protected virtual void ProduceMessage(IOutboundMessage message) - => _broker.GetProducer(message.Endpoint).Produce(message.Message, message.Headers); + + protected virtual Task ProduceMessage(IOutboundMessage message) + => _broker.GetProducer(message.Endpoint).ProduceAsync(message.Message, message.Headers); } } diff --git a/src/Silverback.Integration/Messaging/Connectors/OutboundQueueWorkerService.cs b/src/Silverback.Integration/Messaging/Connectors/OutboundQueueWorkerService.cs new file mode 100644 index 000000000..06c57e388 --- /dev/null +++ b/src/Silverback.Integration/Messaging/Connectors/OutboundQueueWorkerService.cs @@ -0,0 +1,28 @@ +// Copyright (c) 2018-2019 Sergio Aquilini +// This code is licensed under MIT license (see LICENSE file for details) + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Silverback.Background; + +namespace Silverback.Messaging.Connectors +{ + public class OutboundQueueWorkerService : RecurringDistributedBackgroundService + { + private readonly IOutboundQueueWorker _outboundQueueWorker; + private readonly TimeSpan? _interval; + + public OutboundQueueWorkerService(TimeSpan interval, IOutboundQueueWorker outboundQueueWorker, DistributedLockSettings distributedLockSettings, + IDistributedLockManager distributedLockManager, ILogger logger) + : base(interval, distributedLockSettings, distributedLockManager, logger) + { + _outboundQueueWorker = outboundQueueWorker; + _interval = interval; + } + + protected override Task ExecuteRecurringAsync(CancellationToken stoppingToken) => + _outboundQueueWorker.ProcessQueue(stoppingToken); + } +} \ No newline at end of file diff --git a/src/Silverback.Integration/Messaging/Connectors/Repositories/IInboundLog.cs b/src/Silverback.Integration/Messaging/Connectors/Repositories/IInboundLog.cs index 891d365e7..d3aafa613 100644 --- a/src/Silverback.Integration/Messaging/Connectors/Repositories/IInboundLog.cs +++ b/src/Silverback.Integration/Messaging/Connectors/Repositories/IInboundLog.cs @@ -1,5 +1,6 @@ // Copyright (c) 2018-2019 Sergio Aquilini // This code is licensed under MIT license (see LICENSE file for details) + namespace Silverback.Messaging.Connectors.Repositories { public interface IInboundLog diff --git a/src/Silverback.Integration/Messaging/Connectors/Repositories/IOutboundQueueConsumer.cs b/src/Silverback.Integration/Messaging/Connectors/Repositories/IOutboundQueueConsumer.cs index 6678a93cc..46116d2ba 100644 --- a/src/Silverback.Integration/Messaging/Connectors/Repositories/IOutboundQueueConsumer.cs +++ b/src/Silverback.Integration/Messaging/Connectors/Repositories/IOutboundQueueConsumer.cs @@ -2,6 +2,7 @@ // This code is licensed under MIT license (see LICENSE file for details) using System.Collections.Generic; +using System.Threading.Tasks; namespace Silverback.Messaging.Connectors.Repositories { @@ -9,16 +10,16 @@ public interface IOutboundQueueConsumer { int Length { get; } - IEnumerable Dequeue(int count); + Task> Dequeue(int count); /// /// Re-enqueue the message to retry. /// - void Retry(QueuedMessage queuedMessage); + Task Retry(QueuedMessage queuedMessage); /// /// Acknowledges the specified message has been sent. /// - void Acknowledge(QueuedMessage queuedMessage); + Task Acknowledge(QueuedMessage queuedMessage); } } \ No newline at end of file diff --git a/src/Silverback.Integration/Messaging/Connectors/Repositories/InMemoryOutboundQueue.cs b/src/Silverback.Integration/Messaging/Connectors/Repositories/InMemoryOutboundQueue.cs index dfa20709b..33abdc321 100644 --- a/src/Silverback.Integration/Messaging/Connectors/Repositories/InMemoryOutboundQueue.cs +++ b/src/Silverback.Integration/Messaging/Connectors/Repositories/InMemoryOutboundQueue.cs @@ -40,16 +40,21 @@ public Task Enqueue(IOutboundMessage message) #region Reader - public IEnumerable Dequeue(int count) => Entries.Take(count).ToArray(); + public Task> Dequeue(int count) => Task.FromResult(Entries.Take(count).ToArray().AsEnumerable()); - public void Retry(QueuedMessage queuedMessage) + public Task Retry(QueuedMessage queuedMessage) { // Nothing to do in the current implementation // --> the messages just stay in the queue until acknowledged // --> that's why reading is not thread-safe + return Task.CompletedTask; } - public void Acknowledge(QueuedMessage queuedMessage) => Remove(queuedMessage); + public Task Acknowledge(QueuedMessage queuedMessage) + { + Remove(queuedMessage); + return Task.CompletedTask; + } #endregion } diff --git a/src/Silverback.Integration/Messaging/ErrorHandling/ErrorAction.cs b/src/Silverback.Integration/Messaging/ErrorHandling/ErrorAction.cs index 404e37391..054ecc10f 100644 --- a/src/Silverback.Integration/Messaging/ErrorHandling/ErrorAction.cs +++ b/src/Silverback.Integration/Messaging/ErrorHandling/ErrorAction.cs @@ -1,5 +1,6 @@ // Copyright (c) 2018-2019 Sergio Aquilini // This code is licensed under MIT license (see LICENSE file for details) + namespace Silverback.Messaging.ErrorHandling { public enum ErrorAction diff --git a/src/Silverback.Integration/Messaging/ErrorHandling/ErrorHandlerEventArgs.cs b/src/Silverback.Integration/Messaging/ErrorHandling/ErrorHandlerEventArgs.cs deleted file mode 100644 index e78bd9be0..000000000 --- a/src/Silverback.Integration/Messaging/ErrorHandling/ErrorHandlerEventArgs.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) 2018-2019 Sergio Aquilini -// This code is licensed under MIT license (see LICENSE file for details) - -using System; -using Silverback.Messaging.Messages; - -namespace Silverback.Messaging.ErrorHandling -{ - public class ErrorHandlerEventArgs : EventArgs - { - public ErrorHandlerEventArgs(Exception exception, FailedMessage message) - { - Exception = exception; - FailedMessage = message; - Action = ErrorAction.StopConsuming; - } - - public Exception Exception { get; } - public FailedMessage FailedMessage { get; } - public ErrorAction Action { get; set; } - } -} \ No newline at end of file diff --git a/src/Silverback.Integration/Messaging/ErrorHandling/ErrorPolicyBase.cs b/src/Silverback.Integration/Messaging/ErrorHandling/ErrorPolicyBase.cs index 7847fb227..213c1c51c 100644 --- a/src/Silverback.Integration/Messaging/ErrorHandling/ErrorPolicyBase.cs +++ b/src/Silverback.Integration/Messaging/ErrorHandling/ErrorPolicyBase.cs @@ -4,75 +4,135 @@ using System; using System.Collections.Generic; using System.Linq; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Silverback.Messaging.Messages; +using Silverback.Messaging.Publishing; namespace Silverback.Messaging.ErrorHandling { public abstract class ErrorPolicyBase : IErrorPolicy { + private readonly IServiceProvider _serviceProvider; private readonly ILogger _logger; private readonly MessageLogger _messageLogger; private readonly List _excludedExceptions = new List(); private readonly List _includedExceptions = new List(); - private Func _applyRule; - private int _maxFailedAttempts = -1; + private Func _applyRule; + - protected ErrorPolicyBase(ILogger logger, MessageLogger messageLogger) + protected ErrorPolicyBase(IServiceProvider serviceProvider, ILogger logger, MessageLogger messageLogger) { + _serviceProvider = serviceProvider; _logger = logger; _messageLogger = messageLogger; } + internal Func MessageToPublishFactory { get; private set; } + + internal int MaxFailedAttemptsSetting { get; private set; } = -1; + + /// + /// Restricts the application of this policy to the specified exception type only. + /// It is possible to combine multiple calls to ApplyTo and Exclude. + /// + /// The type of the exception to be handled. + /// public ErrorPolicyBase ApplyTo() where T : Exception { ApplyTo(typeof(T)); return this; } + /// + /// Restricts the application of this policy to the specified exception type only. + /// It is possible to combine multiple calls to ApplyTo and Exclude. + /// + /// The type of the exception to be handled. + /// public ErrorPolicyBase ApplyTo(Type exceptionType) { _includedExceptions.Add(exceptionType); return this; } + /// + /// Restricts the application of this policy to all exceptions but the specified type. + /// It is possible to combine multiple calls to ApplyTo and Exclude. + /// + /// The type of the exception to be ignored. + /// public ErrorPolicyBase Exclude() where T : Exception { Exclude(typeof(T)); return this; } + /// + /// Restricts the application of this policy to all exceptions but the specified type. + /// It is possible to combine multiple calls to ApplyTo and Exclude. + /// + /// The type of the exception to be ignored. + /// public ErrorPolicyBase Exclude(Type exceptionType) { _excludedExceptions.Add(exceptionType); return this; } - public ErrorPolicyBase ApplyWhen(Func applyRule) + /// + /// Specifies a predicate to be used to determine whether the policy has to be applied + /// according to the current message and exception. + /// + /// The predicate. + /// + + public ErrorPolicyBase ApplyWhen(Func applyRule) { _applyRule = applyRule; return this; } + /// + /// Specifies how many times this rule can be applied to the same message. Most useful + /// for and to limit the + /// number of iterations. + /// If multiple policies are chained in an then the next policy will + /// be triggered after the allotted amount of retries. + /// + /// The number of retries. + /// public ErrorPolicyBase MaxFailedAttempts(int maxFailedAttempts) { - _maxFailedAttempts = maxFailedAttempts; + MaxFailedAttemptsSetting = maxFailedAttempts; + return this; + } + + /// + /// Specify a delegate to create a message to be published to the internal bus + /// when this policy is applied. Useful to execute some custom code. + /// + /// The factory returning the message to be published. + /// + public ErrorPolicyBase Publish(Func factory) + { + MessageToPublishFactory = factory; return this; } - public virtual bool CanHandle(FailedMessage failedMessage, Exception exception) + public virtual bool CanHandle(IInboundMessage message, Exception exception) { - if (failedMessage == null) + if (message == null) { _logger.LogTrace($"The policy '{GetType().Name}' cannot be applied because the message is null."); return false; } - if (_maxFailedAttempts >= 0 && failedMessage.FailedAttempts > _maxFailedAttempts) + if (MaxFailedAttemptsSetting >= 0 && message.FailedAttempts > MaxFailedAttemptsSetting) { _messageLogger.LogTrace(_logger, $"The policy '{GetType().Name}' will be skipped because the current failed attempts " + - $"({failedMessage.FailedAttempts}) exceeds the configured maximum attempts " + - $"({_maxFailedAttempts}).", failedMessage); + $"({message.FailedAttempts}) exceeds the configured maximum attempts " + + $"({MaxFailedAttemptsSetting}).", message); return false; } @@ -80,7 +140,7 @@ public virtual bool CanHandle(FailedMessage failedMessage, Exception exception) if (_includedExceptions.Any() && _includedExceptions.All(e => !e.IsInstanceOfType(exception))) { _messageLogger.LogTrace(_logger, $"The policy '{GetType().Name}' will be skipped because the {exception.GetType().Name} " + - $"is not in the list of handled exceptions.", failedMessage); + $"is not in the list of handled exceptions.", message); return false; } @@ -88,21 +148,37 @@ public virtual bool CanHandle(FailedMessage failedMessage, Exception exception) if (_excludedExceptions.Any(e => e.IsInstanceOfType(exception))) { _messageLogger.LogTrace(_logger, $"The policy '{GetType().Name}' will be skipped because the {exception.GetType().Name} " + - $"is in the list of excluded exceptions.", failedMessage); + $"is in the list of excluded exceptions.", message); return false; } - if (_applyRule != null && !_applyRule.Invoke(failedMessage, exception)) + if (_applyRule != null && !_applyRule.Invoke(message, exception)) { _messageLogger.LogTrace(_logger, $"The policy '{GetType().Name}' will be skipped because the apply rule has been " + - $"evaluated and returned false.", failedMessage); + $"evaluated and returned false.", message); return false; } return true; } - public abstract ErrorAction HandleError(FailedMessage failedMessage, Exception exception); + public ErrorAction HandleError(IInboundMessage message, Exception exception) + { + var result = ApplyPolicy(message, exception); + + if (MessageToPublishFactory != null) + { + using (var scope = _serviceProvider.CreateScope()) + { + scope.ServiceProvider.GetRequiredService() + .Publish(MessageToPublishFactory.Invoke(message)); + } + } + + return result; + } + + protected abstract ErrorAction ApplyPolicy(IInboundMessage message, Exception exception); } } \ No newline at end of file diff --git a/src/Silverback.Integration/Messaging/ErrorHandling/ErrorPolicyChain.cs b/src/Silverback.Integration/Messaging/ErrorHandling/ErrorPolicyChain.cs index fc2adab49..612e42bb3 100644 --- a/src/Silverback.Integration/Messaging/ErrorHandling/ErrorPolicyChain.cs +++ b/src/Silverback.Integration/Messaging/ErrorHandling/ErrorPolicyChain.cs @@ -18,32 +18,47 @@ public class ErrorPolicyChain : ErrorPolicyBase private readonly MessageLogger _messageLogger; private readonly IEnumerable _policies; - public ErrorPolicyChain(ILogger logger, MessageLogger messageLogger, params ErrorPolicyBase[] policies) - : this (policies.AsEnumerable(), logger, messageLogger) + public ErrorPolicyChain(IServiceProvider serviceProvider, ILogger logger, MessageLogger messageLogger, params ErrorPolicyBase[] policies) + : this (policies.AsEnumerable(), serviceProvider, logger, messageLogger) { } - public ErrorPolicyChain(IEnumerable policies, ILogger logger, MessageLogger messageLogger) - : base(logger, messageLogger) + public ErrorPolicyChain(IEnumerable policies, IServiceProvider serviceProvider, ILogger logger, MessageLogger messageLogger) + : base(serviceProvider, logger, messageLogger) { _logger = logger; _messageLogger = messageLogger; _policies = policies ?? throw new ArgumentNullException(nameof(policies)); + StackMaxFailedAttempts(policies); + if (_policies.Any(p => p == null)) throw new ArgumentNullException(nameof(policies), "One or more policies in the chain have a null value."); } - public override ErrorAction HandleError(FailedMessage failedMessage, Exception exception) + protected override ErrorAction ApplyPolicy(IInboundMessage message, Exception exception) { foreach (var policy in _policies) { - if (policy.CanHandle(failedMessage, exception)) - return policy.HandleError(failedMessage, exception); + if (policy.CanHandle(message, exception)) + return policy.HandleError(message, exception); } - _messageLogger.LogTrace(_logger, "All policies have been applied but the message still couldn't be successfully processed. The consumer will be stopped.", failedMessage); + _messageLogger.LogTrace(_logger, "All policies have been applied but the message still couldn't be successfully processed. The consumer will be stopped.", message); return ErrorAction.StopConsuming; } + + private static void StackMaxFailedAttempts(IEnumerable policies) + { + var totalAttempts = 0; + foreach (var policy in policies) + { + if (policy.MaxFailedAttemptsSetting <= 0) + continue; + + totalAttempts += policy.MaxFailedAttemptsSetting; + policy.MaxFailedAttempts(totalAttempts); + } + } } } \ No newline at end of file diff --git a/src/Silverback.Integration/Messaging/ErrorHandling/ErrorPolicyTryProcessExtension.cs b/src/Silverback.Integration/Messaging/ErrorHandling/ErrorPolicyTryProcessExtension.cs deleted file mode 100644 index f092f0a05..000000000 --- a/src/Silverback.Integration/Messaging/ErrorHandling/ErrorPolicyTryProcessExtension.cs +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright (c) 2018-2019 Sergio Aquilini -// This code is licensed under MIT license (see LICENSE file for details) - -using System; -using Silverback.Messaging.Broker; -using Silverback.Messaging.Messages; - -namespace Silverback.Messaging.ErrorHandling -{ - // TODO: Test - public static class ErrorPolicyTryProcessExtension - { - public static void TryProcess(this IErrorPolicy errorPolicy, TMessage message, Action messageHandler) - { - int attempts = 1; - - while (true) - { - var result = HandleMessage(message, messageHandler, attempts, errorPolicy); - - if (result.IsSuccessful || result.Action == ErrorAction.Skip) - return; - - attempts++; - } - } - - private static MessageHandlerResult HandleMessage(TMessage message, Action messageHandler, int failedAttempts, - IErrorPolicy errorPolicy) - { - try - { - messageHandler(message); - - return MessageHandlerResult.Success; - } - catch (Exception ex) - { - if (errorPolicy == null) - throw; - - var failedMessage = new FailedMessage(message, failedAttempts); - - if (!errorPolicy.CanHandle(failedMessage, ex)) - throw; - - var action = errorPolicy.HandleError(failedMessage, ex); - - if (action == ErrorAction.StopConsuming) - throw; - - return MessageHandlerResult.Error(action); - } - } - } -} diff --git a/src/Silverback.Integration/Messaging/ErrorHandling/IErrorPolicy.cs b/src/Silverback.Integration/Messaging/ErrorHandling/IErrorPolicy.cs index c5963fc7d..f0d6b3a6d 100644 --- a/src/Silverback.Integration/Messaging/ErrorHandling/IErrorPolicy.cs +++ b/src/Silverback.Integration/Messaging/ErrorHandling/IErrorPolicy.cs @@ -11,8 +11,8 @@ namespace Silverback.Messaging.ErrorHandling /// public interface IErrorPolicy { - bool CanHandle(FailedMessage failedMessage, Exception exception); + bool CanHandle(IInboundMessage message, Exception exception); - ErrorAction HandleError(FailedMessage failedMessage, Exception exception); + ErrorAction HandleError(IInboundMessage message, Exception exception); } } \ No newline at end of file diff --git a/src/Silverback.Integration/Messaging/ErrorHandling/InboundMessageProcessor.cs b/src/Silverback.Integration/Messaging/ErrorHandling/InboundMessageProcessor.cs new file mode 100644 index 000000000..dc58e58ce --- /dev/null +++ b/src/Silverback.Integration/Messaging/ErrorHandling/InboundMessageProcessor.cs @@ -0,0 +1,102 @@ +// Copyright (c) 2018-2019 Sergio Aquilini +// This code is licensed under MIT license (see LICENSE file for details) + +using System; +using System.Linq; +using Microsoft.Extensions.Logging; +using Silverback.Messaging.Broker; +using Silverback.Messaging.Messages; +using Silverback.Util; + +namespace Silverback.Messaging.ErrorHandling +{ + // TODO: Test + public class InboundMessageProcessor + { + private readonly ILogger _logger; + private readonly MessageLogger _messageLogger; + + public InboundMessageProcessor(ILogger logger, MessageLogger messageLogger) + { + _logger = logger; + _messageLogger = messageLogger; + } + + public void TryDeserializeAndProcess(TInboundMessage message, IErrorPolicy errorPolicy, Action messageHandler) + where TInboundMessage : IInboundMessage + { + var attempt = message.FailedAttempts + 1; + + while (true) + { + var result = HandleMessage(message, messageHandler, errorPolicy, attempt); + + if (result.IsSuccessful || result.Action == ErrorAction.Skip) + return; + + attempt++; + } + } + + private MessageHandlerResult HandleMessage(TInboundMessage message, Action messageHandler, IErrorPolicy errorPolicy, int attempt) + where TInboundMessage : IInboundMessage + { + try + { + message = (TInboundMessage)DeserializeIfNeeded(message); + + _messageLogger.LogProcessing(_logger, message); + + messageHandler(message); + + return MessageHandlerResult.Success; + } + catch (Exception ex) + { + _messageLogger.LogProcessingError(_logger, message, ex); + + if (errorPolicy == null) + throw; + + UpdateFailedAttemptsHeader(message, attempt); + + if (!errorPolicy.CanHandle(message, ex)) + throw; + + var action = errorPolicy.HandleError(message, ex); + + if (action == ErrorAction.StopConsuming) + throw; + + return MessageHandlerResult.Error(action); + } + } + + private static void UpdateFailedAttemptsHeader(IInboundMessage message, int attempt) + { + if (message is IInboundBatch batch) + batch.Messages.ForEach(m => UpdateFailedAttemptsHeader(m, attempt)); + else + message.Headers.AddOrReplace(MessageHeader.FailedAttemptsHeaderName, attempt.ToString()); + } + + private static IInboundMessage DeserializeIfNeeded(IInboundMessage message) + { + if (message is IInboundBatch batch) + return new InboundBatch( + batch.Id, + batch.Messages.Select(DeserializeIfNeeded), + batch.Endpoint); + + if (message.Message is byte[]) + return InboundMessageHelper.CreateNewInboundMessage( + Deserialize(message), + message); + + return message; + } + + private static object Deserialize(IInboundMessage message) => + message.Endpoint.Serializer.Deserialize((byte[])message.Message); + } +} diff --git a/src/Silverback.Integration/Messaging/ErrorHandling/MoveMessageErrorPolicy.cs b/src/Silverback.Integration/Messaging/ErrorHandling/MoveMessageErrorPolicy.cs index 0abe37ae5..81f6c96af 100644 --- a/src/Silverback.Integration/Messaging/ErrorHandling/MoveMessageErrorPolicy.cs +++ b/src/Silverback.Integration/Messaging/ErrorHandling/MoveMessageErrorPolicy.cs @@ -19,44 +19,53 @@ public class MoveMessageErrorPolicy : ErrorPolicyBase private readonly MessageLogger _messageLogger; private Func _transformationFunction; + private Func _headersTransformationFunction; - public MoveMessageErrorPolicy(IBroker broker, IEndpoint endpoint, ILogger logger, MessageLogger messageLogger) - : base(logger, messageLogger) + public MoveMessageErrorPolicy(IBroker broker, IEndpoint endpoint, IServiceProvider serviceProvider, ILogger logger, MessageLogger messageLogger) + : base(serviceProvider, logger, messageLogger) { + if (broker == null) throw new ArgumentNullException(nameof(broker)); + if (endpoint == null) throw new ArgumentNullException(nameof(endpoint)); + if (serviceProvider == null) throw new ArgumentNullException(nameof(serviceProvider)); + _producer = broker.GetProducer(endpoint); _endpoint = endpoint; _logger = logger; _messageLogger = messageLogger; } - public MoveMessageErrorPolicy Transform(Func transformationFunction) + public MoveMessageErrorPolicy Transform(Func transformationFunction, + Func headersTransformationFunction = null) { _transformationFunction = transformationFunction; + _headersTransformationFunction = headersTransformationFunction; return this; } - public override ErrorAction HandleError(FailedMessage failedMessage, Exception exception) + protected override ErrorAction ApplyPolicy(IInboundMessage message, Exception exception) { - if (failedMessage.Message is BatchCompleteEvent batchMessage) + _messageLogger.LogInformation(_logger, $"The message will be be moved to '{_endpoint.Name}'.", message); + + if (message is IInboundBatch inboundBatch) { - foreach (var singleFailedMessage in batchMessage.Messages) + foreach (var singleFailedMessage in inboundBatch.Messages) { PublishToNewEndpoint(singleFailedMessage, exception); } } else { - PublishToNewEndpoint(failedMessage, exception); + PublishToNewEndpoint(message, exception); } - _messageLogger.LogTrace(_logger, "The failed message has been moved and will be skipped.", failedMessage, _endpoint); - return ErrorAction.Skip; } - private void PublishToNewEndpoint(object failedMessage, Exception exception) + private void PublishToNewEndpoint(IInboundMessage message, Exception exception) { - _producer.Produce(_transformationFunction?.Invoke(failedMessage, exception) ?? failedMessage); + _producer.Produce( + _transformationFunction?.Invoke(message.Message, exception) ?? message.Message, + _headersTransformationFunction?.Invoke(message.Headers, exception) ?? message.Headers); } } } \ No newline at end of file diff --git a/src/Silverback.Integration/Messaging/ErrorHandling/RetryErrorPolicy.cs b/src/Silverback.Integration/Messaging/ErrorHandling/RetryErrorPolicy.cs index fd5964df4..7f1682415 100644 --- a/src/Silverback.Integration/Messaging/ErrorHandling/RetryErrorPolicy.cs +++ b/src/Silverback.Integration/Messaging/ErrorHandling/RetryErrorPolicy.cs @@ -20,8 +20,8 @@ public class RetryErrorPolicy : ErrorPolicyBase private readonly ILogger _logger; private readonly MessageLogger _messageLogger; - public RetryErrorPolicy(ILogger logger, MessageLogger messageLogger, TimeSpan? initialDelay = null, TimeSpan? delayIncrement = null) - : base(logger, messageLogger) + public RetryErrorPolicy(IServiceProvider serviceProvider, ILogger logger, MessageLogger messageLogger, TimeSpan? initialDelay = null, TimeSpan? delayIncrement = null) + : base(serviceProvider, logger, messageLogger) { _initialDelay = initialDelay ?? TimeSpan.Zero; _delayIncrement = delayIncrement ?? TimeSpan.Zero; @@ -29,23 +29,23 @@ public RetryErrorPolicy(ILogger logger, MessageLogger messageL _messageLogger = messageLogger; } - public override ErrorAction HandleError(FailedMessage failedMessage, Exception exception) + protected override ErrorAction ApplyPolicy(IInboundMessage message, Exception exception) { - ApplyDelay(failedMessage); + ApplyDelay(message); - _messageLogger.LogTrace(_logger, "The message will be processed again.", failedMessage); + _messageLogger.LogInformation(_logger, "The message will be processed again.", message); return ErrorAction.Retry; } - private void ApplyDelay(FailedMessage failedMessage) + private void ApplyDelay(IInboundMessage message) { - var delay = _initialDelay.Milliseconds + failedMessage.FailedAttempts * _delayIncrement.Milliseconds; + var delay = _initialDelay.Milliseconds + message.FailedAttempts * _delayIncrement.Milliseconds; if (delay <= 0) return; - _messageLogger.LogTrace(_logger, $"Waiting {delay} milliseconds before retrying the message.", failedMessage); + _messageLogger.LogTrace(_logger, $"Waiting {delay} milliseconds before retrying the message.", message); Thread.Sleep(delay); } } diff --git a/src/Silverback.Integration/Messaging/ErrorHandling/SkipMessageErrorPolicy.cs b/src/Silverback.Integration/Messaging/ErrorHandling/SkipMessageErrorPolicy.cs index 0b53fe8dc..be128f0ae 100644 --- a/src/Silverback.Integration/Messaging/ErrorHandling/SkipMessageErrorPolicy.cs +++ b/src/Silverback.Integration/Messaging/ErrorHandling/SkipMessageErrorPolicy.cs @@ -15,16 +15,16 @@ public class SkipMessageErrorPolicy : ErrorPolicyBase private readonly ILogger _logger; private readonly MessageLogger _messageLogger; - public SkipMessageErrorPolicy(ILogger logger, MessageLogger messageLogger) - : base(logger, messageLogger) + public SkipMessageErrorPolicy(IServiceProvider serviceProvider, ILogger logger, MessageLogger messageLogger) + : base(serviceProvider, logger, messageLogger) { _logger = logger; _messageLogger = messageLogger; } - public override ErrorAction HandleError(FailedMessage failedMessage, Exception exception) + protected override ErrorAction ApplyPolicy(IInboundMessage message, Exception exception) { - _messageLogger.LogTrace(_logger, "The message will be skipped.", failedMessage); + _messageLogger.LogWarning(_logger, exception, "The message will be skipped.", message); return ErrorAction.Skip; } diff --git a/src/Silverback.Integration/Messaging/IConsumerEndpoint.cs b/src/Silverback.Integration/Messaging/IConsumerEndpoint.cs index 15b08169e..033d622ca 100644 --- a/src/Silverback.Integration/Messaging/IConsumerEndpoint.cs +++ b/src/Silverback.Integration/Messaging/IConsumerEndpoint.cs @@ -1,5 +1,6 @@ // Copyright (c) 2018-2019 Sergio Aquilini // This code is licensed under MIT license (see LICENSE file for details) + namespace Silverback.Messaging { public interface IConsumerEndpoint : IEndpoint diff --git a/src/Silverback.Integration/Messaging/LargeMessages/IOffloadStoreCleaner.cs b/src/Silverback.Integration/Messaging/LargeMessages/IOffloadStoreCleaner.cs index 4ad244d0d..533a789ad 100644 --- a/src/Silverback.Integration/Messaging/LargeMessages/IOffloadStoreCleaner.cs +++ b/src/Silverback.Integration/Messaging/LargeMessages/IOffloadStoreCleaner.cs @@ -1,5 +1,6 @@ // Copyright (c) 2018-2019 Sergio Aquilini // This code is licensed under MIT license (see LICENSE file for details) + namespace Silverback.Messaging.LargeMessages { public interface IOffloadStoreCleaner diff --git a/src/Silverback.Integration/Messaging/LargeMessages/IOffloadStoreReader.cs b/src/Silverback.Integration/Messaging/LargeMessages/IOffloadStoreReader.cs index 44661a5d5..c313f3354 100644 --- a/src/Silverback.Integration/Messaging/LargeMessages/IOffloadStoreReader.cs +++ b/src/Silverback.Integration/Messaging/LargeMessages/IOffloadStoreReader.cs @@ -1,5 +1,6 @@ // Copyright (c) 2018-2019 Sergio Aquilini // This code is licensed under MIT license (see LICENSE file for details) + namespace Silverback.Messaging.LargeMessages { public interface IOffloadStoreReader diff --git a/src/Silverback.Integration/Messaging/Messages/FailedMessage.cs b/src/Silverback.Integration/Messaging/Messages/FailedMessage.cs index 618cc6ed4..444b49200 100644 --- a/src/Silverback.Integration/Messaging/Messages/FailedMessage.cs +++ b/src/Silverback.Integration/Messaging/Messages/FailedMessage.cs @@ -1,25 +1,25 @@ // Copyright (c) 2018-2019 Sergio Aquilini // This code is licensed under MIT license (see LICENSE file for details) -using System; +// TODO: DELETE, replaced by InboundMessage namespace Silverback.Messaging.Messages { - public class FailedMessage - { - public FailedMessage() - { } + //public class FailedMessage + //{ + // public FailedMessage() + // { } - public FailedMessage(object message, int failedAttempts = 1) - { - if (failedAttempts < 1) throw new ArgumentOutOfRangeException(nameof(failedAttempts), failedAttempts, "failedAttempts must be >= 1"); + // public FailedMessage(object message, int failedAttempts = 1) + // { + // if (failedAttempts < 1) throw new ArgumentOutOfRangeException(nameof(failedAttempts), failedAttempts, "failedAttempts must be >= 1"); - Message = message; - FailedAttempts = failedAttempts; - } + // Message = message; + // FailedAttempts = failedAttempts; + // } - public object Message { get; set; } + // public object Message { get; set; } - public int FailedAttempts { get; set; } - } + // public int FailedAttempts { get; set; } + //} } \ No newline at end of file diff --git a/src/Silverback.Integration/Messaging/Messages/IInboundBatch.cs b/src/Silverback.Integration/Messaging/Messages/IInboundBatch.cs new file mode 100644 index 000000000..be508cd3f --- /dev/null +++ b/src/Silverback.Integration/Messaging/Messages/IInboundBatch.cs @@ -0,0 +1,18 @@ +// Copyright (c) 2018-2019 Sergio Aquilini +// This code is licensed under MIT license (see LICENSE file for details) + +using System; +using System.Collections.Generic; + +namespace Silverback.Messaging.Messages +{ + internal interface IInboundBatch + { + Guid Id { get; } + + IEnumerable Messages { get; } + + int Size { get; } + IEndpoint Endpoint { get; } + } +} \ No newline at end of file diff --git a/src/Silverback.Integration/Messaging/Messages/IInboundMessage.cs b/src/Silverback.Integration/Messaging/Messages/IInboundMessage.cs index 63910819d..d97d60361 100644 --- a/src/Silverback.Integration/Messaging/Messages/IInboundMessage.cs +++ b/src/Silverback.Integration/Messaging/Messages/IInboundMessage.cs @@ -1,18 +1,50 @@ // Copyright (c) 2018-2019 Sergio Aquilini // This code is licensed under MIT license (see LICENSE file for details) + +using Silverback.Messaging.Broker; + namespace Silverback.Messaging.Messages { public interface IInboundMessage { - IEndpoint Endpoint { get; set; } + /// + /// Gets the message. + /// + object Message { get; } + /// + /// Gets the optional message headers. + /// MessageHeaderCollection Headers { get; } - object Message { get; set; } + /// + /// Gets the message offset (or similar construct if using a message broker other than Kafka). + /// + IOffset Offset { get; } + + /// + /// Gets the source endpoint. + /// + IEndpoint Endpoint { get; } + + /// + /// Gets the number of failed processing attempt for this message. + /// + int FailedAttempts { get; } + + /// + /// Gets a boolean value indicating whether the contained Message must be extracted and + /// published to the internal bus. (This is true, unless specifically configured otherwise + /// to handle some special cases.) + /// + bool MustUnwrap { get; } } public interface IInboundMessage : IInboundMessage { + /// + /// Gets the deserialized message. + /// new TMessage Message { get; } } } \ No newline at end of file diff --git a/src/Silverback.Integration/Messaging/Messages/IMessageKeyProvider.cs b/src/Silverback.Integration/Messaging/Messages/IMessageKeyProvider.cs index 4eb252ee3..0e07ec217 100644 --- a/src/Silverback.Integration/Messaging/Messages/IMessageKeyProvider.cs +++ b/src/Silverback.Integration/Messaging/Messages/IMessageKeyProvider.cs @@ -1,5 +1,6 @@ // Copyright (c) 2018-2019 Sergio Aquilini // This code is licensed under MIT license (see LICENSE file for details) + namespace Silverback.Messaging.Messages { public interface IMessageKeyProvider diff --git a/src/Silverback.Integration/Messaging/Messages/IOutboundMessage.cs b/src/Silverback.Integration/Messaging/Messages/IOutboundMessage.cs index 0e978eeae..7b61e943b 100644 --- a/src/Silverback.Integration/Messaging/Messages/IOutboundMessage.cs +++ b/src/Silverback.Integration/Messaging/Messages/IOutboundMessage.cs @@ -1,13 +1,23 @@ // Copyright (c) 2018-2019 Sergio Aquilini // This code is licensed under MIT license (see LICENSE file for details) + namespace Silverback.Messaging.Messages { public interface IOutboundMessage { + /// + /// Gets the destination endpoint. + /// IEndpoint Endpoint { get; set; } + /// + /// Gets the optional message headers. + /// MessageHeaderCollection Headers { get; } + /// + /// Gets the message. + /// object Message { get; set; } } diff --git a/src/Silverback.Integration/Messaging/Messages/InboundBatch.cs b/src/Silverback.Integration/Messaging/Messages/InboundBatch.cs new file mode 100644 index 000000000..da193a8a3 --- /dev/null +++ b/src/Silverback.Integration/Messaging/Messages/InboundBatch.cs @@ -0,0 +1,42 @@ +// Copyright (c) 2018-2019 Sergio Aquilini +// This code is licensed under MIT license (see LICENSE file for details) + +using System; +using System.Collections.Generic; +using System.Linq; +using Silverback.Messaging.Broker; + +namespace Silverback.Messaging.Messages +{ + /// + /// Used only as a wrapper to reuse the error policies and logging logic. + /// + internal class InboundBatch : IInboundBatch, IInboundMessage + { + public InboundBatch(Guid batchId, IEnumerable messages, IEndpoint endpoint) + { + Id = batchId; + Messages = messages ?? throw new ArgumentNullException(nameof(messages)); + Endpoint = endpoint ?? throw new ArgumentNullException(nameof(endpoint)); + Size = messages.Count(); + } + + public Guid Id { get; } + + public IEnumerable Messages { get; } + + public int Size { get; } + + public IEndpoint Endpoint { get; } + + public int FailedAttempts => Messages.Min(m => m.FailedAttempts); + + public object Message => null; + + public MessageHeaderCollection Headers => null; + + public IOffset Offset => null; + + public bool MustUnwrap => true; + } +} \ No newline at end of file diff --git a/src/Silverback.Integration/Messaging/Messages/InboundMessage.cs b/src/Silverback.Integration/Messaging/Messages/InboundMessage.cs index 8b4882bb6..6b2034567 100644 --- a/src/Silverback.Integration/Messaging/Messages/InboundMessage.cs +++ b/src/Silverback.Integration/Messaging/Messages/InboundMessage.cs @@ -1,19 +1,41 @@ // Copyright (c) 2018-2019 Sergio Aquilini // This code is licensed under MIT license (see LICENSE file for details) + +using Silverback.Messaging.Broker; + namespace Silverback.Messaging.Messages { - public class InboundMessage : IInboundMessage + internal class InboundMessage : IInboundMessage { + public object Message { get; set; } + + public MessageHeaderCollection Headers { get; } = new MessageHeaderCollection(); + + public IOffset Offset { get; set; } + public IEndpoint Endpoint { get; set; } - public MessageHeaderCollection Headers { get; set; } = new MessageHeaderCollection(); + public int FailedAttempts + { + get => Headers.GetValue(MessageHeader.FailedAttemptsHeaderName); + set + { + Headers.Remove(MessageHeader.FailedAttemptsHeaderName); - public TMessage Message { get; set; } + if (value > 0) + Headers.Add(MessageHeader.FailedAttemptsHeaderName, value.ToString()); + } + } - object IInboundMessage.Message + public bool MustUnwrap { get; set; } + } + + internal class InboundMessage : InboundMessage, IInboundMessage + { + public new TMessage Message { - get => Message; - set => Message = (TMessage)value; + get => (TMessage) base.Message; + set => base.Message = value; } } } diff --git a/src/Silverback.Integration/Messaging/Messages/InboundMessageHelper.cs b/src/Silverback.Integration/Messaging/Messages/InboundMessageHelper.cs new file mode 100644 index 000000000..66926ff97 --- /dev/null +++ b/src/Silverback.Integration/Messaging/Messages/InboundMessageHelper.cs @@ -0,0 +1,30 @@ +// Copyright (c) 2018-2019 Sergio Aquilini +// This code is licensed under MIT license (see LICENSE file for details) + +using System; + +namespace Silverback.Messaging.Messages +{ + internal static class InboundMessageHelper + { + public static IInboundMessage CreateNewInboundMessage(object message, IInboundMessage sourceInboundMessage) => + CreateNewInboundMessage(message, sourceInboundMessage); + + public static IInboundMessage CreateNewInboundMessage(TMessage message, + IInboundMessage sourceInboundMessage) + { + var newMessage = (InboundMessage) Activator.CreateInstance(typeof(InboundMessage<>).MakeGenericType(message.GetType())); + + if (sourceInboundMessage.Headers != null) + newMessage.Headers.AddRange(sourceInboundMessage.Headers); + + newMessage.Message = message; + newMessage.Endpoint = sourceInboundMessage.Endpoint; + newMessage.Offset = sourceInboundMessage.Offset; + newMessage.FailedAttempts = sourceInboundMessage.FailedAttempts; + newMessage.MustUnwrap = sourceInboundMessage.MustUnwrap; + + return (IInboundMessage) newMessage; + } + } +} \ No newline at end of file diff --git a/src/Silverback.Integration/Messaging/Messages/MessageHeader.cs b/src/Silverback.Integration/Messaging/Messages/MessageHeader.cs index dadc76b6e..0a204c0bf 100644 --- a/src/Silverback.Integration/Messaging/Messages/MessageHeader.cs +++ b/src/Silverback.Integration/Messaging/Messages/MessageHeader.cs @@ -7,6 +7,8 @@ namespace Silverback.Messaging.Messages { public class MessageHeader { + public const string FailedAttemptsHeaderName = "Silverback.FailedAttempts"; + public MessageHeader() { } diff --git a/src/Silverback.Integration/Messaging/Messages/MessageHeaderCollection.cs b/src/Silverback.Integration/Messaging/Messages/MessageHeaderCollection.cs index c84a12ac1..7a1800cc4 100644 --- a/src/Silverback.Integration/Messaging/Messages/MessageHeaderCollection.cs +++ b/src/Silverback.Integration/Messaging/Messages/MessageHeaderCollection.cs @@ -9,5 +9,14 @@ public class MessageHeaderCollection : List { public void Add(string key, string value) => Add(new MessageHeader {Key = key, Value = value}); + + public void Remove(string key) => + RemoveAll(x => x.Key == key); + + public void AddOrReplace(string key, string newValue) + { + Remove(key); + Add(key, newValue); + } } } \ No newline at end of file diff --git a/src/Silverback.Integration/Messaging/Messages/MessageHeaderCollectionExtensions.cs b/src/Silverback.Integration/Messaging/Messages/MessageHeaderCollectionExtensions.cs new file mode 100644 index 000000000..a89587bce --- /dev/null +++ b/src/Silverback.Integration/Messaging/Messages/MessageHeaderCollectionExtensions.cs @@ -0,0 +1,29 @@ +// Copyright (c) 2018-2019 Sergio Aquilini +// This code is licensed under MIT license (see LICENSE file for details) + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Silverback.Messaging.Messages +{ + public static class MessageHeaderCollectionExtensions + { + public static T GetValue(this IEnumerable headers, string headerName) + { + var value = headers.FirstOrDefault(h => h.Key == MessageHeader.FailedAttemptsHeaderName)?.Value; + + if (value == null) + return default; + + try + { + return (T) Convert.ChangeType(value, typeof(T)); + } + catch + { + return default; + } + } + } +} \ No newline at end of file diff --git a/src/Silverback.Integration/Messaging/Messages/MessageLogger.cs b/src/Silverback.Integration/Messaging/Messages/MessageLogger.cs index 9c0c6f161..4cfb475d8 100644 --- a/src/Silverback.Integration/Messaging/Messages/MessageLogger.cs +++ b/src/Silverback.Integration/Messaging/Messages/MessageLogger.cs @@ -5,12 +5,11 @@ using System.Collections.Generic; using System.Linq; using Microsoft.Extensions.Logging; -using Silverback.Messaging.Batch; using Silverback.Messaging.Broker; namespace Silverback.Messaging.Messages { - // TODO: Review and test + // TODO: Test public class MessageLogger { private readonly MessageKeyProvider _messageKeyProvider; @@ -20,56 +19,107 @@ public MessageLogger(MessageKeyProvider messageKeyProvider) _messageKeyProvider = messageKeyProvider; } - public void LogTrace(ILogger logger, string logMessage, object message, IEndpoint endpoint = null, MessageBatch batch = null, IOffset offset = null) => - Log(logger, LogLevel.Trace, null, logMessage, message, endpoint, batch, offset); + #region Generic - public void LogWarning(ILogger logger, Exception exception, string logMessage, object message, IEndpoint endpoint = null, MessageBatch batch = null, IOffset offset = null) => - Log(logger, LogLevel.Warning, exception, logMessage, message, endpoint, batch, offset); + public void LogTrace(ILogger logger, string logMessage, object message, IEndpoint endpoint = null, IOffset offset = null, Guid? batchId = null, int? batchSize = null) => + Log(logger, LogLevel.Trace, null, logMessage, message, endpoint, offset, null, batchId, batchSize); - public void LogError(ILogger logger, Exception exception, string logMessage, object message, IEndpoint endpoint = null, MessageBatch batch = null, IOffset offset = null) => - Log(logger, LogLevel.Error, exception, logMessage, message, endpoint, batch, offset); + public void LogTrace(ILogger logger, string logMessage, IInboundMessage message, Guid? batchId = null, int? batchSize = null) => + Log(logger, LogLevel.Trace, null, logMessage, message, batchId, batchSize); - public void LogCritical(ILogger logger, Exception exception, string logMessage, object message, IEndpoint endpoint = null, MessageBatch batch = null, IOffset offset = null) => - Log(logger, LogLevel.Critical, exception, logMessage, message, endpoint, batch, offset); + public void LogInformation(ILogger logger, string logMessage, object message, IEndpoint endpoint = null, IOffset offset = null, Guid? batchId = null, int? batchSize = null) => + Log(logger, LogLevel.Information, null, logMessage, message, endpoint, offset, null, batchId, batchSize); - public void Log(ILogger logger, LogLevel logLevel, Exception exception, string logMessage, object message, IEndpoint endpoint = null, MessageBatch batch = null, IOffset offset = null) - { - var failedMessage = message as FailedMessage; + public void LogInformation(ILogger logger, string logMessage, IInboundMessage message, Guid? batchId = null, int? batchSize = null) => + Log(logger, LogLevel.Information, null, logMessage, message, batchId, batchSize); - var innerMessage = failedMessage?.Message ?? message; + public void LogWarning(ILogger logger, Exception exception, string logMessage, object message, IEndpoint endpoint = null, IOffset offset = null, Guid? batchId = null, int? batchSize = null) => + Log(logger, LogLevel.Warning, exception, logMessage, message, endpoint, offset, null, batchId, batchSize); - var properties = new List<(string, string, object)>(); + public void LogWarning(ILogger logger, Exception exception, string logMessage, IInboundMessage message, Guid? batchId = null, int? batchSize = null) => + Log(logger, LogLevel.Warning, exception, logMessage, message, batchId, batchSize); + + public void LogError(ILogger logger, Exception exception, string logMessage, object message, IEndpoint endpoint = null, IOffset offset = null, Guid? batchId = null, int? batchSize = null) => + Log(logger, LogLevel.Error, exception, logMessage, message, endpoint, offset, null, batchId, batchSize); - var key = _messageKeyProvider.GetKey(message, false); - if (key != null) - properties.Add(("id", "messageId", key)); + public void LogError(ILogger logger, string logMessage, IInboundMessage message, Guid? batchId = null, int? batchSize = null) => + Log(logger, LogLevel.Error, null, logMessage, message, batchId, batchSize); + public void LogCritical(ILogger logger, Exception exception, string logMessage, object message, IEndpoint endpoint = null, IOffset offset = null, Guid? batchId = null, int? batchSize = null) => + Log(logger, LogLevel.Critical, exception, logMessage, message, endpoint, offset, null, batchId, batchSize); + + public void LogCritical(ILogger logger, string logMessage, IInboundMessage message, Guid? batchId = null, int? batchSize = null) => + Log(logger, LogLevel.Critical, null, logMessage, message, batchId, batchSize); + + private void Log(ILogger logger, LogLevel logLevel, Exception exception, string logMessage, IInboundMessage message, Guid? batchId, int? batchSize) => + Log(logger, logLevel, exception, logMessage, message.Message, message.Endpoint, message.Offset, message.FailedAttempts, batchId, batchSize); + + private void Log(ILogger logger, LogLevel logLevel, Exception exception, string logMessage, object message, + IEndpoint endpoint, IOffset offset, int? failedAttempts, Guid? batchId, int? batchSize) + { + var properties = new List<(string, string, object)>(); + if (offset != null) properties.Add(("offset", "offset", $"{offset.Key}@{offset.Value}")); - + if (endpoint != null) properties.Add(("endpoint", "endpointName", endpoint.Name)); - properties.Add(("type", "messageType", innerMessage.GetType().Name)); - - if (batch != null) + if (message != null && !(message is byte[])) { - properties.Add(("batchId", "batchId", batch.CurrentBatchId)); - properties.Add(("batchSize", "batchSize", batch.CurrentSize)); - } - else if (message is BatchEvent batchMessage) - { - properties.Add(("batchId", "batchId", batchMessage.BatchId)); - properties.Add(("batchSize", "batchSize", batchMessage.BatchSize)); + properties.Add(("type", "messageType", message.GetType().Name)); + + var key = _messageKeyProvider.GetKey(message, false); + if (key != null) + properties.Add(("id", "messageId", key)); } - if (failedMessage != null) - properties.Add(("failedAttempts", "failedAttempts", failedMessage.FailedAttempts)); + if (failedAttempts != null && failedAttempts > 0) + properties.Add(("failedAttempts", "failedAttempts", failedAttempts)); - logger.Log( - logLevel, exception, - logMessage + " {{" + string.Join(", ", properties.Select(p => $"{p.Item1}={{{p.Item2}}}")) + "}}", - properties.Select(p => p.Item3).ToArray()); + if (batchId != null) + properties.Add(("batchId", "batchId", batchId)); + + if (batchSize != null) + properties.Add(("batchSize", "batchSize", batchSize)); + + logger.Log( + logLevel, exception, + logMessage + " {{" + string.Join(", ", properties.Select(p => $"{p.Item1}={{{p.Item2}}}")) + "}}", + properties.Select(p => p.Item3).ToArray()); } + + #endregion + + #region Specific + + public void LogProcessing(ILogger logger, IInboundMessage message) + { + var batch = message as IInboundBatch; + + LogInformation(logger, + batch != null + ? "Processing inbound batch." + : "Processing inbound message.", + message, + batch?.Id, + batch?.Size); + } + + public void LogProcessingError(ILogger logger, IInboundMessage message, Exception exception) + { + var batch = message as IInboundBatch; + + LogWarning(logger, + exception, + batch != null + ? "Error occurred processing the inbound batch." + : "Error occurred processing the inbound message.", + message, + batch?.Id, + batch?.Size); + } + + #endregion } } diff --git a/src/Silverback.Integration/Messaging/Messages/OutboundMessage.cs b/src/Silverback.Integration/Messaging/Messages/OutboundMessage.cs index d1b6f5a76..49c9a4b42 100644 --- a/src/Silverback.Integration/Messaging/Messages/OutboundMessage.cs +++ b/src/Silverback.Integration/Messaging/Messages/OutboundMessage.cs @@ -5,11 +5,11 @@ namespace Silverback.Messaging.Messages { - public class OutboundMessage : IOutboundMessage, IOutboundMessageInternal + internal class OutboundMessage : IOutboundMessage, IOutboundMessageInternal { public IEndpoint Endpoint { get; set; } - public MessageHeaderCollection Headers { get; set; } = new MessageHeaderCollection(); + public MessageHeaderCollection Headers { get; } = new MessageHeaderCollection(); public TMessage Message { get; set; } diff --git a/src/Silverback.Integration/Messaging/Serialization/IMessageSerializer.cs b/src/Silverback.Integration/Messaging/Serialization/IMessageSerializer.cs index 777bd27ef..6b5ff9bdc 100644 --- a/src/Silverback.Integration/Messaging/Serialization/IMessageSerializer.cs +++ b/src/Silverback.Integration/Messaging/Serialization/IMessageSerializer.cs @@ -1,5 +1,6 @@ // Copyright (c) 2018-2019 Sergio Aquilini // This code is licensed under MIT license (see LICENSE file for details) + namespace Silverback.Messaging.Serialization { /// diff --git a/src/Silverback.Integration/Messaging/Serialization/JsonMessageSerializer.cs b/src/Silverback.Integration/Messaging/Serialization/JsonMessageSerializer.cs index 4b1e8ddb7..2358cde73 100644 --- a/src/Silverback.Integration/Messaging/Serialization/JsonMessageSerializer.cs +++ b/src/Silverback.Integration/Messaging/Serialization/JsonMessageSerializer.cs @@ -24,6 +24,9 @@ public byte[] Serialize(object message) { if (message == null) throw new ArgumentNullException(nameof(message)); + if (message is byte[] bytes) + return bytes; + var json = JsonConvert.SerializeObject(message, typeof(TMessage), Settings); return GetEncoding().GetBytes(json); diff --git a/src/Silverback.Integration/Messaging/Serialization/MessageEncoding.cs b/src/Silverback.Integration/Messaging/Serialization/MessageEncoding.cs index 0d00f510f..32df36d6c 100644 --- a/src/Silverback.Integration/Messaging/Serialization/MessageEncoding.cs +++ b/src/Silverback.Integration/Messaging/Serialization/MessageEncoding.cs @@ -1,5 +1,6 @@ // Copyright (c) 2018-2019 Sergio Aquilini // This code is licensed under MIT license (see LICENSE file for details) + namespace Silverback.Messaging.Serialization { public enum MessageEncoding diff --git a/src/Silverback.Integration/Silverback.Integration.csproj b/src/Silverback.Integration/Silverback.Integration.csproj index deef671c4..61f07ffe5 100644 --- a/src/Silverback.Integration/Silverback.Integration.csproj +++ b/src/Silverback.Integration/Silverback.Integration.csproj @@ -1,4 +1,4 @@ - + netstandard2.0 @@ -7,20 +7,19 @@ BEagle1984 Silverback is a simple framework to build reactive, event-driven, microservices. This package contains the message broker and connectors abstractions. - https://github.com/BEagle1984/silverback/blob/master/LICENSE https://github.com/BEagle1984/silverback/ - 1.0.0.0 - 0.6.1.1 - True - https://github.com/BEagle1984/silverback/raw/develop/graphics/Icon.png + 0.10.0.0 + True + https://github.com/BEagle1984/silverback/raw/develop/graphics/Icon.png + latest + 1701;1702;CS1591 + MIT bin\Debug\netstandard2.0\Silverback.Integration.xml - latest - 1701;1702;CS1591 @@ -30,7 +29,7 @@ - + diff --git a/tests/Silverback.Core.EntityFrameworkCore.Tests/Silverback.Core.EntityFrameworkCore.Tests.csproj b/tests/Silverback.Core.EntityFrameworkCore.Tests/Silverback.Core.EntityFrameworkCore.Tests.csproj index bd8a1fc8c..290e4d32e 100644 --- a/tests/Silverback.Core.EntityFrameworkCore.Tests/Silverback.Core.EntityFrameworkCore.Tests.csproj +++ b/tests/Silverback.Core.EntityFrameworkCore.Tests/Silverback.Core.EntityFrameworkCore.Tests.csproj @@ -1,19 +1,21 @@ - + - netcoreapp2.1 + netcoreapp2.2 Silverback.Tests.Core.EntityFrameworkCore - - - latest - - - - + + + + + all + runtime; build; native; contentfiles; analyzers + + + all diff --git a/tests/Silverback.Core.EntityFrameworkCore.Tests/TestTypes/Base/Domain/DomainEntity.cs b/tests/Silverback.Core.EntityFrameworkCore.Tests/TestTypes/Base/Domain/DomainEntity.cs index 75232f833..3085f81d0 100644 --- a/tests/Silverback.Core.EntityFrameworkCore.Tests/TestTypes/Base/Domain/DomainEntity.cs +++ b/tests/Silverback.Core.EntityFrameworkCore.Tests/TestTypes/Base/Domain/DomainEntity.cs @@ -2,39 +2,37 @@ // This code is licensed under MIT license (see LICENSE file for details) using System.Collections.Generic; -using System.ComponentModel.DataAnnotations.Schema; -using System.Linq; +using Silverback.Messaging.Messages; namespace Silverback.Tests.Core.EntityFrameworkCore.TestTypes.Base.Domain { - /// - /// A sample implementation of . - /// - public abstract class DomainEntity : IDomainEntity + public abstract class DomainEntity : IMessagesSource { - private List> _domainEvents; + private List _domainEvents; - [NotMapped] - public IEnumerable> DomainEvents => - _domainEvents?.AsReadOnly() ?? Enumerable.Empty>(); + #region IMessagesSource - public void ClearEvents() => _domainEvents?.Clear(); + public IEnumerable GetMessages() => _domainEvents; - protected void AddEvent(IDomainEvent domainEvent) + public void ClearMessages() => _domainEvents.Clear(); + + #endregion + + protected void AddEvent(IDomainEvent domainEvent) { - _domainEvents = _domainEvents ?? new List>(); + _domainEvents = _domainEvents ?? new List(); - ((IDomainEvent)domainEvent).Source = this; + domainEvent.Source = this; _domainEvents.Add(domainEvent); } protected TEvent AddEvent() - where TEvent : IDomainEvent, new() + where TEvent : IDomainEvent, new() { - var evnt = new TEvent(); - AddEvent(evnt); - return evnt; + var @event = new TEvent(); + AddEvent(@event); + return @event; } protected void RemoveEvent(IDomainEvent domainEvent) => _domainEvents?.Remove(domainEvent); diff --git a/tests/Silverback.Core.EntityFrameworkCore.Tests/TestTypes/Base/Domain/DomainEntityEventsAccessor.cs b/tests/Silverback.Core.EntityFrameworkCore.Tests/TestTypes/Base/Domain/DomainEntityEventsAccessor.cs deleted file mode 100644 index 1605c8c79..000000000 --- a/tests/Silverback.Core.EntityFrameworkCore.Tests/TestTypes/Base/Domain/DomainEntityEventsAccessor.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) 2018-2019 Sergio Aquilini -// This code is licensed under MIT license (see LICENSE file for details) - -using System; -using System.Collections.Generic; - -namespace Silverback.Tests.Core.EntityFrameworkCore.TestTypes.Base.Domain -{ - public static class DomainEntityEventsAccessor - { - public static Func> EventsSelector = e => e.DomainEvents; - - public static Action ClearEventsAction = e => e.ClearEvents(); - } -} diff --git a/tests/Silverback.Core.EntityFrameworkCore.Tests/TestTypes/Base/Domain/DomainEvent.cs b/tests/Silverback.Core.EntityFrameworkCore.Tests/TestTypes/Base/Domain/DomainEvent.cs index cedcca059..d3d456acd 100644 --- a/tests/Silverback.Core.EntityFrameworkCore.Tests/TestTypes/Base/Domain/DomainEvent.cs +++ b/tests/Silverback.Core.EntityFrameworkCore.Tests/TestTypes/Base/Domain/DomainEvent.cs @@ -1,9 +1,9 @@ // Copyright (c) 2018-2019 Sergio Aquilini // This code is licensed under MIT license (see LICENSE file for details) + namespace Silverback.Tests.Core.EntityFrameworkCore.TestTypes.Base.Domain { public abstract class DomainEvent : IDomainEvent - where TEntity : IDomainEntity { public TEntity Source { get; set; } @@ -16,7 +16,7 @@ protected DomainEvent() { } - IDomainEntity IDomainEvent.Source + object IDomainEvent.Source { get => Source; set => Source = (TEntity) value; diff --git a/tests/Silverback.Core.EntityFrameworkCore.Tests/TestTypes/Base/Domain/IAggregateRoot.cs b/tests/Silverback.Core.EntityFrameworkCore.Tests/TestTypes/Base/Domain/IAggregateRoot.cs deleted file mode 100644 index 0799e7c59..000000000 --- a/tests/Silverback.Core.EntityFrameworkCore.Tests/TestTypes/Base/Domain/IAggregateRoot.cs +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) 2018-2019 Sergio Aquilini -// This code is licensed under MIT license (see LICENSE file for details) -namespace Silverback.Tests.Core.EntityFrameworkCore.TestTypes.Base.Domain -{ - /// - /// This empty interface has no other purpose than help recognizing the aggregate root. - /// - public interface IAggregateRoot - { - } -} \ No newline at end of file diff --git a/tests/Silverback.Core.EntityFrameworkCore.Tests/TestTypes/Base/Domain/IDomainEntity.cs b/tests/Silverback.Core.EntityFrameworkCore.Tests/TestTypes/Base/Domain/IDomainEntity.cs deleted file mode 100644 index 9f5ce1198..000000000 --- a/tests/Silverback.Core.EntityFrameworkCore.Tests/TestTypes/Base/Domain/IDomainEntity.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) 2018-2019 Sergio Aquilini -// This code is licensed under MIT license (see LICENSE file for details) - -using System.Collections.Generic; - -namespace Silverback.Tests.Core.EntityFrameworkCore.TestTypes.Base.Domain -{ - /// - /// Exposes the methods to retrieve the collection related to - /// an entity. - /// See for a sample implementation of this interface. - /// - public interface IDomainEntity - { - IEnumerable> DomainEvents { get; } - - void ClearEvents(); - } -} \ No newline at end of file diff --git a/tests/Silverback.Core.EntityFrameworkCore.Tests/TestTypes/Base/Domain/IDomainEvent.cs b/tests/Silverback.Core.EntityFrameworkCore.Tests/TestTypes/Base/Domain/IDomainEvent.cs index effe79ae8..4678c61fa 100644 --- a/tests/Silverback.Core.EntityFrameworkCore.Tests/TestTypes/Base/Domain/IDomainEvent.cs +++ b/tests/Silverback.Core.EntityFrameworkCore.Tests/TestTypes/Base/Domain/IDomainEvent.cs @@ -1,14 +1,14 @@ // Copyright (c) 2018-2019 Sergio Aquilini // This code is licensed under MIT license (see LICENSE file for details) + namespace Silverback.Tests.Core.EntityFrameworkCore.TestTypes.Base.Domain { public interface IDomainEvent : IEvent { - IDomainEntity Source { get; set; } + object Source { get; set; } } public interface IDomainEvent : IDomainEvent - where TEntity : IDomainEntity { new TEntity Source { get; } } diff --git a/tests/Silverback.Core.EntityFrameworkCore.Tests/TestTypes/Base/IQuery.cs b/tests/Silverback.Core.EntityFrameworkCore.Tests/TestTypes/Base/IQuery.cs index eb2cd3e13..29657c327 100644 --- a/tests/Silverback.Core.EntityFrameworkCore.Tests/TestTypes/Base/IQuery.cs +++ b/tests/Silverback.Core.EntityFrameworkCore.Tests/TestTypes/Base/IQuery.cs @@ -1,5 +1,6 @@ // Copyright (c) 2018-2019 Sergio Aquilini // This code is licensed under MIT license (see LICENSE file for details) + namespace Silverback.Tests.Core.EntityFrameworkCore.TestTypes.Base { public interface IQuery : IRequest diff --git a/tests/Silverback.Core.EntityFrameworkCore.Tests/TestTypes/TestAggregateRoot.cs b/tests/Silverback.Core.EntityFrameworkCore.Tests/TestTypes/TestAggregateRoot.cs index 81321d8fc..950e426d7 100644 --- a/tests/Silverback.Core.EntityFrameworkCore.Tests/TestTypes/TestAggregateRoot.cs +++ b/tests/Silverback.Core.EntityFrameworkCore.Tests/TestTypes/TestAggregateRoot.cs @@ -6,16 +6,16 @@ namespace Silverback.Tests.Core.EntityFrameworkCore.TestTypes { - public class TestAggregateRoot : DomainEntity, IAggregateRoot + public class TestAggregateRoot : DomainEntity { [Key] public int Id { get; set; } - public new void AddEvent(IDomainEvent domainEvent) + public new void AddEvent(IDomainEvent domainEvent) => base.AddEvent(domainEvent); public new TEvent AddEvent() - where TEvent : IDomainEvent, new() + where TEvent : IDomainEvent, new() => base.AddEvent(); } } diff --git a/tests/Silverback.Core.EntityFrameworkCore.Tests/TestTypes/TestDbContext.cs b/tests/Silverback.Core.EntityFrameworkCore.Tests/TestTypes/TestDbContext.cs index 326515f99..b98ca84d1 100644 --- a/tests/Silverback.Core.EntityFrameworkCore.Tests/TestTypes/TestDbContext.cs +++ b/tests/Silverback.Core.EntityFrameworkCore.Tests/TestTypes/TestDbContext.cs @@ -6,34 +6,24 @@ using Microsoft.EntityFrameworkCore; using Silverback.EntityFrameworkCore; using Silverback.Messaging.Publishing; -using Silverback.Tests.Core.EntityFrameworkCore.TestTypes.Base.Domain; namespace Silverback.Tests.Core.EntityFrameworkCore.TestTypes { public class TestDbContext : DbContext { - private DbContextEventsPublisher _eventsPublisher; + private readonly DbContextEventsPublisher _eventsPublisher; public DbSet TestAggregates { get; set; } public TestDbContext(IPublisher publisher) { - InitEventsPublisher(publisher); + _eventsPublisher = new DbContextEventsPublisher(publisher, this); } public TestDbContext(DbContextOptions options, IPublisher publisher) : base(options) { - InitEventsPublisher(publisher); - } - - private void InitEventsPublisher(IPublisher publisher) - { - _eventsPublisher = new DbContextEventsPublisher( - DomainEntityEventsAccessor.EventsSelector, - DomainEntityEventsAccessor.ClearEventsAction, - publisher, - this); + _eventsPublisher = new DbContextEventsPublisher(publisher, this); } public override int SaveChanges() diff --git a/tests/Silverback.Core.Model.Tests/Domain/EntityTests.cs b/tests/Silverback.Core.Model.Tests/Domain/DomainEntityTests.cs similarity index 69% rename from tests/Silverback.Core.Model.Tests/Domain/EntityTests.cs rename to tests/Silverback.Core.Model.Tests/Domain/DomainEntityTests.cs index 0682cc4d3..05e754f5b 100644 --- a/tests/Silverback.Core.Model.Tests/Domain/EntityTests.cs +++ b/tests/Silverback.Core.Model.Tests/Domain/DomainEntityTests.cs @@ -8,10 +8,10 @@ namespace Silverback.Tests.Core.Model.Domain { - public class EntityTests + public class DomainEntityTests { [Fact] - public void AddEventTest() + public void AddEvent_EventInstance_AddedToCollection() { var entity = new TestAggregateRoot(); @@ -25,7 +25,7 @@ public void AddEventTest() } [Fact] - public void AddEventGenericTest() + public void AddEvent_EventType_AddedToCollection() { var entity = new TestAggregateRoot(); @@ -39,14 +39,26 @@ public void AddEventGenericTest() } [Fact] - public void ClearEventsTest() + public void AddEvent_SameEventTypeWithoutAllowMultiple_AddedOnlyOnceToCollection() + { + var entity = new TestAggregateRoot(); + + entity.AddEvent(false); + entity.AddEvent(false); + entity.AddEvent(false); + + entity.DomainEvents.Count().Should().Be(2); + } + + [Fact] + public void ClearMessages_WithSomePendingMessages_MessagesCleared() { var entity = new TestAggregateRoot(); entity.AddEvent(); entity.AddEvent(); entity.AddEvent(); - entity.ClearEvents(); + entity.ClearMessages(); entity.DomainEvents.Should().NotBeNull(); entity.DomainEvents.Should().BeEmpty(); diff --git a/tests/Silverback.Core.Model.Tests/Messaging/Publishing/QueryPublisherTests.cs b/tests/Silverback.Core.Model.Tests/Messaging/Publishing/QueryPublisherTests.cs index 3e168e7c4..648eb156b 100644 --- a/tests/Silverback.Core.Model.Tests/Messaging/Publishing/QueryPublisherTests.cs +++ b/tests/Silverback.Core.Model.Tests/Messaging/Publishing/QueryPublisherTests.cs @@ -19,26 +19,50 @@ public class QueryPublisherTests private readonly IQueryPublisher _publisher; public QueryPublisherTests() - { + { var services = new ServiceCollection(); services.AddBus(options => options.UseModel()); services.AddSingleton(); services.AddSingleton(typeof(ILogger<>), typeof(NullLogger<>)); - services.AddSingleton(_ => new QueriesHandler()); + services.AddSingleton(_ => new QueriesHandler()); - var serviceProvider = services.BuildServiceProvider(); + var serviceProvider = services.BuildServiceProvider(new ServiceProviderOptions { ValidateScopes = true }); - _publisher = serviceProvider.GetRequiredService(); + _publisher = serviceProvider.CreateScope().ServiceProvider.GetRequiredService(); } [Fact] public async Task ExecuteAsync_ListQuery_EnumerableReturned() { - var result = await _publisher.ExecuteAsync(new ListQuery {Count = 3}); + var result = await _publisher.ExecuteAsync(new ListQuery { Count = 3 }); result.Should().BeEquivalentTo(1, 2, 3); } + + [Fact] + public async Task ExecuteAsync_ListQueries_EnumerablesReturned() + { + var result = await _publisher.ExecuteAsync(new[] { new ListQuery { Count = 3 }, new ListQuery { Count = 3 } }); + + result.Should().BeEquivalentTo(new[] { 1, 2, 3 }, new[] { 1, 2, 3 }); + } + + [Fact] + public void Execute_ListQuery_EnumerableReturned() + { + var result = _publisher.Execute(new ListQuery { Count = 3 }); + + result.Should().BeEquivalentTo(1, 2, 3); + } + + [Fact] + public void Execute_ListQueries_EnumerablesReturned() + { + var result = _publisher.Execute(new[] { new ListQuery { Count = 3 }, new ListQuery { Count = 3 } }); + + result.Should().BeEquivalentTo(new[] { 1, 2, 3 }, new[] { 1, 2, 3 }); + } } } \ No newline at end of file diff --git a/tests/Silverback.Core.Model.Tests/Silverback.Core.Model.Tests.csproj b/tests/Silverback.Core.Model.Tests/Silverback.Core.Model.Tests.csproj index 5e06cd5b8..8c03293ef 100644 --- a/tests/Silverback.Core.Model.Tests/Silverback.Core.Model.Tests.csproj +++ b/tests/Silverback.Core.Model.Tests/Silverback.Core.Model.Tests.csproj @@ -1,19 +1,21 @@  - netcoreapp2.1 + netcoreapp2.2 Silverback.Tests.Core.Model - - - latest - - - - + + + + all + runtime; build; native; contentfiles; analyzers + + + + all diff --git a/tests/Silverback.Core.Model.Tests/TestTypes/Domain/TestAggregateRoot.cs b/tests/Silverback.Core.Model.Tests/TestTypes/Domain/TestAggregateRoot.cs index 751ba47ea..b0e715b56 100644 --- a/tests/Silverback.Core.Model.Tests/TestTypes/Domain/TestAggregateRoot.cs +++ b/tests/Silverback.Core.Model.Tests/TestTypes/Domain/TestAggregateRoot.cs @@ -7,11 +7,11 @@ namespace Silverback.Tests.Core.Model.TestTypes.Domain { public class TestAggregateRoot : DomainEntity, IAggregateRoot { - public new void AddEvent(IDomainEvent domainEvent) + public new void AddEvent(IDomainEvent domainEvent) => base.AddEvent(domainEvent); - public new TEvent AddEvent() - where TEvent : IDomainEvent, new() - => base.AddEvent(); + public new TEvent AddEvent(bool allowMultiple = true) + where TEvent : IDomainEvent, new() + => base.AddEvent(allowMultiple); } } diff --git a/tests/Silverback.Core.Model.Tests/TestTypes/Subscribers/Subscribers.cs b/tests/Silverback.Core.Model.Tests/TestTypes/Subscribers/QueriesHandler.cs similarity index 84% rename from tests/Silverback.Core.Model.Tests/TestTypes/Subscribers/Subscribers.cs rename to tests/Silverback.Core.Model.Tests/TestTypes/Subscribers/QueriesHandler.cs index 49d8d4255..bbe561f9e 100644 --- a/tests/Silverback.Core.Model.Tests/TestTypes/Subscribers/Subscribers.cs +++ b/tests/Silverback.Core.Model.Tests/TestTypes/Subscribers/QueriesHandler.cs @@ -13,5 +13,8 @@ public class QueriesHandler : ISubscriber { [Subscribe] public Task> Handle(ListQuery query) => Task.FromResult(Enumerable.Range(1, query.Count)); + + [Subscribe] + public Task TryToBreak(ListQuery query) => Task.FromResult(new object[0]); } } diff --git a/tests/Silverback.Core.Rx.Tests/Messaging/MessageObservableTests.cs b/tests/Silverback.Core.Rx.Tests/Messaging/MessageObservableTests.cs index 4147a3fd0..7633d82a3 100644 --- a/tests/Silverback.Core.Rx.Tests/Messaging/MessageObservableTests.cs +++ b/tests/Silverback.Core.Rx.Tests/Messaging/MessageObservableTests.cs @@ -37,9 +37,9 @@ public MessageObservableTests() services.AddSingleton(_messageObservable); - var serviceProvider = services.BuildServiceProvider(); + var serviceProvider = services.BuildServiceProvider(new ServiceProviderOptions { ValidateScopes = true }); - _publisher = serviceProvider.GetRequiredService(); + _publisher = serviceProvider.CreateScope().ServiceProvider.GetRequiredService(); } [Fact] diff --git a/tests/Silverback.Core.Rx.Tests/Messaging/Publishing/PublisherTests.cs b/tests/Silverback.Core.Rx.Tests/Messaging/Publishing/PublisherTests.cs index f2c202cd9..7a242eae8 100644 --- a/tests/Silverback.Core.Rx.Tests/Messaging/Publishing/PublisherTests.cs +++ b/tests/Silverback.Core.Rx.Tests/Messaging/Publishing/PublisherTests.cs @@ -19,8 +19,6 @@ namespace Silverback.Tests.Core.Rx.Messaging.Publishing { public class PublisherTests { - private IPublisher GetPublisher(params ISubscriber[] subscribers) => GetPublisher(null, subscribers); - private IPublisher GetPublisher(Action configAction, params ISubscriber[] subscribers) { var services = new ServiceCollection(); @@ -30,13 +28,13 @@ private IPublisher GetPublisher(Action configAction, params ISu services.AddSingleton(typeof(ILogger<>), typeof(NullLogger<>)); foreach (var sub in subscribers) - services.AddSingleton(sub); + services.AddScoped(_ => sub); - var serviceProvider = services.BuildServiceProvider(); + var serviceProvider = services.BuildServiceProvider(new ServiceProviderOptions { ValidateScopes = true }); configAction?.Invoke(serviceProvider.GetRequiredService()); - return serviceProvider.GetRequiredService(); + return serviceProvider.CreateScope().ServiceProvider.GetRequiredService(); } [Fact] diff --git a/tests/Silverback.Core.Rx.Tests/Messaging/TypedMessageObservableTests.cs b/tests/Silverback.Core.Rx.Tests/Messaging/TypedMessageObservableTests.cs index 55036b9fb..54d2912d6 100644 --- a/tests/Silverback.Core.Rx.Tests/Messaging/TypedMessageObservableTests.cs +++ b/tests/Silverback.Core.Rx.Tests/Messaging/TypedMessageObservableTests.cs @@ -39,9 +39,9 @@ public TypedMessageObservableTests() services.AddSingleton(observable); - var serviceProvider = services.BuildServiceProvider(); + var serviceProvider = services.BuildServiceProvider(new ServiceProviderOptions { ValidateScopes = true }); - _publisher = serviceProvider.GetRequiredService(); + _publisher = serviceProvider.CreateScope().ServiceProvider.GetRequiredService(); } [Fact] diff --git a/tests/Silverback.Core.Rx.Tests/Silverback.Core.Rx.Tests.csproj b/tests/Silverback.Core.Rx.Tests/Silverback.Core.Rx.Tests.csproj index d8aa2c1dd..32e7974c9 100644 --- a/tests/Silverback.Core.Rx.Tests/Silverback.Core.Rx.Tests.csproj +++ b/tests/Silverback.Core.Rx.Tests/Silverback.Core.Rx.Tests.csproj @@ -1,14 +1,20 @@  - netcoreapp2.1 + netcoreapp2.2 Silverback.Tests.Core.Rx + latest - - - + + + all + runtime; build; native; contentfiles; analyzers + + + + all diff --git a/tests/Silverback.Core.Rx.Tests/TestTypes/Messages/Base/IQuery.cs b/tests/Silverback.Core.Rx.Tests/TestTypes/Messages/Base/IQuery.cs index 1d0b011ba..dd728a10f 100644 --- a/tests/Silverback.Core.Rx.Tests/TestTypes/Messages/Base/IQuery.cs +++ b/tests/Silverback.Core.Rx.Tests/TestTypes/Messages/Base/IQuery.cs @@ -1,5 +1,6 @@ // Copyright (c) 2018-2019 Sergio Aquilini // This code is licensed under MIT license (see LICENSE file for details) + namespace Silverback.Tests.Core.Rx.TestTypes.Messages.Base { public interface IQuery : IRequest diff --git a/tests/Silverback.Core.Tests/Background/DistributedBackgroundServiceTests.cs b/tests/Silverback.Core.Tests/Background/DistributedBackgroundServiceTests.cs new file mode 100644 index 000000000..40497790a --- /dev/null +++ b/tests/Silverback.Core.Tests/Background/DistributedBackgroundServiceTests.cs @@ -0,0 +1,104 @@ +// Copyright (c) 2018-2019 Sergio Aquilini +// This code is licensed under MIT license (see LICENSE file for details) + +using System; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Extensions.Logging; +using NSubstitute; +using Silverback.Background; +using Silverback.Tests.Core.TestTypes; +using Silverback.Tests.Core.TestTypes.Background; +using Xunit; + +namespace Silverback.Tests.Core.Background +{ + public class DistributedBackgroundServiceTests + { + [Fact] + public async Task StartAsync_NullLockManager_TaskIsExecuted() + { + bool executed = false; + + var service = new TestDistributedBackgroundService(_ => + { + executed = true; + return Task.CompletedTask; + }, new NullLockManager()); + await service.StartAsync(CancellationToken.None); + + AsyncTestingUtil.Wait(() => executed); + + executed.Should().BeTrue(); + } + + [Fact] + public async Task StartAsync_WithTestLockManager_TaskIsExecuted() + { + bool executed = false; + + var service = new TestDistributedBackgroundService(_ => + { + executed = true; + return Task.CompletedTask; + }, new TestLockManager()); + await service.StartAsync(CancellationToken.None); + + AsyncTestingUtil.Wait(() => executed); + + executed.Should().BeTrue(); + } + + [Fact] + public async Task StartAsync_WithTestLockManager_OnlyOneTaskIsExecutedSimultaneously() + { + var lockManager = new TestLockManager(); + bool executed1 = false; + bool executed2 = false; + + var service1 = new TestDistributedBackgroundService(async stoppingToken => + { + executed1 = true; + + while (!stoppingToken.IsCancellationRequested) + { + await Task.Delay(10, stoppingToken); + } + }, lockManager); + await service1.StartAsync(CancellationToken.None); + + await AsyncTestingUtil.WaitAsync(() => executed1); + + var service2 = new TestDistributedBackgroundService(_ => + { + executed2 = true; + return Task.CompletedTask; + }, lockManager); + await service2.StartAsync(CancellationToken.None); + + await AsyncTestingUtil.WaitAsync(() => executed2, 100); + + executed1.Should().BeTrue(); + executed2.Should().BeFalse(); + + await service1.StopAsync(CancellationToken.None); + await AsyncTestingUtil.WaitAsync(() => executed2); + + executed2.Should().BeTrue(); + } + + public class TestDistributedBackgroundService : DistributedBackgroundService + { + private readonly Func _task; + + public TestDistributedBackgroundService(Func task, IDistributedLockManager lockManager) + : base(new DistributedLockSettings("test"), lockManager, Substitute.For>()) + { + _task = task; + } + + protected override Task ExecuteLockedAsync(CancellationToken stoppingToken) => _task.Invoke(stoppingToken); + } + } +} \ No newline at end of file diff --git a/tests/Silverback.Core.Tests/Background/RecurringDistributedBackgroundServiceTests.cs b/tests/Silverback.Core.Tests/Background/RecurringDistributedBackgroundServiceTests.cs new file mode 100644 index 000000000..5d19f2986 --- /dev/null +++ b/tests/Silverback.Core.Tests/Background/RecurringDistributedBackgroundServiceTests.cs @@ -0,0 +1,122 @@ +// Copyright (c) 2018-2019 Sergio Aquilini +// This code is licensed under MIT license (see LICENSE file for details) + +using System; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Extensions.Logging; +using NSubstitute; +using Silverback.Background; +using Silverback.Tests.Core.TestTypes; +using Silverback.Tests.Core.TestTypes.Background; +using Xunit; + +namespace Silverback.Tests.Core.Background +{ + public class RecurringDistributedBackgroundServiceTests + { + [Fact] + public async Task StartAsync_WithTestLockManager_TaskIsExecuted() + { + bool executed = false; + + var service = new TestRecurringDistributedBackgroundService(_ => + { + executed = true; + return Task.CompletedTask; + }, new TestLockManager()); + await service.StartAsync(CancellationToken.None); + + AsyncTestingUtil.Wait(() => executed); + + executed.Should().BeTrue(); + } + + [Fact] + public async Task StartAsync_WithTestLockManager_OnlyOneTaskIsExecutedSimultaneously() + { + var lockManager = new TestLockManager(); + bool executed1 = false; + bool executed2 = false; + + var service1 = new TestRecurringDistributedBackgroundService(stoppingToken => + { + executed1 = true; + return Task.CompletedTask; + }, lockManager); + await service1.StartAsync(CancellationToken.None); + + await AsyncTestingUtil.WaitAsync(() => executed1); + + var service2 = new TestRecurringDistributedBackgroundService(_ => + { + executed2 = true; + return Task.CompletedTask; + }, lockManager); + await service2.StartAsync(CancellationToken.None); + + await AsyncTestingUtil.WaitAsync(() => executed2, 100); + + executed1.Should().BeTrue(); + executed2.Should().BeFalse(); + + await service1.StopAsync(CancellationToken.None); + await AsyncTestingUtil.WaitAsync(() => executed2); + + executed2.Should().BeTrue(); + } + + [Fact] + public async Task StartAsync_SimpleTask_TaskExecutedMultipleTimes() + { + int executions = 0; + + var service = new TestRecurringDistributedBackgroundService(_ => + { + executions++; + return Task.CompletedTask; + }, new TestLockManager()); + await service.StartAsync(CancellationToken.None); + + await Task.Delay(100); + + executions.Should().BeGreaterThan(1); + } + + [Fact] + public async Task StopAsync_SimpleTask_ExecutionStopped() + { + int executions = 0; + + var service = new TestRecurringDistributedBackgroundService(_ => + { + executions++; + return Task.CompletedTask; + }, new TestLockManager()); + await service.StartAsync(CancellationToken.None); + + await Task.Delay(100); + + await service.StopAsync(CancellationToken.None); + var executionsBeforeStop = executions; + + await Task.Delay(100); + + executions.Should().Be(executionsBeforeStop); + } + + public class TestRecurringDistributedBackgroundService : RecurringDistributedBackgroundService + { + private readonly Func _task; + + public TestRecurringDistributedBackgroundService(Func task, IDistributedLockManager lockManager) + : base(TimeSpan.FromMilliseconds(10), new DistributedLockSettings("test"), lockManager, Substitute.For>()) + { + _task = task; + } + + protected override Task ExecuteRecurringAsync(CancellationToken stoppingToken) => _task.Invoke(stoppingToken); + } + } +} \ No newline at end of file diff --git a/tests/Silverback.Core.Tests/Messaging/Publishing/PublisherTests.cs b/tests/Silverback.Core.Tests/Messaging/Publishing/PublisherTests.cs index 5adcf2fc7..323cf7f8e 100644 --- a/tests/Silverback.Core.Tests/Messaging/Publishing/PublisherTests.cs +++ b/tests/Silverback.Core.Tests/Messaging/Publishing/PublisherTests.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; using System.Threading.Tasks; using FluentAssertions; using Microsoft.Extensions.DependencyInjection; @@ -14,6 +15,7 @@ using Silverback.Messaging.Publishing; using Silverback.Messaging.Subscribers; using Silverback.Messaging.Subscribers.Subscriptions; +using Silverback.Tests.Core.TestTypes; using Silverback.Tests.Core.TestTypes.Behaviors; using Silverback.Tests.Core.TestTypes.Messages; using Silverback.Tests.Core.TestTypes.Messages.Base; @@ -40,7 +42,7 @@ public PublisherTests() private IPublisher GetPublisher(params ISubscriber[] subscribers) => GetPublisher(null, subscribers); private IPublisher GetPublisher(Action configAction, params ISubscriber[] subscribers) => - GetPublisher(configAction, null, subscribers); + GetPublisher(configAction, null, subscribers); private IPublisher GetPublisher(Action configAction, IBehavior[] behaviors, params ISubscriber[] subscribers) { @@ -202,7 +204,7 @@ public async Task Publish_SomeMessages_ReceivedByAllSubscribedMethods() service2.ReceivedMessagesCount.Should().Be(4); } - [Theory, ClassData(typeof(Publish_SubscribedMessage_ReceivedRepublishedMessages_TestData))] + [Theory, MemberData(nameof(Publish_SubscribedMessage_ReceivedRepublishedMessages_TestData))] public void Publish_SubscribedMessage_ReceivedRepublishedMessages(IEvent message, int expectedEventOne, int expectedEventTwo) { var service1 = new TestServiceOne(); @@ -215,7 +217,7 @@ public void Publish_SubscribedMessage_ReceivedRepublishedMessages(IEvent message service2.ReceivedMessagesCount.Should().Be(expectedEventTwo * 2); } - [Theory, ClassData(typeof(Publish_SubscribedMessage_ReceivedRepublishedMessages_TestData))] + [Theory, MemberData(nameof(Publish_SubscribedMessage_ReceivedRepublishedMessages_TestData))] public async Task PublishAsync_SubscribedMessage_ReceivedRepublishedMessages(IEvent message, int expectedEventOne, int expectedEventTwo) { var service1 = new TestServiceOne(); @@ -228,13 +230,23 @@ public async Task PublishAsync_SubscribedMessage_ReceivedRepublishedMessages(IEv service2.ReceivedMessagesCount.Should().Be(expectedEventTwo * 2); } + public static IEnumerable Publish_SubscribedMessage_ReceivedRepublishedMessages_TestData => + new List + { + new object[] { new TestEventOne(), 1, 0 }, + new object[] { new TestEventTwo(), 1, 1 } + }; + [Fact] public void Publish_ExceptionInSubscriber_ExceptionReturned() { var publisher = GetPublisher(new TestExceptionSubscriber()); - publisher.Invoking(x => x.Publish(new TestEventOne())).Should().Throw(); - publisher.Invoking(x => x.Publish(new TestEventTwo())).Should().Throw(); + Action act1 = () => publisher.Publish(new TestEventOne()); + Action act2 = () => publisher.Publish(new TestEventTwo()); + + act1.Should().Throw(); + act2.Should().Throw(); } [Fact] @@ -245,8 +257,8 @@ public void PublishAsync_ExceptionInSubscriber_ExceptionReturned() Func act1 = async () => await publisher.PublishAsync(new TestEventOne()); Func act2 = async () => await publisher.PublishAsync(new TestEventTwo()); - act1.Should().Throw(); - act2.Should().Throw(); + act1.Should().Throw(); + act2.Should().Throw(); } [Fact] @@ -302,6 +314,36 @@ public void Publish_HandlersReturnValue_ResultsReturned() results.Should().Equal("response", "response2"); } + + [Fact] + public void Publish_HandlersReturnValueOfWrongType_EmptyResultReturned() + { + var publisher = GetPublisher(config => config.Subscribe(false), new TestRequestReplier()); + + var results = publisher.Publish(new TestRequestCommandOne()); + + results.Should().BeEmpty(); + } + + [Fact] + public void Publish_SomeHandlersReturnValueOfWrongType_ValuesOfCorrectTypeAreReturned() + { + var publisher = GetPublisher(config => config.Subscribe(false), new TestRequestReplier(), new TestRequestReplierWithWrongResponseType()); + + var results = publisher.Publish(new TestRequestCommandOne()); + + results.Should().Equal("response", "response2"); + } + + [Fact] + public void Publish_HandlersReturnNull_EmptyResultReturned() + { + var publisher = GetPublisher(config => config.Subscribe(false), new TestRequestReplierReturningNull()); + + var results = publisher.Publish(new TestRequestCommandOne()); + + results.Should().BeEmpty(); + } [Fact] public async Task PublishAsync_HandlersReturnValue_ResultsReturned() @@ -313,6 +355,36 @@ public async Task PublishAsync_HandlersReturnValue_ResultsReturned() results.Should().Equal("response", "response2"); } + [Fact] + public async Task PublishAsync_HandlersReturnValueOfWrongType_EmptyResultReturned() + { + var publisher = GetPublisher(config => config.Subscribe(false), new TestRequestReplier()); + + var results = await publisher.PublishAsync(new TestRequestCommandOne()); + + results.Should().Equal("response", "response2"); + } + + [Fact] + public async Task PublishAsync_SomeHandlersReturnValueOfWrongType_ValuesOfCorrectTypeAreReturned() + { + var publisher = GetPublisher(config => config.Subscribe(false), new TestRequestReplier(), new TestRequestReplierWithWrongResponseType()); + + var results = await publisher.PublishAsync(new TestRequestCommandOne()); + + results.Should().Equal("response", "response2"); + } + + [Fact] + public async Task PublishAsync_HandlersReturnNull_EmptyResultReturned() + { + var publisher = GetPublisher(config => config.Subscribe(false), new TestRequestReplierReturningNull()); + + var results = await publisher.PublishAsync(new TestRequestCommandOne()); + + results.Should().BeEmpty(); + } + [Fact] public void Publish_HandlersReturnValue_EnumerableReturned() { @@ -432,6 +504,26 @@ public async Task PublishAsync_MessagesBatch_BatchReceived() _asyncEnumerableSubscriber.ReceivedMessagesCount.Should().Be(3); } + [Fact] + public async Task PublishAsync_MessagesBatch_ResultsReturned() + { + var publisher = GetPublisher(config => config.Subscribe(false), new TestRequestReplier()); + + var results = await publisher.PublishAsync>(new[] { new TestRequestCommandTwo(), new TestRequestCommandTwo() }); + + results.SelectMany(x => x).Should().Equal("one", "two", "one", "two"); + } + + [Fact] + public void Publish_MessagesBatch_ResultsReturned() + { + var publisher = GetPublisher(config => config.Subscribe(false), new TestRequestReplier()); + + var results = publisher.Publish>(new[] { new TestRequestCommandTwo(), new TestRequestCommandTwo() }); + + results.SelectMany(x => x).Should().Equal("one", "two", "one", "two"); + } + [Fact] public void Publish_MessagesBatch_EachMessageReceivedByDelegateSubscription() { @@ -542,7 +634,7 @@ public void Publish_ExclusiveSubscribers_SequentiallyInvoked() subscriber.Parallel.Steps.Should().BeEquivalentTo(1, 2); } - [Fact, Trait("CI", "false")] + [Fact, Trait("CI", "true")] public void Publish_NonExclusiveSubscribers_InvokedInParallel() { var subscriber = new NonExclusiveSubscriberTestService(); @@ -564,7 +656,7 @@ public async Task PublishAsync_ExclusiveSubscribers_SequentiallyInvoked() subscriber.Parallel.Steps.Should().BeEquivalentTo(1, 2); } - [Fact, Trait("CI", "false")] + [Fact, Trait("CI", "true")] public async Task PublishAsync_NonExclusiveSubscribers_InvokedInParallel() { var subscriber = new NonExclusiveSubscriberTestService(); @@ -593,7 +685,7 @@ public void Publish_ExclusiveDelegateSubscription_SequentiallyInvoked() parallel.Steps.Should().BeEquivalentTo(1, 2); } - [Fact, Trait("CI", "false")] + [Fact, Trait("CI", "true")] public void Publish_NonExclusiveDelegateSubscription_InvokedInParallel() { var parallel = new ParallelTestingUtil(); @@ -627,7 +719,7 @@ public void Publish_NonParallelSubscriber_SequentiallyProcessing() subscriber.Parallel.Steps.Should().BeEquivalentTo(1, 2, 3, 4); } - [Fact, Trait("CI", "false")] + [Fact, Trait("CI", "true")] public void Publish_ParallelSubscriber_ProcessingInParallel() { var subscriber = new ParallelSubscriberTestService(); @@ -665,7 +757,7 @@ public void Publish_NonParallelDelegateSubscription_SequentiallyProcessing() parallel.Steps.Should().BeEquivalentTo(1, 2, 3, 4); } - [Fact, Trait("CI", "false")] + [Fact, Trait("CI", "true")] public void Publish_ParallelDelegateSubscription_ProcessingInParallel() { var parallel = new ParallelTestingUtil(); @@ -688,7 +780,7 @@ public void Publish_ParallelDelegateSubscription_ProcessingInParallel() parallel.Steps.Should().BeEquivalentTo(1, 1, 3, 3); } - [Fact, Trait("CI", "false")] + [Fact, Trait("CI", "true")] public void Publish_LimitedParallelSubscriber_ProcessingInParallel() { var subscriber = new LimitedParallelSubscriberTestService(); @@ -703,7 +795,7 @@ public void Publish_LimitedParallelSubscriber_ProcessingInParallel() subscriber.Parallel.Steps.Should().BeEquivalentTo(1, 1, 3, 3); } - [Fact, Trait("CI", "false")] + [Fact, Trait("CI", "true")] public async Task PublishAsync_LimitedParallelSubscriber_ProcessingInParallel() { var subscriber = new LimitedParallelSubscriberTestService(); @@ -719,7 +811,7 @@ await publisher.PublishAsync(new ICommand[] subscriber.Parallel.Steps.Should().BeEquivalentTo(1, 1, 3, 4, 4, 6); } - [Fact, Trait("CI", "false")] + [Fact, Trait("CI", "true")] public void Publish_LimitedParallelDelegateSubscription_ProcessingInParallel() { var parallel = new ParallelTestingUtil(); @@ -743,7 +835,7 @@ public void Publish_LimitedParallelDelegateSubscription_ProcessingInParallel() parallel.Steps.Should().BeEquivalentTo(1, 1, 3, 4, 4, 6); } - [Fact, Trait("CI", "false")] + [Fact, Trait("CI", "true")] public async Task PublishAsync_LimitedParallelDelegateSubscription_ProcessingInParallel() { var parallel = new ParallelTestingUtil(); diff --git a/tests/Silverback.Core.Tests/Messaging/Publishing/PublisherTestsData.cs b/tests/Silverback.Core.Tests/Messaging/Publishing/PublisherTestsData.cs deleted file mode 100644 index 8ca399e8e..000000000 --- a/tests/Silverback.Core.Tests/Messaging/Publishing/PublisherTestsData.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) 2018-2019 Sergio Aquilini -// This code is licensed under MIT license (see LICENSE file for details) - -using System.Collections; -using System.Collections.Generic; -using Silverback.Tests.Core.TestTypes.Messages; - -namespace Silverback.Tests.Core.Messaging.Publishing -{ - public class Publish_SubscribedMessage_ReceivedRepublishedMessages_TestData : IEnumerable - { - public IEnumerator GetEnumerator() - { - // event - yield return new object[] { new TestEventOne(), 1, 0 }; - yield return new object[] { new TestEventTwo(), 1, 1 }; - } - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - } -} diff --git a/tests/Silverback.Core.Tests/Silverback.Core.Tests.csproj b/tests/Silverback.Core.Tests/Silverback.Core.Tests.csproj index 75a5c4087..f28c4ba68 100644 --- a/tests/Silverback.Core.Tests/Silverback.Core.Tests.csproj +++ b/tests/Silverback.Core.Tests/Silverback.Core.Tests.csproj @@ -1,19 +1,21 @@ - netcoreapp2.1 + netcoreapp2.2 Silverback.Tests.Core - - - latest - - - - + + + all + runtime; build; native; contentfiles; analyzers + + + + + all diff --git a/tests/Silverback.Core.Tests/TestTypes/AsyncTestingUtil.cs b/tests/Silverback.Core.Tests/TestTypes/AsyncTestingUtil.cs new file mode 100644 index 000000000..0962f7b93 --- /dev/null +++ b/tests/Silverback.Core.Tests/TestTypes/AsyncTestingUtil.cs @@ -0,0 +1,36 @@ +// Copyright (c) 2018-2019 Sergio Aquilini +// This code is licensed under MIT license (see LICENSE file for details) + +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Silverback.Tests.Core.TestTypes +{ + public static class AsyncTestingUtil + { + public static void Wait(Func breakCondition, int timeoutInMilliseconds = 1000) + { + const int sleep = 10; + for (int i = 0; i < timeoutInMilliseconds; i = i + sleep) + { + if (breakCondition()) + break; + + Thread.Sleep(10); + } + } + + public static async Task WaitAsync(Func breakCondition, int timeoutInMilliseconds = 1000) + { + const int sleep = 10; + for (int i = 0; i < timeoutInMilliseconds; i = i + sleep) + { + if (breakCondition()) + break; + + await Task.Delay(sleep); + } + } + } +} diff --git a/tests/Silverback.Core.Tests/TestTypes/Background/TestLockManager.cs b/tests/Silverback.Core.Tests/TestTypes/Background/TestLockManager.cs new file mode 100644 index 000000000..56c696585 --- /dev/null +++ b/tests/Silverback.Core.Tests/TestTypes/Background/TestLockManager.cs @@ -0,0 +1,65 @@ +// Copyright (c) 2018-2019 Sergio Aquilini +// This code is licensed under MIT license (see LICENSE file for details) + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Silverback.Background; + +namespace Silverback.Tests.Core.TestTypes.Background +{ + public class TestLockManager : IDistributedLockManager + { + private readonly List _locks = new List(); + + public int Heartbeats { get; set; } = 0; + + public Task Acquire(DistributedLockSettings settings, CancellationToken cancellationToken = default) => + Acquire(settings.ResourceName, settings.AcquireTimeout, settings.AcquireRetryInterval); + + public async Task Acquire(string resourceName, TimeSpan? acquireTimeout = null, TimeSpan? acquireRetryInterval = null, + TimeSpan? heartbeatTimeout = null, CancellationToken cancellationToken = default) + { + var start = DateTime.Now; + while (acquireTimeout == null || DateTime.Now - start < acquireTimeout) + { + if (!_locks.Contains(resourceName)) + { + lock (_locks) + { + if (!_locks.Contains(resourceName)) + { + _locks.Add(resourceName); + return new DistributedLock(resourceName, this, 50); + } + } + } + + if (acquireRetryInterval != null) + await Task.Delay(acquireRetryInterval.Value.Milliseconds, cancellationToken); + + if (cancellationToken.IsCancellationRequested) + break; + } + + throw new TimeoutException("Couldn't get lock."); + } + + public Task SendHeartbeat(string resourceName) + { + Heartbeats++; + return Task.CompletedTask; + } + + public Task Release(string resourceName) + { + lock (_locks) + { + _locks.Remove(resourceName); + } + + return Task.CompletedTask; + } + } +} diff --git a/tests/Silverback.Core.Tests/TestTypes/Messages/Base/IQuery.cs b/tests/Silverback.Core.Tests/TestTypes/Messages/Base/IQuery.cs index 2ebfe393e..9cffa5173 100644 --- a/tests/Silverback.Core.Tests/TestTypes/Messages/Base/IQuery.cs +++ b/tests/Silverback.Core.Tests/TestTypes/Messages/Base/IQuery.cs @@ -1,5 +1,6 @@ // Copyright (c) 2018-2019 Sergio Aquilini // This code is licensed under MIT license (see LICENSE file for details) + namespace Silverback.Tests.Core.TestTypes.Messages.Base { public interface IQuery : IRequest diff --git a/tests/Silverback.Core.Tests/TestTypes/ParallelTestingUtil.cs b/tests/Silverback.Core.Tests/TestTypes/ParallelTestingUtil.cs index 5c6661b6a..61c4f4c58 100644 --- a/tests/Silverback.Core.Tests/TestTypes/ParallelTestingUtil.cs +++ b/tests/Silverback.Core.Tests/TestTypes/ParallelTestingUtil.cs @@ -1,11 +1,11 @@ -using System; -using System.Collections.Concurrent; +// Copyright (c) 2018-2019 Sergio Aquilini +// This code is licensed under MIT license (see LICENSE file for details) + using System.Collections.Generic; -using System.Text; using System.Threading; using System.Threading.Tasks; -namespace Silverback.Tests.Core.Messaging.Publishing +namespace Silverback.Tests.Core.TestTypes { public class ParallelTestingUtil { diff --git a/tests/Silverback.Core.Tests/TestTypes/Subscribers/ExclusiveSubscriberTestService.cs b/tests/Silverback.Core.Tests/TestTypes/Subscribers/ExclusiveSubscriberTestService.cs index bdd431022..c6a201616 100644 --- a/tests/Silverback.Core.Tests/TestTypes/Subscribers/ExclusiveSubscriberTestService.cs +++ b/tests/Silverback.Core.Tests/TestTypes/Subscribers/ExclusiveSubscriberTestService.cs @@ -3,7 +3,6 @@ using System.Threading.Tasks; using Silverback.Messaging.Subscribers; -using Silverback.Tests.Core.Messaging.Publishing; namespace Silverback.Tests.Core.TestTypes.Subscribers { diff --git a/tests/Silverback.Core.Tests/TestTypes/Subscribers/LimitedParallelSubscriberTestService.cs b/tests/Silverback.Core.Tests/TestTypes/Subscribers/LimitedParallelSubscriberTestService.cs index 411217b9b..55a76d527 100644 --- a/tests/Silverback.Core.Tests/TestTypes/Subscribers/LimitedParallelSubscriberTestService.cs +++ b/tests/Silverback.Core.Tests/TestTypes/Subscribers/LimitedParallelSubscriberTestService.cs @@ -3,7 +3,6 @@ using System.Threading.Tasks; using Silverback.Messaging.Subscribers; -using Silverback.Tests.Core.Messaging.Publishing; namespace Silverback.Tests.Core.TestTypes.Subscribers { diff --git a/tests/Silverback.Core.Tests/TestTypes/Subscribers/NonExclusiveSubscriberTestService.cs b/tests/Silverback.Core.Tests/TestTypes/Subscribers/NonExclusiveSubscriberTestService.cs index 5ea7431d0..be53326b4 100644 --- a/tests/Silverback.Core.Tests/TestTypes/Subscribers/NonExclusiveSubscriberTestService.cs +++ b/tests/Silverback.Core.Tests/TestTypes/Subscribers/NonExclusiveSubscriberTestService.cs @@ -3,7 +3,6 @@ using System.Threading.Tasks; using Silverback.Messaging.Subscribers; -using Silverback.Tests.Core.Messaging.Publishing; namespace Silverback.Tests.Core.TestTypes.Subscribers { diff --git a/tests/Silverback.Core.Tests/TestTypes/Subscribers/NonParallelSubscriberTestService.cs b/tests/Silverback.Core.Tests/TestTypes/Subscribers/NonParallelSubscriberTestService.cs index 9a045b46b..4b92e4b3d 100644 --- a/tests/Silverback.Core.Tests/TestTypes/Subscribers/NonParallelSubscriberTestService.cs +++ b/tests/Silverback.Core.Tests/TestTypes/Subscribers/NonParallelSubscriberTestService.cs @@ -3,7 +3,6 @@ using System.Threading.Tasks; using Silverback.Messaging.Subscribers; -using Silverback.Tests.Core.Messaging.Publishing; namespace Silverback.Tests.Core.TestTypes.Subscribers { diff --git a/tests/Silverback.Core.Tests/TestTypes/Subscribers/ParallelSubscriberTestService.cs b/tests/Silverback.Core.Tests/TestTypes/Subscribers/ParallelSubscriberTestService.cs index 0a4998c40..e6a0eb628 100644 --- a/tests/Silverback.Core.Tests/TestTypes/Subscribers/ParallelSubscriberTestService.cs +++ b/tests/Silverback.Core.Tests/TestTypes/Subscribers/ParallelSubscriberTestService.cs @@ -3,7 +3,6 @@ using System.Threading.Tasks; using Silverback.Messaging.Subscribers; -using Silverback.Tests.Core.Messaging.Publishing; namespace Silverback.Tests.Core.TestTypes.Subscribers { diff --git a/tests/Silverback.Core.Tests/TestTypes/Subscribers/TestRequestReplierReturningNull.cs b/tests/Silverback.Core.Tests/TestTypes/Subscribers/TestRequestReplierReturningNull.cs new file mode 100644 index 000000000..db42d2ff6 --- /dev/null +++ b/tests/Silverback.Core.Tests/TestTypes/Subscribers/TestRequestReplierReturningNull.cs @@ -0,0 +1,32 @@ +// Copyright (c) 2018-2019 Sergio Aquilini +// This code is licensed under MIT license (see LICENSE file for details) + +using System.Threading.Tasks; +using Silverback.Messaging.Subscribers; +using Silverback.Tests.Core.TestTypes.Messages.Base; + +namespace Silverback.Tests.Core.TestTypes.Subscribers +{ + public class TestRequestReplierReturningNull : ISubscriber + { + public int ReceivedMessagesCount { get; private set; } + + [Subscribe] + public string OnRequestReceived(IRequest message) + { + ReceivedMessagesCount++; + + return null; + } + + + [Subscribe] + public async Task OnRequestReceived2(IRequest message) + { + await Task.Delay(1); + ReceivedMessagesCount++; + + return null; + } + } +} \ No newline at end of file diff --git a/tests/Silverback.Core.Tests/TestTypes/Subscribers/TestRequestReplierWithWrongResponseType.cs b/tests/Silverback.Core.Tests/TestTypes/Subscribers/TestRequestReplierWithWrongResponseType.cs new file mode 100644 index 000000000..45fe7a5ce --- /dev/null +++ b/tests/Silverback.Core.Tests/TestTypes/Subscribers/TestRequestReplierWithWrongResponseType.cs @@ -0,0 +1,32 @@ +// Copyright (c) 2018-2019 Sergio Aquilini +// This code is licensed under MIT license (see LICENSE file for details) + +using System.Threading.Tasks; +using Silverback.Messaging.Subscribers; +using Silverback.Tests.Core.TestTypes.Messages.Base; + +namespace Silverback.Tests.Core.TestTypes.Subscribers +{ + public class TestRequestReplierWithWrongResponseType : ISubscriber + { + public int ReceivedMessagesCount { get; private set; } + + [Subscribe] + public int OnRequestReceived(IRequest message) + { + ReceivedMessagesCount++; + + return 1; + } + + + [Subscribe] + public async Task OnRequestReceived2(IRequest message) + { + await Task.Delay(1); + ReceivedMessagesCount++; + + return 2; + } + } +} \ No newline at end of file diff --git a/tests/Silverback.Core.Tests/Util/ReflectionHelperTests.cs b/tests/Silverback.Core.Tests/Util/ReflectionHelperTests.cs new file mode 100644 index 000000000..115800096 --- /dev/null +++ b/tests/Silverback.Core.Tests/Util/ReflectionHelperTests.cs @@ -0,0 +1,55 @@ +// Copyright (c) 2018-2019 Sergio Aquilini +// This code is licensed under MIT license (see LICENSE file for details) + +using System.Reflection; +using System.Threading.Tasks; +using FluentAssertions; +using Silverback.Util; +using Xunit; + +namespace Silverback.Tests.Core.Util +{ + public class ReflectionHelperTests + { + [Fact] + public void IsAsync_ReturnsTrue_IfAsyncWithTask() + { + MethodInfo methodInfo = GetType().GetMethod(nameof(AsyncWithTask), BindingFlags.Static | BindingFlags.NonPublic); ; + methodInfo.ReturnsTask().Should().BeTrue(); + } + + [Fact] + public void IsAsync_ReturnsFalse_IfAsyncWithoutTask() + { + MethodInfo methodInfo = GetType().GetMethod(nameof(AsyncWithoutTask), BindingFlags.Static | BindingFlags.NonPublic); ; + methodInfo.ReturnsTask().Should().BeFalse(); + } + + [Fact] + public void IsAsync_ReturnsFalse_IfNotAsyncWithVoid() + { + MethodInfo methodInfo = GetType().GetMethod(nameof(NotAsyncWithVoid), BindingFlags.Static | BindingFlags.NonPublic); ; + methodInfo.ReturnsTask().Should().BeFalse(); + } + + [Fact] + public void ReturnsTaskc_ReturnsTrue_IfNotAsyncWithTask() + { + MethodInfo methodInfo = GetType().GetMethod(nameof(NotAsyncWithTask), BindingFlags.Static | BindingFlags.NonPublic); ; + methodInfo.ReturnsTask().Should().BeTrue(); + } + + +#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously + private static async Task AsyncWithTask() { } +#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously + +#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously + private static async void AsyncWithoutTask() { } +#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously + + private static void NotAsyncWithVoid() { } + + private static Task NotAsyncWithTask() => Task.CompletedTask; + } +} diff --git a/tests/Silverback.EventSourcing.EntityFrameworkCore.Tests/EventStore/DbContextEventStoreRepositoryTests.cs b/tests/Silverback.EventSourcing.EntityFrameworkCore.Tests/EventStore/DbContextEventStoreRepositoryTests.cs new file mode 100644 index 000000000..94352f600 --- /dev/null +++ b/tests/Silverback.EventSourcing.EntityFrameworkCore.Tests/EventStore/DbContextEventStoreRepositoryTests.cs @@ -0,0 +1,625 @@ +// Copyright (c) 2018-2019 Sergio Aquilini +// This code is licensed under MIT license (see LICENSE file for details) + +using System; +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Silverback.Tests.EventSourcing.EntityFrameworkCore.TestTypes; +using Xunit; + +namespace Silverback.Tests.EventSourcing.EntityFrameworkCore.EventStore +{ + public class _dbContextEventStoreRepositoryTests : IDisposable + { + private readonly TestDbContext _dbContext; + private readonly SqliteConnection _conn; + + public _dbContextEventStoreRepositoryTests() + { + _conn = new SqliteConnection("DataSource=:memory:"); + _conn.Open(); + + _dbContext = new TestDbContext(new DbContextOptionsBuilder().UseSqlite(_conn).Options); + _dbContext.Database.EnsureCreated(); + } + + #region Store (Basics) + + [Fact] + public void Store_EntityWithSomeEvents_EventsSaved() + { + var repo = new PersonEventStoreRepository(_dbContext); + + var person = new Person(); + person.ChangeName("Sergio"); + person.ChangeAge(35); + + repo.Store(person); + _dbContext.SaveChanges(); + + _dbContext.Persons.Count().Should().Be(1); + _dbContext.Persons.First().Events.Count.Should().Be(2); + } + + [Fact] + public async Task StoreAsync_EntityWithSomeEvents_EventsSaved() + { + var repo = new PersonEventStoreRepository(_dbContext); + + var person = new Person(); + person.ChangeName("Sergio"); + person.ChangeAge(35); + + await repo.StoreAsync(person); + await _dbContext.SaveChangesAsync(); + + _dbContext.Persons.Count().Should().Be(1); + _dbContext.Persons.First().Events.Count.Should().Be(2); + } + + [Fact] + public void Store_ExistingEntity_NewEventsSaved() + { + var eventStore = _dbContext.Persons.Add(new PersonEventStore { Id = 12, EntityVersion = 1 }).Entity; + eventStore.Events.Add(new PersonEvent + { + SerializedEvent = "{" + + "\"$type\": \"Silverback.Tests.EventSourcing.EntityFrameworkCore.TestTypes.Person+NameChangedEvent, Silverback.EventSourcing.EntityFrameworkCore.Tests\"," + + "\"NewName\": \"Silverback\"" + + "}" + }); + _dbContext.SaveChanges(); + + var repo = new PersonEventStoreRepository(_dbContext); + + var person = repo.Get(p => p.Id == 12); + + person.ChangeName("Sergio"); + person.ChangeAge(35); + + repo.Store(person); + _dbContext.SaveChanges(); + + _dbContext.Persons.Count().Should().Be(1); + _dbContext.Persons.First().Events.Count.Should().Be(3); + } + + [Fact] + public async Task StoreAsync_ExistingEntity_NewEventsSaved() + { + var eventStore = _dbContext.Persons.Add(new PersonEventStore { Id = 12, EntityVersion = 1 }).Entity; + eventStore.Events.Add(new PersonEvent + { + SerializedEvent = "{" + + "\"$type\": \"Silverback.Tests.EventSourcing.EntityFrameworkCore.TestTypes.Person+NameChangedEvent, Silverback.EventSourcing.EntityFrameworkCore.Tests\"," + + "\"NewName\": \"Silverback\"" + + "}" + }); + _dbContext.SaveChanges(); + + var repo = new PersonEventStoreRepository(_dbContext); + + var person = await repo.GetAsync(p => p.Id == 12); + + person.ChangeName("Sergio"); + person.ChangeAge(35); + + await repo.StoreAsync(person); + await _dbContext.SaveChangesAsync(); + + _dbContext.Persons.Count().Should().Be(1); + _dbContext.Persons.First().Events.Count.Should().Be(3); + } + + #endregion + + + #region Store (Concurrency) + + [Fact] + public void Store_EntityWithSomeEvents_VersionCalculated() + { + var repo = new PersonEventStoreRepository(_dbContext); + var person = new Person(); + person.ChangeName("Sergio"); + person.ChangeAge(35); + + repo.Store(person); + _dbContext.SaveChanges(); + + _dbContext.Persons.First().EntityVersion.Should().Be(2); + } + + [Fact] + public async Task StoreAsync_EntityWithSomeEvents_VersionCalculated() + { + var repo = new PersonEventStoreRepository(_dbContext); + var person = new Person(); + person.ChangeName("Sergio"); + person.ChangeAge(35); + + await repo.StoreAsync(person); + await _dbContext.SaveChangesAsync(); + + _dbContext.Persons.First().EntityVersion.Should().Be(2); + } + + [Fact] + public void Store_ExistingEntity_VersionIncremented() + { + var eventStore = _dbContext.Persons.Add(new PersonEventStore { Id = 12, EntityVersion = 1 }).Entity; + eventStore.Events.Add(new PersonEvent + { + SerializedEvent = "{" + + "\"$type\": \"Silverback.Tests.EventSourcing.EntityFrameworkCore.TestTypes.Person+NameChangedEvent, Silverback.EventSourcing.EntityFrameworkCore.Tests\"," + + "\"NewName\": \"Silverback\"" + + "}" + }); + _dbContext.SaveChanges(); + + var repo = new PersonEventStoreRepository(_dbContext); + + var person = repo.Get(p => p.Id == 12); + + person.ChangeName("Sergio"); + person.ChangeAge(35); + + repo.Store(person); + + _dbContext.Persons.First().EntityVersion.Should().Be(3); + } + + [Fact] + public async Task StoreAsync_ExistingEntity_VersionIncremented() + { + var eventStore = _dbContext.Persons.Add(new PersonEventStore { Id = 12, EntityVersion = 1 }).Entity; + eventStore.Events.Add(new PersonEvent + { + SerializedEvent = "{" + + "\"$type\": \"Silverback.Tests.EventSourcing.EntityFrameworkCore.TestTypes.Person+NameChangedEvent, Silverback.EventSourcing.EntityFrameworkCore.Tests\"," + + "\"NewName\": \"Silverback\"" + + "}" + }); + _dbContext.SaveChanges(); + + var repo = new PersonEventStoreRepository(_dbContext); + + var person = repo.Get(p => p.Id == 12); + + person.ChangeName("Sergio"); + person.ChangeAge(35); + + await repo.StoreAsync(person); + + _dbContext.Persons.First().EntityVersion.Should().Be(3); + } + + [Fact] + public void Store_ConcurrentlyModifyExistingEntity_ExceptionThrown() + { + var eventStore = _dbContext.Persons.Add(new PersonEventStore { Id = 12, EntityVersion = 1 }).Entity; + eventStore.Events.Add(new PersonEvent + { + SerializedEvent = "{" + + "\"$type\": \"Silverback.Tests.EventSourcing.EntityFrameworkCore.TestTypes.Person+NameChangedEvent, Silverback.EventSourcing.EntityFrameworkCore.Tests\"," + + "\"NewName\": \"Silverback\"" + + "}" + }); + _dbContext.SaveChanges(); + + var repo = new PersonEventStoreRepository(_dbContext); + + var person = repo.Get(p => p.Id == 12); + var person2 = repo.Get(p => p.Id == 12); + + person.ChangeName("Sergio"); + person.ChangeAge(35); + person2.ChangeName("Sergio"); + person2.ChangeAge(35); + + repo.Store(person); + Action act = () => repo.Store(person2); + + act.Should().Throw(); + } + + [Fact] + public async Task StoreAsync_ConcurrentlyModifyExistingEntity_ExceptionThrown() + { + var eventStore = _dbContext.Persons.Add(new PersonEventStore { Id = 12, EntityVersion = 1 }).Entity; + eventStore.Events.Add(new PersonEvent + { + SerializedEvent = "{" + + "\"$type\": \"Silverback.Tests.EventSourcing.EntityFrameworkCore.TestTypes.Person+NameChangedEvent, Silverback.EventSourcing.EntityFrameworkCore.Tests\"," + + "\"NewName\": \"Silverback\"" + + "}" + }); + _dbContext.SaveChanges(); + + var repo = new PersonEventStoreRepository(_dbContext); + + var person = repo.Get(p => p.Id == 12); + var person2 = repo.Get(p => p.Id == 12); + + person.ChangeName("Sergio"); + person.ChangeAge(35); + person2.ChangeName("Sergio"); + person2.ChangeAge(35); + + await repo.StoreAsync(person); + Func act = async () => await repo.StoreAsync(person2); + + act.Should().Throw(); + } + + #endregion + + #region Get + + [Fact] + public void Get_ExistingId_EntityRecreated() + { + var eventStore = _dbContext.Persons.Add(new PersonEventStore { Id = 12 }).Entity; + eventStore.Events.Add(new PersonEvent + { + SerializedEvent = "{" + + "\"$type\": \"Silverback.Tests.EventSourcing.EntityFrameworkCore.TestTypes.Person+NameChangedEvent, Silverback.EventSourcing.EntityFrameworkCore.Tests\"," + + "\"NewName\": \"Silverback\"" + + "}" + }); + _dbContext.SaveChanges(); + + var repo = new PersonEventStoreRepository(_dbContext); + + var entity = repo.Get(p => p.Id == 12); + + entity.Should().NotBe(null); + } + + [Fact] + public void Get_ExistingId_EventsApplied() + { + var eventStore = _dbContext.Persons.Add(new PersonEventStore { Id = 12 }).Entity; + eventStore.Events.Add(new PersonEvent + { + SerializedEvent = "{" + + "\"$type\": \"Silverback.Tests.EventSourcing.EntityFrameworkCore.TestTypes.Person+NameChangedEvent, Silverback.EventSourcing.EntityFrameworkCore.Tests\"," + + "\"NewName\": \"Silverback\"" + + "}" + }); + eventStore.Events.Add(new PersonEvent + { + SerializedEvent = "{" + + "\"$type\": \"Silverback.Tests.EventSourcing.EntityFrameworkCore.TestTypes.Person+AgeChangedEvent, Silverback.EventSourcing.EntityFrameworkCore.Tests\"," + + "\"NewAge\": 35" + + "}" + }); + + _dbContext.SaveChanges(); + + var repo = new PersonEventStoreRepository(_dbContext); + + var entity = repo.Get(p => p.Id == 12); + + entity.Name.Should().Be("Silverback"); + entity.Age.Should().Be(35); + } + + [Fact] + public void Get_ExistingId_EventsAppliedInRightOrder() + { + var eventStore = _dbContext.Persons.Add(new PersonEventStore { Id = 12 }).Entity; + eventStore.Events.Add(new PersonEvent + { + SerializedEvent = "{" + + "\"$type\": \"Silverback.Tests.EventSourcing.EntityFrameworkCore.TestTypes.Person+NameChangedEvent, Silverback.EventSourcing.EntityFrameworkCore.Tests\"," + + "\"NewName\": \"Silverback\"" + + "}", + Timestamp = DateTime.Parse("2000-05-05") + }); + eventStore.Events.Add(new PersonEvent + { + SerializedEvent = "{" + + "\"$type\": \"Silverback.Tests.EventSourcing.EntityFrameworkCore.TestTypes.Person+NameChangedEvent, Silverback.EventSourcing.EntityFrameworkCore.Tests\"," + + "\"NewName\": \"Sergio\"" + + "}", + Timestamp = DateTime.Parse("2000-03-01") + }); + + _dbContext.SaveChanges(); + + var repo = new PersonEventStoreRepository(_dbContext); + + var entity = repo.Get(p => p.Id == 12); + + entity.Name.Should().Be("Silverback"); + } + + [Fact] + public void Get_NonExistingId_NullReturned() + { + var repo = new PersonEventStoreRepository(_dbContext); + + var entity = repo.Get(p => p.Id == 12); + + entity.Should().BeNull(); + } + + [Fact] + public async Task GetAsync_ExistingId_EntityRecreated() + { + var eventStore = _dbContext.Persons.Add(new PersonEventStore { Id = 12 }).Entity; + eventStore.Events.Add(new PersonEvent + { + SerializedEvent = "{" + + "\"$type\": \"Silverback.Tests.EventSourcing.EntityFrameworkCore.TestTypes.Person+NameChangedEvent, Silverback.EventSourcing.EntityFrameworkCore.Tests\"," + + "\"NewName\": \"Silverback\"" + + "}" + }); + _dbContext.SaveChanges(); + + var repo = new PersonEventStoreRepository(_dbContext); + + var entity = await repo.GetAsync(p => p.Id == 12); + + entity.Should().NotBe(null); + } + + [Fact] + public async Task GetAsync_ExistingId_EventsApplied() + { + var eventStore = _dbContext.Persons.Add(new PersonEventStore { Id = 12 }).Entity; + eventStore.Events.Add(new PersonEvent + { + SerializedEvent = "{" + + "\"$type\": \"Silverback.Tests.EventSourcing.EntityFrameworkCore.TestTypes.Person+NameChangedEvent, Silverback.EventSourcing.EntityFrameworkCore.Tests\"," + + "\"NewName\": \"Silverback\"" + + "}" + }); + eventStore.Events.Add(new PersonEvent + { + SerializedEvent = "{" + + "\"$type\": \"Silverback.Tests.EventSourcing.EntityFrameworkCore.TestTypes.Person+AgeChangedEvent, Silverback.EventSourcing.EntityFrameworkCore.Tests\"," + + "\"NewAge\": 35" + + "}" + }); + + _dbContext.SaveChanges(); + + var repo = new PersonEventStoreRepository(_dbContext); + + var entity = await repo.GetAsync(p => p.Id == 12); + + entity.Name.Should().Be("Silverback"); + entity.Age.Should().Be(35); + } + + [Fact] + public async Task GetAsync_ExistingId_EventsAppliedInRightOrder() + { + var eventStore = _dbContext.Persons.Add(new PersonEventStore { Id = 12 }).Entity; + eventStore.Events.Add(new PersonEvent + { + SerializedEvent = "{" + + "\"$type\": \"Silverback.Tests.EventSourcing.EntityFrameworkCore.TestTypes.Person+NameChangedEvent, Silverback.EventSourcing.EntityFrameworkCore.Tests\"," + + "\"NewName\": \"Silverback\"" + + "}", + Timestamp = DateTime.Parse("2000-05-05") + }); + eventStore.Events.Add(new PersonEvent + { + SerializedEvent = "{" + + "\"$type\": \"Silverback.Tests.EventSourcing.EntityFrameworkCore.TestTypes.Person+NameChangedEvent, Silverback.EventSourcing.EntityFrameworkCore.Tests\"," + + "\"NewName\": \"Sergio\"" + + "}", + Timestamp = DateTime.Parse("2000-03-01") + }); + + _dbContext.SaveChanges(); + + var repo = new PersonEventStoreRepository(_dbContext); + + var entity = await repo.GetAsync(p => p.Id == 12); + + entity.Name.Should().Be("Silverback"); + } + + [Fact] + public async Task GetAsync_NonExistingId_NullReturned() + { + var repo = new PersonEventStoreRepository(_dbContext); + + var entity = await repo.GetAsync(p => p.Id == 12); + + entity.Should().BeNull(); + } + + #endregion + + #region GetSnapshot + + [Fact] + public void GetSnapshot_ExistingIdWithPastSnapshot_OnlyRelevantEventsApplied() + { + var eventStore = _dbContext.Persons.Add(new PersonEventStore { Id = 12 }).Entity; + eventStore.Events.Add(new PersonEvent + { + SerializedEvent = "{" + + "\"$type\": \"Silverback.Tests.EventSourcing.EntityFrameworkCore.TestTypes.Person+NameChangedEvent, Silverback.EventSourcing.EntityFrameworkCore.Tests\"," + + "\"NewName\": \"Silverback\"" + + "}", + Timestamp = DateTime.Parse("2000-05-05") + }); + eventStore.Events.Add(new PersonEvent + { + SerializedEvent = "{" + + "\"$type\": \"Silverback.Tests.EventSourcing.EntityFrameworkCore.TestTypes.Person+NameChangedEvent, Silverback.EventSourcing.EntityFrameworkCore.Tests\"," + + "\"NewName\": \"Sergio\"" + + "}", + Timestamp = DateTime.Parse("2000-03-01") + }); + eventStore.Events.Add(new PersonEvent + { + SerializedEvent = "{" + + "\"$type\": \"Silverback.Tests.EventSourcing.EntityFrameworkCore.TestTypes.Person+AgeChangedEvent, Silverback.EventSourcing.EntityFrameworkCore.Tests\"," + + "\"NewAge\": 16" + + "}", + Timestamp = DateTime.Parse("2000-02-01") + }); + eventStore.Events.Add(new PersonEvent + { + SerializedEvent = "{" + + "\"$type\": \"Silverback.Tests.EventSourcing.EntityFrameworkCore.TestTypes.Person+AgeChangedEvent, Silverback.EventSourcing.EntityFrameworkCore.Tests\"," + + "\"NewAge\": 35" + + "}", + Timestamp = DateTime.Parse("2019-07-06") + }); + + _dbContext.SaveChanges(); + + var repo = new PersonEventStoreRepository(_dbContext); + + var entity = repo.GetSnapshot(p => p.Id == 12, DateTime.Parse("2000-03-01")); + + entity.Name.Should().Be("Sergio"); + entity.Age.Should().Be(16); + } + + [Fact] + public async Task GetSnapshotAsync_ExistingIdWithPastSnapshot_OnlyRelevantEventsApplied() + { + var eventStore = _dbContext.Persons.Add(new PersonEventStore { Id = 12 }).Entity; + eventStore.Events.Add(new PersonEvent + { + SerializedEvent = "{" + + "\"$type\": \"Silverback.Tests.EventSourcing.EntityFrameworkCore.TestTypes.Person+NameChangedEvent, Silverback.EventSourcing.EntityFrameworkCore.Tests\"," + + "\"NewName\": \"Silverback\"" + + "}", + Timestamp = DateTime.Parse("2000-05-05") + }); + eventStore.Events.Add(new PersonEvent + { + SerializedEvent = "{" + + "\"$type\": \"Silverback.Tests.EventSourcing.EntityFrameworkCore.TestTypes.Person+NameChangedEvent, Silverback.EventSourcing.EntityFrameworkCore.Tests\"," + + "\"NewName\": \"Sergio\"" + + "}", + Timestamp = DateTime.Parse("2000-03-01") + }); + eventStore.Events.Add(new PersonEvent + { + SerializedEvent = "{" + + "\"$type\": \"Silverback.Tests.EventSourcing.EntityFrameworkCore.TestTypes.Person+AgeChangedEvent, Silverback.EventSourcing.EntityFrameworkCore.Tests\"," + + "\"NewAge\": 16" + + "}", + Timestamp = DateTime.Parse("2000-02-01") + }); + eventStore.Events.Add(new PersonEvent + { + SerializedEvent = "{" + + "\"$type\": \"Silverback.Tests.EventSourcing.EntityFrameworkCore.TestTypes.Person+AgeChangedEvent, Silverback.EventSourcing.EntityFrameworkCore.Tests\"," + + "\"NewAge\": 35" + + "}", + Timestamp = DateTime.Parse("2019-07-06") + }); + + _dbContext.SaveChanges(); + + var repo = new PersonEventStoreRepository(_dbContext); + + var entity = await repo.GetSnapshotAsync(p => p.Id == 12, DateTime.Parse("2000-03-01")); + + entity.Name.Should().Be("Sergio"); + entity.Age.Should().Be(16); + } + + #endregion + + #region Remove + + [Fact] + public void Remove_ExistingEntity_EntityDeleted() + { + var eventStore = _dbContext.Persons.Add(new PersonEventStore { Id = 12 }).Entity; + eventStore.Events.Add(new PersonEvent + { + SerializedEvent = "{" + + "\"$type\": \"Silverback.Tests.EventSourcing.EntityFrameworkCore.TestTypes.Person+NameChangedEvent, Silverback.EventSourcing.EntityFrameworkCore.Tests\"," + + "\"NewName\": \"Silverback\"" + + "}" + }); + _dbContext.SaveChanges(); + + var repo = new PersonEventStoreRepository(_dbContext); + + var entity = repo.Get(p => p.Id == 12); + entity.Should().NotBeNull(); + + repo.Remove(entity); + _dbContext.SaveChanges(); + + _dbContext.Persons.Count().Should().Be(0); + _dbContext.Persons.SelectMany(s => s.Events).Count().Should().Be(0); + } + + [Fact] + public async Task RemoveAsync_ExistingEntity_EntityDeleted() + { + var eventStore = _dbContext.Persons.Add(new PersonEventStore { Id = 12 }).Entity; + eventStore.Events.Add(new PersonEvent + { + SerializedEvent = "{" + + "\"$type\": \"Silverback.Tests.EventSourcing.EntityFrameworkCore.TestTypes.Person+NameChangedEvent, Silverback.EventSourcing.EntityFrameworkCore.Tests\"," + + "\"NewName\": \"Silverback\"" + + "}" + }); + _dbContext.SaveChanges(); + + var repo = new PersonEventStoreRepository(_dbContext); + + var entity = await repo.GetAsync(p => p.Id == 12); + entity.Should().NotBeNull(); + + await repo.RemoveAsync(entity); + await _dbContext.SaveChangesAsync(); + + _dbContext.Persons.Count().Should().Be(0); + _dbContext.Persons.SelectMany(s => s.Events).Count().Should().Be(0); + + } + + [Fact] + public void Remove_NonExistingEntity_ReturnsNull() + { + var repo = new PersonEventStoreRepository(_dbContext); + + var entity = new Person(123); + + var result = repo.Remove(entity); + + result.Should().BeNull(); + } + + [Fact] + public async Task RemoveAsync_NonExistingEntity_ReturnsNull() + { + var repo = new PersonEventStoreRepository(_dbContext); + + var entity = new Person(123); + + var result = await repo.RemoveAsync(entity); + + result.Should().BeNull(); + + } + + #endregion + + public void Dispose() + { + _conn.Close(); + } + } +} \ No newline at end of file diff --git a/tests/Silverback.EventSourcing.EntityFrameworkCore.Tests/Silverback.EventSourcing.EntityFrameworkCore.Tests.csproj b/tests/Silverback.EventSourcing.EntityFrameworkCore.Tests/Silverback.EventSourcing.EntityFrameworkCore.Tests.csproj new file mode 100644 index 000000000..24e43d6f0 --- /dev/null +++ b/tests/Silverback.EventSourcing.EntityFrameworkCore.Tests/Silverback.EventSourcing.EntityFrameworkCore.Tests.csproj @@ -0,0 +1,30 @@ + + + + netcoreapp2.2 + Silverback.Tests.EventSourcing.EntityFrameworkCore + latest + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + diff --git a/tests/Silverback.EventSourcing.EntityFrameworkCore.Tests/TestTypes/Person.cs b/tests/Silverback.EventSourcing.EntityFrameworkCore.Tests/TestTypes/Person.cs new file mode 100644 index 000000000..218fa2981 --- /dev/null +++ b/tests/Silverback.EventSourcing.EntityFrameworkCore.Tests/TestTypes/Person.cs @@ -0,0 +1,65 @@ +// Copyright (c) 2018-2019 Sergio Aquilini +// This code is licensed under MIT license (see LICENSE file for details) + +using System.Collections.Generic; +using Silverback.Domain; + +namespace Silverback.Tests.EventSourcing.EntityFrameworkCore.TestTypes +{ + public class Person : EventSourcingDomainEntity + { + public class NameChangedEvent : EntityEvent { public string NewName { get; set; } } + public class AgeChangedEvent : EntityEvent { public int NewAge { get; set; } } + public class PhoneNumberChangedEvent : EntityEvent { public string NewPhoneNumber { get; set; } } + + public abstract class PersonDomainEvent { } + + public Person() + { + } + + public Person(int id) + { + Id = id; + } + + public Person(IEnumerable events) : base(events) + { + } + + public string Ssn { get; private set; } + + public string Name { get; private set; } + public int Age { get; private set; } + public string PhoneNumber { get; private set; } + + public void ChangeName(string newName) => + AddAndApplyEvent(new NameChangedEvent + { + NewName = newName + }); + + public void ChangeAge(int newAge) => + AddAndApplyEvent(new AgeChangedEvent + { + NewAge = newAge + }); + + public void ChangePhoneNumber(string newPhoneNumber) => + AddAndApplyEvent(new PhoneNumberChangedEvent + { + NewPhoneNumber = newPhoneNumber + }); + + private void Apply(NameChangedEvent @event) => Name = @event.NewName; + private void Apply(AgeChangedEvent @event) => Age = @event.NewAge; + + private void Apply(PhoneNumberChangedEvent @event, bool isReplaying) + { + PhoneNumber = @event.NewPhoneNumber; + + if (isReplaying) + PhoneNumber += "*"; + } + } +} \ No newline at end of file diff --git a/tests/Silverback.EventSourcing.EntityFrameworkCore.Tests/TestTypes/PersonEvent.cs b/tests/Silverback.EventSourcing.EntityFrameworkCore.Tests/TestTypes/PersonEvent.cs new file mode 100644 index 000000000..132c3aaab --- /dev/null +++ b/tests/Silverback.EventSourcing.EntityFrameworkCore.Tests/TestTypes/PersonEvent.cs @@ -0,0 +1,14 @@ +// Copyright (c) 2018-2019 Sergio Aquilini +// This code is licensed under MIT license (see LICENSE file for details) + +using System.ComponentModel.DataAnnotations; +using Silverback.EventStore; + +namespace Silverback.Tests.EventSourcing.EntityFrameworkCore.TestTypes +{ + public class PersonEvent : EventEntity + { + [Key] + public int Id { get; private set; } + } +} \ No newline at end of file diff --git a/tests/Silverback.EventSourcing.EntityFrameworkCore.Tests/TestTypes/PersonEventStore.cs b/tests/Silverback.EventSourcing.EntityFrameworkCore.Tests/TestTypes/PersonEventStore.cs new file mode 100644 index 000000000..064b84b1a --- /dev/null +++ b/tests/Silverback.EventSourcing.EntityFrameworkCore.Tests/TestTypes/PersonEventStore.cs @@ -0,0 +1,16 @@ +// Copyright (c) 2018-2019 Sergio Aquilini +// This code is licensed under MIT license (see LICENSE file for details) + +using System.ComponentModel.DataAnnotations; +using Silverback.EventStore; + +namespace Silverback.Tests.EventSourcing.EntityFrameworkCore.TestTypes +{ + public class PersonEventStore : EventStoreEntity + { + [Key] + public int Id { get; set; } + + public string Ssn { get; set; } + } +} \ No newline at end of file diff --git a/tests/Silverback.EventSourcing.EntityFrameworkCore.Tests/TestTypes/PersonEventStoreRepository.cs b/tests/Silverback.EventSourcing.EntityFrameworkCore.Tests/TestTypes/PersonEventStoreRepository.cs new file mode 100644 index 000000000..321041bc0 --- /dev/null +++ b/tests/Silverback.EventSourcing.EntityFrameworkCore.Tests/TestTypes/PersonEventStoreRepository.cs @@ -0,0 +1,22 @@ +// Copyright (c) 2018-2019 Sergio Aquilini +// This code is licensed under MIT license (see LICENSE file for details) + +using Microsoft.EntityFrameworkCore; +using Silverback.EventStore; + +namespace Silverback.Tests.EventSourcing.EntityFrameworkCore.TestTypes +{ + public class PersonEventStoreRepository : DbContextEventStoreRepository + { + public PersonEventStoreRepository(DbContext dbContext) : base(dbContext) + { + } + + protected override PersonEventStore GetNewEventStoreEntity(Person aggregateEntity) => + new PersonEventStore + { + Id = aggregateEntity.Id, + Ssn = aggregateEntity.Ssn + }; + } +} diff --git a/tests/Silverback.EventSourcing.EntityFrameworkCore.Tests/TestTypes/TestDbContext.cs b/tests/Silverback.EventSourcing.EntityFrameworkCore.Tests/TestTypes/TestDbContext.cs new file mode 100644 index 000000000..a474263d5 --- /dev/null +++ b/tests/Silverback.EventSourcing.EntityFrameworkCore.Tests/TestTypes/TestDbContext.cs @@ -0,0 +1,16 @@ +// Copyright (c) 2018-2019 Sergio Aquilini +// This code is licensed under MIT license (see LICENSE file for details) + +using Microsoft.EntityFrameworkCore; + +namespace Silverback.Tests.EventSourcing.EntityFrameworkCore.TestTypes +{ + public class TestDbContext : DbContext + { + public TestDbContext(DbContextOptions options) : base(options) + { + } + + public DbSet Persons { get; set; } + } +} diff --git a/tests/Silverback.EventSourcing.Tests/Domain/EventSourcingDomainEntityTests.cs b/tests/Silverback.EventSourcing.Tests/Domain/EventSourcingDomainEntityTests.cs new file mode 100644 index 000000000..3194ed3f9 --- /dev/null +++ b/tests/Silverback.EventSourcing.Tests/Domain/EventSourcingDomainEntityTests.cs @@ -0,0 +1,258 @@ +// Copyright (c) 2018-2019 Sergio Aquilini +// This code is licensed under MIT license (see LICENSE file for details) + +using System; +using System.Linq; +using FluentAssertions; +using Silverback.Domain; +using Silverback.Tests.EventSourcing.TestTypes; +using Xunit; + +namespace Silverback.Tests.EventSourcing.Domain +{ + public class EventSourcingDomainEntityTests + { + [Fact] + public void Constructor_PassingSomeEvents_EventsApplied() + { + var person = new Person(new IEntityEvent[] + { + new Person.NameChangedEvent {NewName = "Sergio"}, + new Person.AgeChangedEvent {NewAge = 35} + }); + + person.Name.Should().Be("Sergio"); + person.Age.Should().Be(35); + } + + [Fact] + public void Constructor_PassingSomeEvents_EventsAppliedAccordingToTimestamp() + { + var person = new Person(new IEntityEvent[] + { + new Person.NameChangedEvent {NewName = "Sergio", Timestamp = new DateTime(2001, 01, 02)}, + new Person.NameChangedEvent {NewName = "Silverback", Timestamp = new DateTime(2001, 01, 01)} + }); + + person.Name.Should().Be("Sergio"); + } + + [Fact] + public void Constructor_PassingSomeEvents_EventsAppliedAccordingToTimestampAndSequence() + { + var person = new Person(new IEntityEvent[] + { + new Person.NameChangedEvent {NewName = "Sergio", Timestamp = new DateTime(2001, 01, 02), Sequence = 2}, + new Person.NameChangedEvent {NewName = "Mario", Timestamp = new DateTime(2001, 01, 02), Sequence = 1}, + new Person.NameChangedEvent {NewName = "Silverback", Timestamp = new DateTime(2001, 01, 01), Sequence = 2} + }); + + person.Name.Should().Be("Sergio"); + } + + + [Fact] + public void GetNewEvents_WithNewAndOldEvents_OnlyNewEventsReturned() + { + var person = new Person(new IEntityEvent[] + { + new Person.NameChangedEvent {NewName = "Sergio"}, + new Person.AgeChangedEvent {NewAge = 35} + }); + + person.ChangePhoneNumber("123456"); + + person.GetNewEvents().Should().HaveCount(1); + } + + [Fact] + public void Constructor_PassingAnEvent_IsReplayingCorrectlySetToTrue() + { + var person = new Person(new IEntityEvent[] + { + new Person.PhoneNumberChangedEvent {NewPhoneNumber = "123456"} + }); + + person.PhoneNumber.Should().Be("123456*"); + } + + [Fact] + public void AddAndApplyEvent_WhateverEvent_IsReplayingCorrectlySetToFalse() + { + var person = new Person(); + + person.ChangePhoneNumber("123456"); + + person.PhoneNumber.Should().Be("123456"); + } + + [Fact] + public void AddAndApplyEvent_SomeEvents_EventsTimestampIsSet() + { + var now = DateTime.UtcNow; + var person = new Person(); + + person.ChangePhoneNumber("1"); + person.ChangePhoneNumber("2"); + person.ChangePhoneNumber("3"); + + person.GetNewEvents().Count().Should().Be(3); + person.GetNewEvents().Select(e => e.Timestamp).ToList().ForEach(t => t.Should().BeAfter(now)); + } + + [Fact] + public void AddAndApplyEvent_SomeEvents_EventsSequenceIsSet() + { + var person = new Person(); + + person.ChangePhoneNumber("1"); + person.ChangePhoneNumber("2"); + person.ChangePhoneNumber("3"); + + person.GetNewEvents().Select(e => e.Sequence).Should().BeEquivalentTo(1, 2, 3); + } + + [Fact] + public void AddAndApplyEvent_EventsFromThePast_EventsTimestampIsPreserved() + { + var now = DateTime.UtcNow; + var person = new Person(); + + person.MergeEvents(new IEntityEvent[] + { + new Person.PhoneNumberChangedEvent {NewPhoneNumber = "1", Timestamp = DateTime.Now.AddDays(-3)}, + new Person.PhoneNumberChangedEvent {NewPhoneNumber = "2", Timestamp = DateTime.Now.AddDays(-2)}, + new Person.PhoneNumberChangedEvent {NewPhoneNumber = "3", Timestamp = DateTime.Now.AddDays(-1)} + }); + + person.GetNewEvents().Count().Should().Be(3); + person.GetNewEvents().Select(e => e.Timestamp).ToList().ForEach(t => t.Should().BeBefore(now)); + } + + [Fact] + public void AddAndApplyEvent_EventsFromThePast_EventsSequenceIsPreserved() + { + var person = new Person(); + + person.MergeEvents(new IEntityEvent[] + { + new Person.PhoneNumberChangedEvent {NewPhoneNumber = "1", Sequence = 100}, + new Person.PhoneNumberChangedEvent {NewPhoneNumber = "2", Sequence = 101}, + new Person.PhoneNumberChangedEvent {NewPhoneNumber = "3", Sequence = 102} + }); + + person.GetNewEvents().Select(e => e.Sequence).Should().BeEquivalentTo(100, 101, 102); + } + + [Fact] + public void AddAndApplyEvent_MergingEventsFromThePast_CorrectSequenceIsRecognizable() + { + var person = new Person(new IEntityEvent[] + { + new Person.NameChangedEvent {NewName = "1", Timestamp = DateTime.Today.AddDays(-10)}, + new Person.NameChangedEvent {NewName = "2", Timestamp = DateTime.Today.AddDays(-8)}, + new Person.NameChangedEvent {NewName = "3", Timestamp = DateTime.Today.AddDays(-5)} + }); + + person.MergeEvents(new IEntityEvent[] + { + new Person.NameChangedEvent {NewName = "4", Timestamp = DateTime.Today.AddDays(-9)}, + new Person.NameChangedEvent {NewName = "5", Timestamp = DateTime.Today.AddDays(-7)}, + new Person.NameChangedEvent {NewName = "6", Timestamp = DateTime.Today.AddDays(-6)} + }); + + person.Name.Should().Be("3"); + } + + [Fact] + public void AddAndApplyEvent_MergingEventsFromThePast_ConcurrencyResolvedConsistently() + { + var person = new Person(new IEntityEvent[] + { + new Person.NameChangedEvent {NewName = "1", Timestamp = DateTime.Today.AddDays(-10)}, + new Person.NameChangedEvent {NewName = "2", Timestamp = DateTime.Today.AddDays(-5)}, + new Person.NameChangedEvent {NewName = "3", Timestamp = DateTime.Today.AddDays(-5)}, + new Person.NameChangedEvent {NewName = "4", Timestamp = DateTime.Today.AddDays(-9)}, + }); + + person.MergeEvents(new IEntityEvent[] + { + new Person.NameChangedEvent {NewName = "5", Timestamp = DateTime.Today.AddDays(-5)}, + new Person.NameChangedEvent {NewName = "6", Timestamp = DateTime.Today.AddDays(-5)}, + new Person.NameChangedEvent {NewName = "7", Timestamp = DateTime.Today.AddDays(-7)} + }); + + person.Name.Should().Be("6"); + + var person2 = new Person(person.Events); + + person2.Name.Should().Be("6"); + } + + [Fact] + public void AddAndApplyEvent_SomeEventsAppendedToOldEvents_EventsSequenceIsSet() + { + var person = new Person(new IEntityEvent[] + { + new Person.NameChangedEvent {NewName = "Sergio"}, + new Person.PhoneNumberChangedEvent {NewPhoneNumber = "123456"} + }); + + person.ChangePhoneNumber("3"); + person.ChangePhoneNumber("4"); + person.ChangePhoneNumber("5"); + + person.GetNewEvents().Select(e => e.Sequence).Should().BeEquivalentTo(3, 4, 5); + } + + [Fact] + public void GetNewEvents_SomeNewEventsApplied_NewEventsReturned() + { + var person = new Person(); + + person.ChangePhoneNumber("1"); + person.ChangePhoneNumber("2"); + person.ChangePhoneNumber("3"); + + person.GetNewEvents().Should().HaveCount(3); + } + + [Fact] + public void GetNewEvents_SomeNewEventsApplied_OnlyNewEventsReturned() + { + var person = new Person(new IEntityEvent[] + { + new Person.NameChangedEvent {NewName = "Sergio"}, + new Person.PhoneNumberChangedEvent {NewPhoneNumber = "123456"} + }); + + person.ChangePhoneNumber("1"); + person.ChangePhoneNumber("2"); + person.ChangePhoneNumber("3"); + + person.GetNewEvents().Should().HaveCount(3); + } + + [Fact] + public void GetNewEvents_SomeNewEventsFromThePastApplied_NewEventsReturned() + { + var person = new Person(new IEntityEvent[] + { + new Person.NameChangedEvent {NewName = "Sergio"}, + new Person.PhoneNumberChangedEvent {NewPhoneNumber = "123456"} + }); + + person.MergeEvents(new IEntityEvent[] + { + new Person.PhoneNumberChangedEvent {NewPhoneNumber = "1", Timestamp = DateTime.Now.AddDays(-3)}, + new Person.PhoneNumberChangedEvent {NewPhoneNumber = "2", Timestamp = DateTime.Now.AddDays(-2)}, + new Person.PhoneNumberChangedEvent {NewPhoneNumber = "3", Timestamp = DateTime.Now.AddDays(-1)} + }); + + person.GetNewEvents().Should().HaveCount(3); + } + } +} + + + \ No newline at end of file diff --git a/tests/Silverback.EventSourcing.Tests/Domain/Util/EntityActivatorTests.cs b/tests/Silverback.EventSourcing.Tests/Domain/Util/EntityActivatorTests.cs new file mode 100644 index 000000000..235efa624 --- /dev/null +++ b/tests/Silverback.EventSourcing.Tests/Domain/Util/EntityActivatorTests.cs @@ -0,0 +1,68 @@ +// Copyright (c) 2018-2019 Sergio Aquilini +// This code is licensed under MIT license (see LICENSE file for details) + +using FluentAssertions; +using Silverback.Domain; +using Silverback.Domain.Util; +using Silverback.Tests.EventSourcing.TestTypes; +using Xunit; + +namespace Silverback.Tests.EventSourcing.Domain.Util +{ + public class EntityActivatorTests + { + [Fact] + public void CreateInstance_WithSomeEvents_EntityCreated() + { + var events = new IEntityEvent[] { new Person.NameChangedEvent(), new Person.AgeChangedEvent() }; + var eventStoreEntity = new { }; + + var entity = EntityActivator.CreateInstance(events, eventStoreEntity); + + entity.Should().NotBeNull(); + entity.Should().BeOfType(); + } + + [Fact] + public void CreateInstance_WithSomeEvents_EventsApplied() + { + var events = new IEntityEvent[] + { + new Person.NameChangedEvent { NewName = "Silverback" }, + new Person.AgeChangedEvent { NewAge = 13 } + }; + var eventStoreEntity = new { }; + + var entity = EntityActivator.CreateInstance(events, eventStoreEntity); + + entity.Name.Should().Be("Silverback"); + entity.Age.Should().Be(13); + } + + [Fact] + public void CreateInstance_WithoutEvents_EntityCreated() + { + var events = new IEntityEvent[0]; + var eventStoreEntity = new { }; + + var entity = EntityActivator.CreateInstance(events, eventStoreEntity); + + entity.Should().NotBeNull(); + entity.Should().BeOfType(); + } + + [Fact] + public void CreateInstance_WithEventStoreEntity_PropertiesValuesCopiedToNewEntity() + { + var events = new IEntityEvent[0]; + var eventStoreEntity = new { PersonId = 1234, Ssn = "123-123 CA", EntityName = "Silverback" }; + + var entity = EntityActivator.CreateInstance(events, eventStoreEntity); + + entity.Should().NotBeNull(); + entity.Id.Should().Be(1234); + entity.Ssn.Should().Be("123-123 CA"); + entity.Name.Should().Be("Silverback"); + } + } +} \ No newline at end of file diff --git a/tests/Silverback.EventSourcing.Tests/Domain/Util/EntityReflectionHelperTests.cs b/tests/Silverback.EventSourcing.Tests/Domain/Util/EntityReflectionHelperTests.cs new file mode 100644 index 000000000..33f713185 --- /dev/null +++ b/tests/Silverback.EventSourcing.Tests/Domain/Util/EntityReflectionHelperTests.cs @@ -0,0 +1,81 @@ +// Copyright (c) 2018-2019 Sergio Aquilini +// This code is licensed under MIT license (see LICENSE file for details) + +using System; +using FluentAssertions; +using Silverback.Domain; +using Silverback.Domain.Util; +using Silverback.Tests.EventSourcing.TestTypes; +using Xunit; + +namespace Silverback.Tests.EventSourcing.Domain.Util +{ + public class EventsApplierTests + { + [Fact] + public void Apply_SingleMatchingMethod_MethodInvoked() + { + var entity = new Person(); + + EventsApplier.Apply(new Person.NameChangedEvent { NewName = "Silverback" }, entity); + + entity.Name.Should().Be("Silverback"); + } + + [Fact] + public void Apply_MultipleMatchingMethods_AllMethodsInvoked() + { + var entity = new TestEntity(); + + EventsApplier.Apply(new TestEntity.TestEntityEvent2(), entity); + + entity.Calls.Should().Be(2); + } + + [Fact] + public void Apply_PublicApplyMethod_MethodInvoked() + { + var entity = new TestEntity(); + + EventsApplier.Apply(new TestEntity.TestEntityEvent1(), entity); + + entity.Calls.Should().Be(1); + } + + [Fact] + public void Apply_PrivateApplyMethods_MethodsInvoked() + { + var entity = new TestEntity(); + + EventsApplier.Apply(new TestEntity.TestEntityEvent2(), entity); + + entity.Calls.Should().Be(2); + } + + + [Fact] + public void Apply_NoMatchingMethod_ExceptionThrown() + { + var entity = new TestEntity(); + + Action action = () => EventsApplier.Apply(new TestEntity.TestEntityEvent3(), entity); + + action.Should().Throw(); + } + + private class TestEntity : EventSourcingDomainEntity + { + public abstract class TestEntityEvent : EntityEvent { } + public class TestEntityEvent1 : TestEntityEvent { } + public class TestEntityEvent2 : TestEntityEvent { } + public class TestEntityEvent3 : TestEntityEvent { } + + public int Calls { get; private set; } = 0; + + public void Apply(TestEntityEvent1 event1) => Calls++; + + protected void Apply(TestEntityEvent2 event2) => Calls++; + private void Apply2(TestEntityEvent2 event2, bool isReplaying) => Calls++; + } + } +} diff --git a/tests/Silverback.EventSourcing.Tests/Domain/Util/PropertiesMapperTests.cs b/tests/Silverback.EventSourcing.Tests/Domain/Util/PropertiesMapperTests.cs new file mode 100644 index 000000000..8373132c4 --- /dev/null +++ b/tests/Silverback.EventSourcing.Tests/Domain/Util/PropertiesMapperTests.cs @@ -0,0 +1,59 @@ +// Copyright (c) 2018-2019 Sergio Aquilini +// This code is licensed under MIT license (see LICENSE file for details) + +using System; +using FluentAssertions; +using Silverback.Domain.Util; +using Xunit; + +namespace Silverback.Tests.EventSourcing.Domain.Util +{ + public class PropertiesMapperTests + { + [Fact] + public void Map_MatchingNames_PropertiesValuesCopied() + { + var source = new {Id = 123, Title = "Silverback for Dummies", Published = DateTime.Today, Pages = 13}; + var dest = new Book(); + + PropertiesMapper.Map(source, dest); + + dest.Id.Should().Be(123); + dest.Title.Should().Be("Silverback for Dummies"); + dest.Published.Should().Be(DateTime.Today); + dest.Pages.Should().Be(13); + } + + [Fact] + public void Map_PrefixedNames_PropertiesValuesCopied() + { + var source = new { EntityId = 123, BookTitle = "Silverback for Dummies" }; + var dest = new Book(); + + PropertiesMapper.Map(source, dest); + + dest.Id.Should().Be(123); + dest.Title.Should().Be("Silverback for Dummies"); + } + + [Fact] + public void Map_NonMatchingNames_PropertiesValuesNotCopied() + { + var source = new { Key = 123, Price = 13.5 }; + var dest = new Book(); + + Action act = () => PropertiesMapper.Map(source, dest); + + act.Should().NotThrow(); + } + + private class Book + { + public int Id { get; private set; } + public string Title { get; private set; } + public string Author { get; private set; } + public DateTime Published { get; set; } + public int? Pages { get; set; } + } + } +} \ No newline at end of file diff --git a/tests/Silverback.EventSourcing.Tests/EventStore/EventStoreRepositoryTests.cs b/tests/Silverback.EventSourcing.Tests/EventStore/EventStoreRepositoryTests.cs new file mode 100644 index 000000000..587faeef6 --- /dev/null +++ b/tests/Silverback.EventSourcing.Tests/EventStore/EventStoreRepositoryTests.cs @@ -0,0 +1,437 @@ +// Copyright (c) 2018-2019 Sergio Aquilini +// This code is licensed under MIT license (see LICENSE file for details) + +using System; +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using Silverback.Tests.EventSourcing.TestTypes; +using Xunit; + +namespace Silverback.Tests.EventSourcing.EventStore +{ + public class EventStoreRepositoryTests + { + #region Store (Basics) + + [Fact] + public void Store_EntityWithSomeEvents_EventsSaved() + { + var repo = new PersonEventStoreRepository(); + var person = new Person(); + person.ChangeName("Sergio"); + person.ChangeAge(35); + + repo.Store(person); + + repo.EventStores.Count.Should().Be(1); + repo.EventStores.First().Events.Count.Should().Be(2); + } + + [Fact] + public async Task StoreAsync_EntityWithSomeEvents_EventsSaved() + { + var repo = new PersonEventStoreRepository(); + var person = new Person(); + person.ChangeName("Sergio"); + person.ChangeAge(35); + + await repo.StoreAsync(person); + + repo.EventStores.Count.Should().Be(1); + repo.EventStores.First().Events.Count.Should().Be(2); + } + + [Fact] + public void Store_ExistingEntity_NewEventsSaved() + { + var eventStore = new PersonEventStore { PersonId = 12, EntityVersion = 1 }; + eventStore.Events.Add(new PersonEventStore.PersonEvent + { + SerializedEvent = "{" + + "\"$type\": \"Silverback.Tests.EventSourcing.TestTypes.Person+NameChangedEvent, Silverback.EventSourcing.Tests\"," + + "\"NewName\": \"Silverback\"" + + "}" + }); + + var repo = new PersonEventStoreRepository(eventStore); + + var person = repo.GetById(12); + + person.ChangeName("Sergio"); + person.ChangeAge(35); + + repo.Store(person); + + repo.EventStores.Count.Should().Be(1); + repo.EventStores.First().Events.Count.Should().Be(3); + } + + [Fact] + public async Task StoreAsync_ExistingEntity_NewEventsSaved() + { + var eventStore = new PersonEventStore { PersonId = 12, EntityVersion = 1 }; + eventStore.Events.Add(new PersonEventStore.PersonEvent + { + SerializedEvent = "{" + + "\"$type\": \"Silverback.Tests.EventSourcing.TestTypes.Person+NameChangedEvent, Silverback.EventSourcing.Tests\"," + + "\"NewName\": \"Silverback\"" + + "}" + }); + + var repo = new PersonEventStoreRepository(eventStore); + + var person = repo.GetById(12); + + person.ChangeName("Sergio"); + person.ChangeAge(35); + + await repo.StoreAsync(person); + + repo.EventStores.Count.Should().Be(1); + repo.EventStores.First().Events.Count.Should().Be(3); + } + + #endregion + + #region Store (Concurrency) + + [Fact] + public void Store_EntityWithSomeEvents_VersionCalculated() + { + var repo = new PersonEventStoreRepository(); + var person = new Person(); + person.ChangeName("Sergio"); + person.ChangeAge(35); + + repo.Store(person); + + repo.EventStores.First().EntityVersion.Should().Be(2); + } + + [Fact] + public async Task StoreAsync_EntityWithSomeEvents_VersionCalculated() + { + var repo = new PersonEventStoreRepository(); + var person = new Person(); + person.ChangeName("Sergio"); + person.ChangeAge(35); + + await repo.StoreAsync(person); + + repo.EventStores.First().EntityVersion.Should().Be(2); + } + + [Fact] + public void Store_ExistingEntity_VersionIncremented() + { + var eventStore = new PersonEventStore { PersonId = 12, EntityVersion = 1 }; + eventStore.Events.Add(new PersonEventStore.PersonEvent + { + SerializedEvent = "{" + + "\"$type\": \"Silverback.Tests.EventSourcing.TestTypes.Person+NameChangedEvent, Silverback.EventSourcing.Tests\"," + + "\"NewName\": \"Silverback\"" + + "}" + }); + + var repo = new PersonEventStoreRepository(eventStore); + + var person = repo.GetById(12); + + person.ChangeName("Sergio"); + person.ChangeAge(35); + + repo.Store(person); + + repo.EventStores.First().EntityVersion.Should().Be(3); + } + + [Fact] + public async Task StoreAsync_ExistingEntity_VersionIncremented() + { + var eventStore = new PersonEventStore { PersonId = 12, EntityVersion = 1 }; + eventStore.Events.Add(new PersonEventStore.PersonEvent + { + SerializedEvent = "{" + + "\"$type\": \"Silverback.Tests.EventSourcing.TestTypes.Person+NameChangedEvent, Silverback.EventSourcing.Tests\"," + + "\"NewName\": \"Silverback\"" + + "}" + }); + + var repo = new PersonEventStoreRepository(eventStore); + + var person = repo.GetById(12); + + person.ChangeName("Sergio"); + person.ChangeAge(35); + + await repo.StoreAsync(person); + + repo.EventStores.First().EntityVersion.Should().Be(3); + } + + [Fact] + public void Store_ConcurrentlyModifyExistingEntity_ExceptionThrown() + { + var eventStore = new PersonEventStore { PersonId = 12, EntityVersion = 1 }; + eventStore.Events.Add(new PersonEventStore.PersonEvent + { + SerializedEvent = "{" + + "\"$type\": \"Silverback.Tests.EventSourcing.TestTypes.Person+NameChangedEvent, Silverback.EventSourcing.Tests\"," + + "\"NewName\": \"Silverback\"" + + "}" + }); + + var repo = new PersonEventStoreRepository(eventStore); + + var person = repo.GetById(12); + var person2 = repo.GetById(12); + + person.ChangeName("Sergio"); + person.ChangeAge(35); + person2.ChangeName("Sergio"); + person2.ChangeAge(35); + + repo.Store(person); + Action act = () => repo.Store(person2); + + act.Should().Throw(); + } + + [Fact] + public async Task StoreAsync_ConcurrentlyModifyExistingEntity_ExceptionThrown() + { + var eventStore = new PersonEventStore { PersonId = 12, EntityVersion = 1 }; + eventStore.Events.Add(new PersonEventStore.PersonEvent + { + SerializedEvent = "{" + + "\"$type\": \"Silverback.Tests.EventSourcing.TestTypes.Person+NameChangedEvent, Silverback.EventSourcing.Tests\"," + + "\"NewName\": \"Silverback\"" + + "}" + }); + + var repo = new PersonEventStoreRepository(eventStore); + + var person = repo.GetById(12); + var person2 = repo.GetById(12); + + person.ChangeName("Sergio"); + person.ChangeAge(35); + person2.ChangeName("Sergio"); + person2.ChangeAge(35); + + await repo.StoreAsync(person); + Func act = async () => await repo.StoreAsync(person2); + + act.Should().Throw(); + } + + #endregion + + #region GetAggregateEntity + + [Fact] + public void GetAggregateEntity_ExistingId_EntityRecreated() + { + var eventStore = new PersonEventStore{PersonId = 12}; + eventStore.Events.Add(new PersonEventStore.PersonEvent + { + SerializedEvent = "{" + + "\"$type\": \"Silverback.Tests.EventSourcing.TestTypes.Person+NameChangedEvent, Silverback.EventSourcing.Tests\"," + + "\"NewName\": \"Silverback\"" + + "}" + }); + + var repo = new PersonEventStoreRepository(eventStore); + + var entity = repo.GetById(12); + + entity.Should().NotBe(null); + } + + [Fact] + public void GetAggregateEntity_ExistingId_EventsApplied() + { + var eventStore = new PersonEventStore { PersonId = 12 }; + eventStore.Events.Add(new PersonEventStore.PersonEvent + { + SerializedEvent = "{" + + "\"$type\": \"Silverback.Tests.EventSourcing.TestTypes.Person+NameChangedEvent, Silverback.EventSourcing.Tests\"," + + "\"NewName\": \"Silverback\"" + + "}" + }); + eventStore.Events.Add(new PersonEventStore.PersonEvent + { + SerializedEvent = "{" + + "\"$type\": \"Silverback.Tests.EventSourcing.TestTypes.Person+AgeChangedEvent, Silverback.EventSourcing.Tests\"," + + "\"NewAge\": 35" + + "}" + }); + + var repo = new PersonEventStoreRepository(eventStore); + + var entity = repo.GetById(12); + + entity.Name.Should().Be("Silverback"); + entity.Age.Should().Be(35); + } + + [Fact] + public void GetAggregateEntity_ExistingId_EventsAppliedInRightOrder() + { + var eventStore = new PersonEventStore { PersonId = 12 }; + eventStore.Events.Add(new PersonEventStore.PersonEvent + { + SerializedEvent = "{" + + "\"$type\": \"Silverback.Tests.EventSourcing.TestTypes.Person+NameChangedEvent, Silverback.EventSourcing.Tests\"," + + "\"NewName\": \"Silverback\"" + + "}", + Timestamp = DateTime.Parse("2000-05-05") + }); + eventStore.Events.Add(new PersonEventStore.PersonEvent + { + SerializedEvent = "{" + + "\"$type\": \"Silverback.Tests.EventSourcing.TestTypes.Person+NameChangedEvent, Silverback.EventSourcing.Tests\"," + + "\"NewName\": \"Sergio\"" + + "}", + Timestamp = DateTime.Parse("2000-03-01") + }); + var repo = new PersonEventStoreRepository(eventStore); + + var entity = repo.GetById(12); + + entity.Name.Should().Be("Silverback"); + } + + [Fact] + public void GetAggregateEntity_NonExistingId_NullReturned() + { + var repo = new PersonEventStoreRepository(); + + var entity = repo.GetById(11); + + entity.Should().BeNull(); + } + + [Fact] + public void GetAggregateEntity_ExistingIdWithPastSnapshot_OnlyRelevantEventsApplied() + { + var eventStore = new PersonEventStore { PersonId = 12 }; + eventStore.Events.Add(new PersonEventStore.PersonEvent + { + SerializedEvent = "{" + + "\"$type\": \"Silverback.Tests.EventSourcing.TestTypes.Person+NameChangedEvent, Silverback.EventSourcing.Tests\"," + + "\"NewName\": \"Silverback\"" + + "}", + Timestamp = DateTime.Parse("2000-05-05") + }); + eventStore.Events.Add(new PersonEventStore.PersonEvent + { + SerializedEvent = "{" + + "\"$type\": \"Silverback.Tests.EventSourcing.TestTypes.Person+NameChangedEvent, Silverback.EventSourcing.Tests\"," + + "\"NewName\": \"Sergio\"" + + "}", + Timestamp = DateTime.Parse("2000-03-01") + }); + eventStore.Events.Add(new PersonEventStore.PersonEvent + { + SerializedEvent = "{" + + "\"$type\": \"Silverback.Tests.EventSourcing.TestTypes.Person+AgeChangedEvent, Silverback.EventSourcing.Tests\"," + + "\"NewAge\": 16" + + "}", + Timestamp = DateTime.Parse("2000-02-01") + }); + eventStore.Events.Add(new PersonEventStore.PersonEvent + { + SerializedEvent = "{" + + "\"$type\": \"Silverback.Tests.EventSourcing.TestTypes.Person+AgeChangedEvent, Silverback.EventSourcing.Tests\"," + + "\"NewAge\": 35" + + "}", + Timestamp = DateTime.Parse("2019-07-06") + }); + + var repo = new PersonEventStoreRepository(eventStore); + + var entity = repo.GetSnapshotById(12, DateTime.Parse("2000-03-01")); + + entity.Name.Should().Be("Sergio"); + entity.Age.Should().Be(16); + } + + #endregion + + #region Remove + + [Fact] + public void Remove_ExistingEntity_EntityDeleted() + { + var eventStore = new PersonEventStore { PersonId = 12 }; + eventStore.Events.Add(new PersonEventStore.PersonEvent + { + SerializedEvent = "{" + + "\"$type\": \"Silverback.Tests.EventSourcing.TestTypes.Person+NameChangedEvent, Silverback.EventSourcing.Tests\"," + + "\"NewName\": \"Silverback\"" + + "}" + }); + + var repo = new PersonEventStoreRepository(eventStore); + + var entity = repo.GetById(12); + entity.Should().NotBeNull(); + + repo.Remove(entity); + + repo.EventStores.Count.Should().Be(0); + repo.EventStores.SelectMany(s => s.Events).Count().Should().Be(0); + } + + [Fact] + public async Task RemoveAsync_ExistingEntity_EntityDeleted() + { + var eventStore = new PersonEventStore { PersonId = 12 }; + eventStore.Events.Add(new PersonEventStore.PersonEvent + { + SerializedEvent = "{" + + "\"$type\": \"Silverback.Tests.EventSourcing.TestTypes.Person+NameChangedEvent, Silverback.EventSourcing.Tests\"," + + "\"NewName\": \"Silverback\"" + + "}" + }); + + var repo = new PersonEventStoreRepository(eventStore); + + var entity = repo.GetById(12); + entity.Should().NotBeNull(); + + await repo.RemoveAsync(entity); + + repo.EventStores.Count.Should().Be(0); + repo.EventStores.SelectMany(s => s.Events).Count().Should().Be(0); + } + + [Fact] + public void Remove_NonExistingEntity_ReturnsNull() + { + var repo = new PersonEventStoreRepository(); + + var entity = new Person(123); + + var result= repo.Remove(entity); + + result.Should().BeNull(); + } + + [Fact] + public async Task RemoveAsync_NonExistingEntity_ReturnsNull() + { + var repo = new PersonEventStoreRepository(); + + var entity = new Person(123); + + var result = await repo.RemoveAsync(entity); + + result.Should().BeNull(); + } + + #endregion + } +} \ No newline at end of file diff --git a/tests/Silverback.EventSourcing.Tests/Silverback.EventSourcing.Tests.csproj b/tests/Silverback.EventSourcing.Tests/Silverback.EventSourcing.Tests.csproj new file mode 100644 index 000000000..3c1c8a5e4 --- /dev/null +++ b/tests/Silverback.EventSourcing.Tests/Silverback.EventSourcing.Tests.csproj @@ -0,0 +1,29 @@ + + + + netcoreapp2.2 + Silverback.Tests.EventSourcing + latest + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + diff --git a/tests/Silverback.EventSourcing.Tests/TestTypes/InMemoryEventStore.cs b/tests/Silverback.EventSourcing.Tests/TestTypes/InMemoryEventStore.cs new file mode 100644 index 000000000..fa4df7560 --- /dev/null +++ b/tests/Silverback.EventSourcing.Tests/TestTypes/InMemoryEventStore.cs @@ -0,0 +1,25 @@ +// Copyright (c) 2018-2019 Sergio Aquilini +// This code is licensed under MIT license (see LICENSE file for details) + +using System.Collections.Generic; +using Silverback.EventStore; + +namespace Silverback.Tests.EventSourcing.TestTypes +{ + public abstract class InMemoryEventStoreRepository + : EventStoreRepository + where TAggregateEntity : IEventSourcingAggregate + where TEventStoreEntity : IEventStoreEntity + where TEventEntity : IEventEntity, new() + { + public readonly List EventStores = new List(); + + protected override TEventStoreEntity Remove(TAggregateEntity aggregateEntity, TEventStoreEntity eventStore) + { + if (eventStore != null) + EventStores.Remove(eventStore); + + return eventStore; + } + } +} diff --git a/tests/Silverback.EventSourcing.Tests/TestTypes/Person.cs b/tests/Silverback.EventSourcing.Tests/TestTypes/Person.cs new file mode 100644 index 000000000..e7ff0565b --- /dev/null +++ b/tests/Silverback.EventSourcing.Tests/TestTypes/Person.cs @@ -0,0 +1,77 @@ +// Copyright (c) 2018-2019 Sergio Aquilini +// This code is licensed under MIT license (see LICENSE file for details) + +using System.Collections.Generic; +using System.Linq; +using Silverback.Domain; + +namespace Silverback.Tests.EventSourcing.TestTypes +{ + public class Person : EventSourcingDomainEntity + { + public class NameChangedEvent : EntityEvent { public string NewName { get; set; } } + public class AgeChangedEvent : EntityEvent { public int NewAge { get; set; } } + public class PhoneNumberChangedEvent : EntityEvent { public string NewPhoneNumber { get; set; } } + + public abstract class PersonDomainEvent { } + + public Person() + { + } + + public Person(int id) + { + Id = id; + } + + public Person(IEnumerable events) : base(events) + { + } + + public string Ssn { get; private set; } + + public string Name { get; private set; } + public int Age { get; private set; } + public string PhoneNumber { get; private set; } + + public void ChangeName(string newName) => + AddAndApplyEvent(new NameChangedEvent + { + NewName = newName + }); + + public void ChangeAge(int newAge) => + AddAndApplyEvent(new AgeChangedEvent + { + NewAge = newAge + }); + + public void ChangePhoneNumber(string newPhoneNumber) => + AddAndApplyEvent(new PhoneNumberChangedEvent + { + NewPhoneNumber = newPhoneNumber + }); + + public IEnumerable MergeEvents(IEnumerable events) => + events.Select(AddAndApplyEvent).ToList(); + + private void Apply(NameChangedEvent @event, bool isReplaying) + { + // Skip if a newer event exists (just to show how it can be done) + if (!isReplaying && Events.Any(e => e is NameChangedEvent && e.Timestamp > @event.Timestamp)) + return; + + Name = @event.NewName; + } + + private void Apply(AgeChangedEvent @event) => Age = @event.NewAge; + + private void Apply(PhoneNumberChangedEvent @event, bool isReplaying) + { + PhoneNumber = @event.NewPhoneNumber; + + if (isReplaying) + PhoneNumber += "*"; + } + } +} \ No newline at end of file diff --git a/tests/Silverback.EventSourcing.Tests/TestTypes/PersonEventStore.cs b/tests/Silverback.EventSourcing.Tests/TestTypes/PersonEventStore.cs new file mode 100644 index 000000000..59fe74fef --- /dev/null +++ b/tests/Silverback.EventSourcing.Tests/TestTypes/PersonEventStore.cs @@ -0,0 +1,18 @@ +// Copyright (c) 2018-2019 Sergio Aquilini +// This code is licensed under MIT license (see LICENSE file for details) + +using Silverback.EventStore; + +namespace Silverback.Tests.EventSourcing.TestTypes +{ + public class PersonEventStore : EventStoreEntity + { + public int PersonId { get; set; } + + public string Ssn { get; set; } + + public class PersonEvent : EventEntity + { + } + } +} \ No newline at end of file diff --git a/tests/Silverback.EventSourcing.Tests/TestTypes/PersonEventStoreRepository.cs b/tests/Silverback.EventSourcing.Tests/TestTypes/PersonEventStoreRepository.cs new file mode 100644 index 000000000..4b1098823 --- /dev/null +++ b/tests/Silverback.EventSourcing.Tests/TestTypes/PersonEventStoreRepository.cs @@ -0,0 +1,49 @@ +// Copyright (c) 2018-2019 Sergio Aquilini +// This code is licensed under MIT license (see LICENSE file for details) + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Silverback.Tests.EventSourcing.TestTypes +{ + public class PersonEventStoreRepository : InMemoryEventStoreRepository + { + public PersonEventStoreRepository() + { + } + + public PersonEventStoreRepository(params PersonEventStore[] eventStoreEntities) + : this(eventStoreEntities.AsEnumerable()) + { + } + + public PersonEventStoreRepository(IEnumerable eventStoreEntities) + { + EventStores.AddRange(eventStoreEntities); + } + + public Person GetById(int id) => GetAggregateEntity(EventStores.FirstOrDefault(x => x.PersonId == id)); + + public Person GetBySsn(string ssn) => GetAggregateEntity(EventStores.FirstOrDefault(x => x.Ssn == ssn)); + + public Person GetSnapshotById(int id, DateTime snapshot) => GetAggregateEntity(EventStores.FirstOrDefault(x => x.PersonId == id), snapshot); + + protected override PersonEventStore GetEventStoreEntity(Person aggregateEntity, bool addIfNotFound) + { + var store = EventStores.FirstOrDefault(s => s.PersonId == aggregateEntity.Id); + + if (store == null && addIfNotFound) + { + store = new PersonEventStore {PersonId = aggregateEntity.Id, Ssn = aggregateEntity.Ssn}; + EventStores.Add(store); + } + + return store; + } + + protected override Task GetEventStoreEntityAsync(Person aggregateEntity, bool addIfNotFound) => + Task.FromResult(GetEventStoreEntity(aggregateEntity, addIfNotFound)); + } +} \ No newline at end of file diff --git a/tests/Silverback.Integration.Configuration.Tests/Messaging/Configuration/ConfigurationReaderTests.cs b/tests/Silverback.Integration.Configuration.Tests/Messaging/Configuration/ConfigurationReaderTests.cs index 824e8c96f..03406cb77 100644 --- a/tests/Silverback.Integration.Configuration.Tests/Messaging/Configuration/ConfigurationReaderTests.cs +++ b/tests/Silverback.Integration.Configuration.Tests/Messaging/Configuration/ConfigurationReaderTests.cs @@ -17,6 +17,7 @@ using Silverback.Messaging.Connectors; using Silverback.Messaging.ErrorHandling; using Silverback.Messaging.Messages; +using Silverback.Messaging.Publishing; using Silverback.Messaging.Serialization; using Silverback.Tests.Integration.Configuration.Types; using Xunit; @@ -34,10 +35,14 @@ public ConfigurationReaderTests() services.AddSingleton(typeof(ILogger<>), typeof(NullLogger<>)); services.AddSingleton(Substitute.For()); + services.AddScoped(s => Substitute.For()); services.AddSingleton(); services.AddSingleton(); - _serviceProvider = services.BuildServiceProvider(); + _serviceProvider = services.BuildServiceProvider(new ServiceProviderOptions + { + ValidateScopes = true + }); _builder = Substitute.For(); } @@ -221,8 +226,8 @@ public void Read_CompleteInbound_ErrorPolicyMaxFailedAttemptsSet() .Read(ConfigFileHelper.GetConfigSection("inbound.complete")); var policy = reader.Inbound.First().ErrorPolicies.First(); - policy.CanHandle(new FailedMessage(null, 3), new ArgumentException()).Should().BeTrue(); - policy.CanHandle(new FailedMessage(null, 6), new ArgumentException()).Should().BeFalse(); + policy.CanHandle(new InboundMessage { FailedAttempts = 3 }, new ArgumentException()).Should().BeTrue(); + policy.CanHandle(new InboundMessage { FailedAttempts = 6 }, new ArgumentException()).Should().BeFalse(); } [Fact] @@ -245,9 +250,9 @@ public void Read_CompleteInbound_ErrorPolicyApplyToSet() .Read(ConfigFileHelper.GetConfigSection("inbound.complete")); var policy = reader.Inbound.First().ErrorPolicies.First(); - policy.CanHandle(new FailedMessage(), new ArgumentException()).Should().BeTrue(); - policy.CanHandle(new FailedMessage(), new InvalidOperationException()).Should().BeTrue(); - policy.CanHandle(new FailedMessage(), new FormatException()).Should().BeFalse(); + policy.CanHandle(new InboundMessage(), new ArgumentException()).Should().BeTrue(); + policy.CanHandle(new InboundMessage(), new InvalidOperationException()).Should().BeTrue(); + policy.CanHandle(new InboundMessage(), new FormatException()).Should().BeFalse(); } [Fact] @@ -258,8 +263,8 @@ public void Read_CompleteInbound_ErrorPolicyExcludeSet() .Read(ConfigFileHelper.GetConfigSection("inbound.complete")); var policy = reader.Inbound.First().ErrorPolicies.First(); - policy.CanHandle(new FailedMessage(), new ArgumentException()).Should().BeTrue(); - policy.CanHandle(new FailedMessage(), new ArgumentNullException()).Should().BeFalse(); + policy.CanHandle(new InboundMessage(), new ArgumentException()).Should().BeTrue(); + policy.CanHandle(new InboundMessage(), new ArgumentNullException()).Should().BeFalse(); } [Fact] diff --git a/tests/Silverback.Integration.Configuration.Tests/Silverback.Integration.Configuration.Tests.csproj b/tests/Silverback.Integration.Configuration.Tests/Silverback.Integration.Configuration.Tests.csproj index bfe9017be..b7f221106 100644 --- a/tests/Silverback.Integration.Configuration.Tests/Silverback.Integration.Configuration.Tests.csproj +++ b/tests/Silverback.Integration.Configuration.Tests/Silverback.Integration.Configuration.Tests.csproj @@ -3,24 +3,26 @@ netcoreapp2.2 Silverback.Tests.Integration.Configuration - - - latest - + + + all + runtime; build; native; contentfiles; analyzers + - - + + + all runtime; build; native; contentfiles; analyzers - + diff --git a/tests/Silverback.Integration.Configuration.Tests/Types/FakeSerializerSettings.cs b/tests/Silverback.Integration.Configuration.Tests/Types/FakeSerializerSettings.cs index d96055c58..656cb5093 100644 --- a/tests/Silverback.Integration.Configuration.Tests/Types/FakeSerializerSettings.cs +++ b/tests/Silverback.Integration.Configuration.Tests/Types/FakeSerializerSettings.cs @@ -1,5 +1,6 @@ // Copyright (c) 2018-2019 Sergio Aquilini // This code is licensed under MIT license (see LICENSE file for details) + namespace Silverback.Tests.Integration.Configuration.Types { public class FakeSerializerSettings diff --git a/tests/Silverback.Integration.Configuration.Tests/Types/IIntegrationCommand.cs b/tests/Silverback.Integration.Configuration.Tests/Types/IIntegrationCommand.cs index d6eded9cc..769315fe3 100644 --- a/tests/Silverback.Integration.Configuration.Tests/Types/IIntegrationCommand.cs +++ b/tests/Silverback.Integration.Configuration.Tests/Types/IIntegrationCommand.cs @@ -1,5 +1,6 @@ // Copyright (c) 2018-2019 Sergio Aquilini // This code is licensed under MIT license (see LICENSE file for details) + namespace Silverback.Tests.Integration.Configuration.Types { public interface IIntegrationCommand diff --git a/tests/Silverback.Integration.Configuration.Tests/Types/IIntegrationEvent.cs b/tests/Silverback.Integration.Configuration.Tests/Types/IIntegrationEvent.cs index b24346c4b..f53cf7014 100644 --- a/tests/Silverback.Integration.Configuration.Tests/Types/IIntegrationEvent.cs +++ b/tests/Silverback.Integration.Configuration.Tests/Types/IIntegrationEvent.cs @@ -1,5 +1,6 @@ // Copyright (c) 2018-2019 Sergio Aquilini // This code is licensed under MIT license (see LICENSE file for details) + namespace Silverback.Tests.Integration.Configuration.Types { public interface IIntegrationEvent diff --git a/tests/Silverback.Integration.InMemory.Tests/Messaging/Broker/InMemoryBrokerTests.cs b/tests/Silverback.Integration.InMemory.Tests/Messaging/Broker/InMemoryBrokerTests.cs index d5f28ef2c..08d4fd388 100644 --- a/tests/Silverback.Integration.InMemory.Tests/Messaging/Broker/InMemoryBrokerTests.cs +++ b/tests/Silverback.Integration.InMemory.Tests/Messaging/Broker/InMemoryBrokerTests.cs @@ -1,7 +1,9 @@ -using System; +// Copyright (c) 2018-2019 Sergio Aquilini +// This code is licensed under MIT license (see LICENSE file for details) + +using System; using System.Collections.Generic; using System.Linq; -using System.Text; using FluentAssertions; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -31,7 +33,7 @@ public InMemoryBrokerTests() services.AddBus(); services.AddBroker(); - _serviceProvider = services.BuildServiceProvider(); + _serviceProvider = services.BuildServiceProvider(new ServiceProviderOptions { ValidateScopes = true }); } [Fact] @@ -65,7 +67,7 @@ public void InMemoryBroker_ProduceMessage_MessageConsumed() var broker = _serviceProvider.GetRequiredService(); var producer = broker.GetProducer(new KafkaProducerEndpoint(endpointName)); var consumer = broker.GetConsumer(new KafkaConsumerEndpoint(endpointName)); - consumer.Received += (_, e) => receivedMessages.Add(e.Message); + consumer.Received += (_, e) => receivedMessages.Add(e.Endpoint.Serializer.Deserialize(e.Message)); producer.Produce(new TestMessage { Content = "hello!" }); producer.Produce(new TestMessage { Content = "hello 2!" }); @@ -83,7 +85,7 @@ public void InMemoryBroker_ProduceMessage_MessageReceived() var broker = _serviceProvider.GetRequiredService(); var producer = broker.GetProducer(new KafkaProducerEndpoint(endpointName)); var consumer = broker.GetConsumer(new KafkaConsumerEndpoint(endpointName)); - consumer.Received += (_, e) => receivedMessages.Add(e.Message); + consumer.Received += (_, e) => receivedMessages.Add(e.Endpoint.Serializer.Deserialize(e.Message)); producer.Produce(new TestMessage { Content = "hello!" }); @@ -103,13 +105,12 @@ public void InMemoryBroker_ProduceMessage_MessageHeadersReceived() consumer.Received += (_, e) => receivedHeaders.Add(e.Headers); producer.Produce( - new TestMessage {Content = "hello!"}, - new[] {new MessageHeader("a", "b"), new MessageHeader("c", "d")}); + new TestMessage { Content = "hello!" }, + new[] { new MessageHeader("a", "b"), new MessageHeader("c", "d") }); receivedHeaders.First().Should().BeEquivalentTo(new MessageHeader("a", "b"), new MessageHeader("c", "d")); } - [Fact] public void InMemoryBroker_PublishMessageThroughConnector_MessageConsumed() { @@ -125,10 +126,13 @@ public void InMemoryBroker_PublishMessageThroughConnector_MessageConsumed() }) .AddOutbound(new KafkaProducerEndpoint(endpointName))); - var publisher = _serviceProvider.GetRequiredService(); + using (var scope = _serviceProvider.CreateScope()) + { + var publisher = scope.ServiceProvider.GetRequiredService(); - publisher.Publish(new TestMessage { Content = "hello!" }); - publisher.Publish(new TestMessage { Content = "hello 2!" }); + publisher.Publish(new TestMessage { Content = "hello!" }); + publisher.Publish(new TestMessage { Content = "hello 2!" }); + } receivedMessages.Count.Should().Be(2); receivedMessages.OfType().Select(x => x.Message).Should().AllBeOfType(); diff --git a/tests/Silverback.Integration.InMemory.Tests/Silverback.Integration.InMemory.Tests.csproj b/tests/Silverback.Integration.InMemory.Tests/Silverback.Integration.InMemory.Tests.csproj index 3319b27aa..8ae436073 100644 --- a/tests/Silverback.Integration.InMemory.Tests/Silverback.Integration.InMemory.Tests.csproj +++ b/tests/Silverback.Integration.InMemory.Tests/Silverback.Integration.InMemory.Tests.csproj @@ -1,16 +1,22 @@  - netcoreapp2.0 + netcoreapp2.2 Silverback.Tests.Integration.InMemory + latest - - - - - + + all + runtime; build; native; contentfiles; analyzers + + + + + + + all diff --git a/tests/Silverback.Integration.Kafka.TestConsumer/Program.cs b/tests/Silverback.Integration.Kafka.TestConsumer/Program.cs index a93179bfc..838361ec6 100644 --- a/tests/Silverback.Integration.Kafka.TestConsumer/Program.cs +++ b/tests/Silverback.Integration.Kafka.TestConsumer/Program.cs @@ -56,7 +56,7 @@ private static void Disconnect() private static void OnMessageReceived(object sender, MessageReceivedEventArgs args) { - var testMessage = args.Message as TestMessage; + var testMessage = args.Endpoint.Serializer.Deserialize(args.Message) as TestMessage; if (testMessage == null) { diff --git a/tests/Silverback.Integration.Kafka.TestConsumer/Silverback.Integration.Kafka.TestConsumer.csproj b/tests/Silverback.Integration.Kafka.TestConsumer/Silverback.Integration.Kafka.TestConsumer.csproj index cdffd3fc1..9a5da7053 100644 --- a/tests/Silverback.Integration.Kafka.TestConsumer/Silverback.Integration.Kafka.TestConsumer.csproj +++ b/tests/Silverback.Integration.Kafka.TestConsumer/Silverback.Integration.Kafka.TestConsumer.csproj @@ -2,7 +2,8 @@ Exe - netcoreapp2.0 + netcoreapp2.2 + latest @@ -16,23 +17,15 @@ - - - - + + + + + - - - ..\..\..\..\..\Users\Fabio\.nuget\packages\silverback.integration\1.0.15\lib\netstandard2.0\Silverback.Integration.dll - - - ..\..\..\..\..\Program Files\dotnet\sdk\NuGetFallbackFolder\microsoft.netcore.app\2.0.0\ref\netcoreapp2.0\System.Collections.dll - - - diff --git a/tests/Silverback.Integration.Kafka.TestProducer/Silverback.Integration.Kafka.TestProducer.csproj b/tests/Silverback.Integration.Kafka.TestProducer/Silverback.Integration.Kafka.TestProducer.csproj index 6ff6f7eb3..5b80f792f 100644 --- a/tests/Silverback.Integration.Kafka.TestProducer/Silverback.Integration.Kafka.TestProducer.csproj +++ b/tests/Silverback.Integration.Kafka.TestProducer/Silverback.Integration.Kafka.TestProducer.csproj @@ -1,8 +1,8 @@ - + Exe - netcoreapp2.0 + netcoreapp2.2 @@ -16,10 +16,11 @@ - - - - + + + + + diff --git a/tests/Silverback.Integration.Kafka.Tests/Silverback.Integration.Kafka.Tests.csproj b/tests/Silverback.Integration.Kafka.Tests/Silverback.Integration.Kafka.Tests.csproj index 74fac2f8c..d567105a4 100644 --- a/tests/Silverback.Integration.Kafka.Tests/Silverback.Integration.Kafka.Tests.csproj +++ b/tests/Silverback.Integration.Kafka.Tests/Silverback.Integration.Kafka.Tests.csproj @@ -1,15 +1,20 @@  - netcoreapp2.0 + netcoreapp2.2 Silverback.Tests.Integration.Kafka - - - - + + + all + runtime; build; native; contentfiles; analyzers + + + + + all diff --git a/tests/Silverback.Integration.Tests/Messaging/Configuration/DependencyInjectionExtensionsTests.cs b/tests/Silverback.Integration.Tests/Messaging/Configuration/DependencyInjectionExtensionsTests.cs index ecb62187f..60bb1be1d 100644 --- a/tests/Silverback.Integration.Tests/Messaging/Configuration/DependencyInjectionExtensionsTests.cs +++ b/tests/Silverback.Integration.Tests/Messaging/Configuration/DependencyInjectionExtensionsTests.cs @@ -9,7 +9,6 @@ using Microsoft.Extensions.Logging.Abstractions; using Silverback.Messaging.Broker; using Silverback.Messaging.Configuration; -using Silverback.Messaging.Connectors; using Silverback.Messaging.Connectors.Repositories; using Silverback.Messaging.Messages; using Silverback.Messaging.Publishing; @@ -26,17 +25,16 @@ public class DependencyInjectionExtensionsTests private readonly IServiceCollection _services; private readonly TestSubscriber _testSubscriber; private IServiceProvider _serviceProvider; + private IServiceScope _serviceScope; - private IServiceProvider GetServiceProvider() => _serviceProvider ?? (_serviceProvider = _services.BuildServiceProvider()); + private IServiceProvider GetServiceProvider() => _serviceProvider ?? (_serviceProvider = _services.BuildServiceProvider(new ServiceProviderOptions { ValidateScopes = true })); + private IServiceProvider GetScopedServiceProvider() => (_serviceScope ?? (_serviceScope = GetServiceProvider().CreateScope())).ServiceProvider; private TestBroker GetBroker() => (TestBroker) GetServiceProvider().GetService(); - private IPublisher GetPublisher() => GetServiceProvider().GetService(); + private IPublisher GetPublisher() => GetScopedServiceProvider().GetService(); private BusConfigurator GetBusConfigurator() => GetServiceProvider().GetService(); - private InMemoryOutboundQueue GetOutboundQueue() => (InMemoryOutboundQueue)GetServiceProvider().GetService(); - - private IInboundConnector GetInboundConnector() => GetServiceProvider().GetService(); - private InMemoryInboundLog GetInboundLog() => (InMemoryInboundLog)GetServiceProvider().GetService(); + private InMemoryOutboundQueue GetOutboundQueue() => (InMemoryOutboundQueue)GetScopedServiceProvider().GetService(); public DependencyInjectionExtensionsTests() { @@ -51,6 +49,7 @@ public DependencyInjectionExtensionsTests() _services.AddSingleton(typeof(ILogger<>), typeof(NullLogger<>)); _serviceProvider = null; // Creation deferred to after AddBroker() has been called + _serviceScope = null; InMemoryInboundLog.Clear(); InMemoryOutboundQueue.Clear(); @@ -158,6 +157,23 @@ public void AddInboundConnector_PushMessages_MessagesReceived() _testSubscriber.ReceivedMessages.Count.Should().Be(5); } + [Fact] + public void AddInboundConnector_CalledMultipleTimes_EachMessageReceivedOnce() + { + _services.AddBroker(options => options.AddInboundConnector().AddInboundConnector()); + GetBusConfigurator().Connect(endpoints => + endpoints + .AddInbound(TestEndpoint.Default)); + + var consumer = GetBroker().Consumers.First(); + consumer.TestPush(new TestEventOne { Id = Guid.NewGuid() }); + consumer.TestPush(new TestEventTwo { Id = Guid.NewGuid() }); + consumer.TestPush(new TestEventOne { Id = Guid.NewGuid() }); + consumer.TestPush(new TestEventTwo { Id = Guid.NewGuid() }); + consumer.TestPush(new TestEventTwo { Id = Guid.NewGuid() }); + + _testSubscriber.ReceivedMessages.Count.Should().Be(5); + } [Fact] public void AddLoggedInboundConnector_PushMessages_MessagesReceived() { diff --git a/tests/Silverback.Integration.Tests/Messaging/Connectors/DeferredOutboundConnectorTests.cs b/tests/Silverback.Integration.Tests/Messaging/Connectors/DeferredOutboundConnectorTests.cs index 56d828eab..15384315b 100644 --- a/tests/Silverback.Integration.Tests/Messaging/Connectors/DeferredOutboundConnectorTests.cs +++ b/tests/Silverback.Integration.Tests/Messaging/Connectors/DeferredOutboundConnectorTests.cs @@ -48,7 +48,7 @@ public async Task OnMessageReceived_SingleMessage_Queued() await _queue.Commit(); _queue.Length.Should().Be(1); - var queued = _queue.Dequeue(1).First(); + var queued = (await _queue.Dequeue(1)).First(); queued.Message.Endpoint.Should().Be(outboundMessage.Endpoint); queued.Message.Headers.Count.Should().Be(2); ((IIntegrationMessage)queued.Message.Message).Id.Should().Be(outboundMessage.Message.Id); diff --git a/tests/Silverback.Integration.Tests/Messaging/Connectors/InboundConnectorTests.cs b/tests/Silverback.Integration.Tests/Messaging/Connectors/InboundConnectorTests.cs index fcf4c2fc5..9b17b2b81 100644 --- a/tests/Silverback.Integration.Tests/Messaging/Connectors/InboundConnectorTests.cs +++ b/tests/Silverback.Integration.Tests/Messaging/Connectors/InboundConnectorTests.cs @@ -44,7 +44,10 @@ public InboundConnectorTests() services.AddBroker(options => options.AddChunkStore(_ => new InMemoryChunkStore())); - var serviceProvider = services.BuildServiceProvider(); + var serviceProvider = services.BuildServiceProvider(new ServiceProviderOptions + { + ValidateScopes = true + }); _broker = (TestBroker)serviceProvider.GetService(); _connector = new InboundConnector(_broker, serviceProvider, new NullLogger()); _errorPolicyBuilder = new ErrorPolicyBuilder(serviceProvider, NullLoggerFactory.Instance); @@ -85,6 +88,36 @@ public void Bind_PushMessages_WrappedInboundMessagesReceived() _inboundSubscriber.ReceivedMessages.OfType>().Count().Should().Be(3); } + [Fact] + public void Bind_PushMessages_HeadersReceivedWithInboundMessages() + { + _connector.Bind(TestEndpoint.Default); + _broker.Connect(); + + var consumer = _broker.Consumers.First(); + consumer.TestPush(new TestEventOne { Id = Guid.NewGuid() }, new[] { new MessageHeader { Key = "key", Value = "value1" } }); + consumer.TestPush(new TestEventOne { Id = Guid.NewGuid() }, new[] { new MessageHeader { Key = "key", Value = "value2" } }); + + var inboundMessages = _inboundSubscriber.ReceivedMessages.OfType(); + inboundMessages.First().Headers.First().Value.Should().Be("value1"); + inboundMessages.Skip(1).First().Headers.First().Value.Should().Be("value2"); + } + + [Fact] + public void Bind_PushMessages_FailedAttemptsReceivedWithInboundMessages() + { + _connector.Bind(TestEndpoint.Default); + _broker.Connect(); + + var consumer = _broker.Consumers.First(); + consumer.TestPush(new TestEventOne { Id = Guid.NewGuid() }); + consumer.TestPush(new TestEventOne { Id = Guid.NewGuid() }, new[] { new MessageHeader { Key = MessageHeader.FailedAttemptsHeaderName, Value = "3" } }); + + var inboundMessages = _inboundSubscriber.ReceivedMessages.OfType(); + inboundMessages.First().FailedAttempts.Should().Be(0); + inboundMessages.Skip(1).First().FailedAttempts.Should().Be(3); + } + [Fact] public void Bind_PushMessagesInBatch_MessagesReceived() { @@ -525,6 +558,77 @@ public void Bind_WithRetryErrorPolicy_RetriedAndReceived() _testSubscriber.ReceivedMessages.Count.Should().Be(4); } + [Fact] + public void Bind_WithRetryErrorPolicyToHandleDeserializerErrors_RetriedAndReceived() + { + var testSerializer = new TestSerializer { MustFailCount = 3 }; + _connector.Bind(new TestEndpoint("test") + { + Serializer = testSerializer + }, _errorPolicyBuilder.Retry().MaxFailedAttempts(3)); + _broker.Connect(); + + var consumer = _broker.Consumers.First(); + consumer.TestPush(new TestEventOne { Content = "Test", Id = Guid.NewGuid() }); + + testSerializer.FailCount.Should().Be(3); + _testSubscriber.ReceivedMessages.Count.Should().Be(1); + } + + [Fact] + public void Bind_WithRetryErrorPolicyToHandleDeserializerErrorsInChunkedMessage_RetriedAndReceived() + { + var testSerializer = new TestSerializer { MustFailCount = 3 }; + _connector.Bind(new TestEndpoint("test") + { + Serializer = testSerializer + }, _errorPolicyBuilder.Retry().MaxFailedAttempts(3)); + _broker.Connect(); + + var buffer = Convert.FromBase64String( + "eyIkdHlwZSI6IlNpbHZlcmJhY2suVGVzdHMuSW50ZWdyYXRpb24uVGVzdFR5cGVzLkRvbWFpbi5UZXN0" + + "RXZlbnRPbmUsIFNpbHZlcmJhY2suSW50ZWdyYXRpb24uVGVzdHMiLCJDb250ZW50IjoiQSBmdWxsIG1l" + + "c3NhZ2UhIiwiSWQiOiI0Mjc1ODMwMi1kOGU5LTQzZjktYjQ3ZS1kN2FjNDFmMmJiMDMifQ=="); + + var consumer = _broker.Consumers.First(); + consumer.TestPush(new MessageChunk + { + MessageId = Guid.NewGuid(), + OriginalMessageId = "123", + ChunkId = 1, + ChunksCount = 4, + Content = buffer.Take(40).ToArray() + }); + consumer.TestPush(new MessageChunk + { + MessageId = Guid.NewGuid(), + OriginalMessageId = "123", + ChunkId = 2, + ChunksCount = 4, + Content = buffer.Skip(40).Take(40).ToArray() + }); + consumer.TestPush(new MessageChunk + { + MessageId = Guid.NewGuid(), + OriginalMessageId = "123", + ChunkId = 3, + ChunksCount = 4, + Content = buffer.Skip(80).Take(40).ToArray() + }); + consumer.TestPush(new MessageChunk + { + MessageId = Guid.NewGuid(), + OriginalMessageId = "123", + ChunkId = 4, + ChunksCount = 4, + Content = buffer.Skip(120).ToArray() + }); + + testSerializer.FailCount.Should().Be(3); + _testSubscriber.ReceivedMessages.Count.Should().Be(1); + _testSubscriber.ReceivedMessages.First().As().Content.Should().Be("A full message!"); + } + [Fact] public void Bind_WithChainedErrorPolicy_RetriedAndMoved() { @@ -544,6 +648,23 @@ public void Bind_WithChainedErrorPolicy_RetriedAndMoved() producer.ProducedMessages.Count.Should().Be(1); } + [Fact] + public void Bind_WithChainedErrorPolicy_OneAndOnlyOneFailedAttemptsHeaderIsAdded() + { + _testSubscriber.MustFailCount = 3; + _connector.Bind(TestEndpoint.Default, _errorPolicyBuilder.Chain( + _errorPolicyBuilder.Retry().MaxFailedAttempts(1), + _errorPolicyBuilder.Move(new TestEndpoint("bad")))); + _broker.Connect(); + + var consumer = _broker.Consumers.First(); + consumer.TestPush(new TestEventOne { Content = "Test", Id = Guid.NewGuid() }); + + var producer = (TestProducer)_broker.GetProducer(new TestEndpoint("bad")); + + producer.ProducedMessages.Last().Headers.Count(h => h.Key == MessageHeader.FailedAttemptsHeaderName).Should().Be(1); + } + [Fact] public void Bind_WithRetryErrorPolicy_RetriedAndReceivedInBatch() { @@ -570,6 +691,34 @@ public void Bind_WithRetryErrorPolicy_RetriedAndReceivedInBatch() _testSubscriber.ReceivedMessages.OfType().Count().Should().Be(5); } + [Fact] + public void Bind_WithRetryErrorPolicyToHandleDeserializerErrors_RetriedAndReceivedInBatch() + { + var testSerializer = new TestSerializer { MustFailCount = 3 }; + _connector.Bind(new TestEndpoint("test") + { + Serializer = testSerializer + }, + _errorPolicyBuilder.Retry().MaxFailedAttempts(3), + new InboundConnectorSettings + { + Batch = new BatchSettings + { + Size = 2 + } + }); + _broker.Connect(); + + var consumer = _broker.Consumers.First(); + consumer.TestPush(new TestEventOne { Content = "Test", Id = Guid.NewGuid() }); + consumer.TestPush(new TestEventOne { Content = "Test", Id = Guid.NewGuid() }); + + testSerializer.FailCount.Should().Be(3); + _testSubscriber.ReceivedMessages.OfType().Count().Should().Be(0); + _testSubscriber.ReceivedMessages.OfType().Count().Should().Be(1); + _testSubscriber.ReceivedMessages.OfType().Count().Should().Be(1); + _testSubscriber.ReceivedMessages.OfType().Count().Should().Be(2); + } #endregion } } \ No newline at end of file diff --git a/tests/Silverback.Integration.Tests/Messaging/Connectors/LoggedInboundConnectorTests.cs b/tests/Silverback.Integration.Tests/Messaging/Connectors/LoggedInboundConnectorTests.cs index f2c2663fb..0b46c16e4 100644 --- a/tests/Silverback.Integration.Tests/Messaging/Connectors/LoggedInboundConnectorTests.cs +++ b/tests/Silverback.Integration.Tests/Messaging/Connectors/LoggedInboundConnectorTests.cs @@ -9,7 +9,6 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Silverback.Messaging.Broker; -using Silverback.Messaging.Configuration; using Silverback.Messaging.Connectors; using Silverback.Messaging.Connectors.Repositories; using Silverback.Messaging.Messages; diff --git a/tests/Silverback.Integration.Tests/Messaging/Connectors/OffsetStoredInboundConnectorTests.cs b/tests/Silverback.Integration.Tests/Messaging/Connectors/OffsetStoredInboundConnectorTests.cs index 4b068b130..d3dc57ae0 100644 --- a/tests/Silverback.Integration.Tests/Messaging/Connectors/OffsetStoredInboundConnectorTests.cs +++ b/tests/Silverback.Integration.Tests/Messaging/Connectors/OffsetStoredInboundConnectorTests.cs @@ -9,7 +9,6 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Silverback.Messaging.Broker; -using Silverback.Messaging.Configuration; using Silverback.Messaging.Connectors; using Silverback.Messaging.Connectors.Repositories; using Silverback.Messaging.Messages; @@ -27,6 +26,7 @@ public class OffsetStoredInboundConnectorTests private readonly IInboundConnector _connector; private readonly TestBroker _broker; private readonly IServiceProvider _serviceProvider; + private readonly IServiceProvider _scopedServiceProvider; public OffsetStoredInboundConnectorTests() { @@ -43,11 +43,13 @@ public OffsetStoredInboundConnectorTests() services.AddScoped(); - _serviceProvider = services.BuildServiceProvider(); + _serviceProvider = services.BuildServiceProvider(new ServiceProviderOptions { ValidateScopes = true }); _broker = (TestBroker)_serviceProvider.GetService(); _connector = new OffsetStoredInboundConnector(_broker, _serviceProvider, new NullLogger(), new MessageLogger(new MessageKeyProvider(new[] { new DefaultPropertiesMessageKeyProvider() }))); + _scopedServiceProvider = _serviceProvider.CreateScope().ServiceProvider; + InMemoryOffsetStore.Clear(); } @@ -123,7 +125,7 @@ public void Bind_PushMessages_OffsetStored() consumer.TestPush(e, offset: o2); consumer.TestPush(e, offset: o1); - _serviceProvider.GetRequiredService().As().Count.Should().Be(2); + _scopedServiceProvider.GetRequiredService().As().Count.Should().Be(2); } [Fact] @@ -178,7 +180,7 @@ public void Bind_PushMessagesInBatch_OffsetStored() consumer.TestPush(e, offset: o2); consumer.TestPush(e, offset: o1); - _serviceProvider.GetRequiredService().As().Count.Should().Be(2); + _scopedServiceProvider.GetRequiredService().As().Count.Should().Be(2); } [Fact] @@ -209,7 +211,7 @@ public void Bind_PushMessagesInBatch_OnlyOffsetOfCommittedBatchStored() try { consumer.TestPush(e, offset: o3); } catch { } try { consumer.TestPush(fail, offset: o4); } catch { } - _serviceProvider.GetRequiredService().GetLatestValue("a").Value.Should().Be("2"); + _scopedServiceProvider.GetRequiredService().GetLatestValue("a").Value.Should().Be("2"); } [Fact] @@ -264,7 +266,7 @@ public void Bind_PushMessagesInBatchToMultipleConsumers_OnlyOffsetOfCommittedBat Task.WaitAll(tasks); - _serviceProvider.GetRequiredService().GetLatestValue("a").Value.Should().Be("2"); + _scopedServiceProvider.GetRequiredService().GetLatestValue("a").Value.Should().Be("2"); } } } \ No newline at end of file diff --git a/tests/Silverback.Integration.Tests/Messaging/Connectors/OutboundConnectorRouterTests.cs b/tests/Silverback.Integration.Tests/Messaging/Connectors/OutboundConnectorRouterTests.cs index b21eab70b..5983352fb 100644 --- a/tests/Silverback.Integration.Tests/Messaging/Connectors/OutboundConnectorRouterTests.cs +++ b/tests/Silverback.Integration.Tests/Messaging/Connectors/OutboundConnectorRouterTests.cs @@ -1,6 +1,7 @@ // Copyright (c) 2018-2019 Sergio Aquilini // This code is licensed under MIT license (see LICENSE file for details) +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using FluentAssertions; @@ -8,7 +9,6 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Silverback.Messaging.Broker; -using Silverback.Messaging.Configuration; using Silverback.Messaging.Connectors; using Silverback.Messaging.Connectors.Repositories; using Silverback.Messaging.Subscribers; @@ -50,7 +50,7 @@ public OutboundConnectorRouterTests() InMemoryOutboundQueue.Clear(); } - [Theory, ClassData(typeof(OnMessageReceived_MultipleMessages_CorrectlyRoutedToEndpoints_TestData))] + [Theory, MemberData(nameof(OnMessageReceived_MultipleMessages_CorrectlyRoutedToEndpoints_TestData))] public async Task OnMessageReceived_MultipleMessages_CorrectlyRoutedToEndpoint(IIntegrationMessage message, string[] expectedEndpointNames) { _routingConfiguration.Add(new TestEndpoint("allMessages"), null); @@ -61,7 +61,7 @@ public async Task OnMessageReceived_MultipleMessages_CorrectlyRoutedToEndpoint(I await _connectorRouter.OnMessageReceived(message); await _outboundQueue.Commit(); - var queued = _outboundQueue.Dequeue(100); + var queued = await _outboundQueue.Dequeue(100); foreach (var expectedEndpointName in expectedEndpointNames) { @@ -78,6 +78,13 @@ public async Task OnMessageReceived_MultipleMessages_CorrectlyRoutedToEndpoint(I } } + public static IEnumerable OnMessageReceived_MultipleMessages_CorrectlyRoutedToEndpoints_TestData => + new[] + { + new object[] { new TestEventOne(), new[] { "allMessages", "allEvents", "eventOne" } }, + new object[] { new TestEventTwo(), new[] { "allMessages", "allEvents", "eventTwo" } } + }; + [Fact] public async Task OnMessageReceived_Message_CorrectlyRoutedToDefaultConnector() { @@ -86,7 +93,7 @@ public async Task OnMessageReceived_Message_CorrectlyRoutedToDefaultConnector() await _connectorRouter.OnMessageReceived(new TestEventOne()); await _outboundQueue.Commit(); - var queued = _outboundQueue.Dequeue(1); + var queued = await _outboundQueue.Dequeue(1); queued.Count().Should().Be(1); _broker.ProducedMessages.Count.Should().Be(0); } @@ -99,7 +106,7 @@ public async Task OnMessageReceived_Message_CorrectlyRoutedToConnector() await _connectorRouter.OnMessageReceived(new TestEventOne()); await _outboundQueue.Commit(); - var queued = _outboundQueue.Dequeue(1); + var queued = await _outboundQueue.Dequeue(1); queued.Count().Should().Be(0); _broker.ProducedMessages.Count.Should().Be(1); } diff --git a/tests/Silverback.Integration.Tests/Messaging/Connectors/OutboundConnectorRouterTestsData.cs b/tests/Silverback.Integration.Tests/Messaging/Connectors/OutboundConnectorRouterTestsData.cs deleted file mode 100644 index 6905e6463..000000000 --- a/tests/Silverback.Integration.Tests/Messaging/Connectors/OutboundConnectorRouterTestsData.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) 2018-2019 Sergio Aquilini -// This code is licensed under MIT license (see LICENSE file for details) - -using System.Collections; -using System.Collections.Generic; -using Silverback.Tests.Integration.TestTypes.Domain; - -namespace Silverback.Tests.Integration.Messaging.Connectors -{ - public class OnMessageReceived_MultipleMessages_CorrectlyRoutedToEndpoints_TestData : IEnumerable - { - public IEnumerator GetEnumerator() - { - // event - yield return new object[]{new TestEventOne(), new[] { "allMessages", "allEvents", "eventOne" }}; - yield return new object[] { new TestEventTwo(), new[] { "allMessages", "allEvents", "eventTwo" }}; - } - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - } -} diff --git a/tests/Silverback.Integration.Tests/Messaging/Connectors/OutboundQueueWorkerTests.cs b/tests/Silverback.Integration.Tests/Messaging/Connectors/OutboundQueueWorkerTests.cs index 5aeaefa8a..da8558622 100644 --- a/tests/Silverback.Integration.Tests/Messaging/Connectors/OutboundQueueWorkerTests.cs +++ b/tests/Silverback.Integration.Tests/Messaging/Connectors/OutboundQueueWorkerTests.cs @@ -1,12 +1,13 @@ // Copyright (c) 2018-2019 Sergio Aquilini // This code is licensed under MIT license (see LICENSE file for details) +using System.Threading; +using System.Threading.Tasks; using FluentAssertions; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Silverback.Messaging.Broker; -using Silverback.Messaging.Configuration; using Silverback.Messaging.Connectors; using Silverback.Messaging.Connectors.Repositories; using Silverback.Messaging.Messages; @@ -38,10 +39,11 @@ public OutboundQueueWorkerTests() services .AddSingleton() .AddSingleton(typeof(ILogger<>), typeof(NullLogger<>)) + .AddSingleton() .AddBus() .AddBroker(options => options.AddDeferredOutboundConnector(_ => new InMemoryOutboundQueue())); - var serviceProvider = services.BuildServiceProvider(); + var serviceProvider = services.BuildServiceProvider(new ServiceProviderOptions { ValidateScopes = true }); serviceProvider.GetRequiredService() .Add(TestEndpoint.Default); @@ -49,27 +51,27 @@ public OutboundQueueWorkerTests() _broker = (TestBroker) serviceProvider.GetRequiredService(); _broker.Connect(); - _worker = new OutboundQueueWorker(_queue, _broker, new NullLogger(), new MessageLogger(new MessageKeyProvider(new[] { new DefaultPropertiesMessageKeyProvider() })), true, 100); // TODO: Test order not enforced + _worker = new OutboundQueueWorker(serviceProvider, _broker, new NullLogger(), new MessageLogger(new MessageKeyProvider(new[] { new DefaultPropertiesMessageKeyProvider() })), true, 100); // TODO: Test order not enforced InMemoryOutboundQueue.Clear(); } [Fact] - public void ProcessQueue_SomeMessages_Produced() + public async Task ProcessQueue_SomeMessages_Produced() { - _queue.Enqueue(new OutboundMessage + await _queue.Enqueue(new OutboundMessage { Message = new TestEventOne { Content = "Test" }, Endpoint = new TestEndpoint("topic1") }); - _queue.Enqueue(new OutboundMessage + await _queue.Enqueue(new OutboundMessage { Message = new TestEventOne { Content = "Test" }, Endpoint = new TestEndpoint("topic2") }); - _queue.Commit(); + await _queue.Commit(); - _worker.ProcessQueue(); + await _worker.ProcessQueue(CancellationToken.None); _broker.ProducedMessages.Count.Should().Be(2); _broker.ProducedMessages[0].Endpoint.Name.Should().Be("topic1"); @@ -77,31 +79,31 @@ public void ProcessQueue_SomeMessages_Produced() } [Fact] - public void ProcessQueue_RunTwice_ProducedOnce() + public async Task ProcessQueue_RunTwice_ProducedOnce() { - _queue.Enqueue(_sampleOutboundMessage); - _queue.Enqueue(_sampleOutboundMessage); - _queue.Commit(); + await _queue.Enqueue(_sampleOutboundMessage); + await _queue.Enqueue(_sampleOutboundMessage); + await _queue.Commit(); - _worker.ProcessQueue(); - _worker.ProcessQueue(); + await _worker.ProcessQueue(CancellationToken.None); + await _worker.ProcessQueue(CancellationToken.None); _broker.ProducedMessages.Count.Should().Be(2); } [Fact] - public void ProcessQueue_RunTwice_ProducedNewMessages() + public async Task ProcessQueue_RunTwice_ProducedNewMessages() { - _queue.Enqueue(_sampleOutboundMessage); - _queue.Enqueue(_sampleOutboundMessage); - _queue.Commit(); + await _queue.Enqueue(_sampleOutboundMessage); + await _queue.Enqueue(_sampleOutboundMessage); + await _queue.Commit(); - _worker.ProcessQueue(); + await _worker.ProcessQueue(CancellationToken.None); - _queue.Enqueue(_sampleOutboundMessage); - _queue.Commit(); + await _queue.Enqueue(_sampleOutboundMessage); + await _queue.Commit(); - _worker.ProcessQueue(); + await _worker.ProcessQueue(CancellationToken.None); _broker.ProducedMessages.Count.Should().Be(3); } diff --git a/tests/Silverback.Integration.Tests/Messaging/Connectors/Repositories/InMemoryOutboundQueueTests.cs b/tests/Silverback.Integration.Tests/Messaging/Connectors/Repositories/InMemoryOutboundQueueTests.cs index 2394f076f..583ee0651 100644 --- a/tests/Silverback.Integration.Tests/Messaging/Connectors/Repositories/InMemoryOutboundQueueTests.cs +++ b/tests/Silverback.Integration.Tests/Messaging/Connectors/Repositories/InMemoryOutboundQueueTests.cs @@ -67,14 +67,14 @@ public void EnqueueRollbackTest() } [Fact] - public void EnqueueCommitRollbackCommitTest() + public async Task EnqueueCommitRollbackCommitTest() { - _queue.Enqueue(_sampleOutboundMessage); - _queue.Commit(); - _queue.Enqueue(_sampleOutboundMessage); - _queue.Rollback(); - _queue.Enqueue(_sampleOutboundMessage); - _queue.Commit(); + await _queue.Enqueue(_sampleOutboundMessage); + await _queue.Commit(); + await _queue.Enqueue(_sampleOutboundMessage); + await _queue.Rollback(); + await _queue.Enqueue(_sampleOutboundMessage); + await _queue.Commit(); _queue.Length.Should().Be(2); } @@ -83,37 +83,37 @@ public void EnqueueCommitRollbackCommitTest() [InlineData(3, 3)] [InlineData(5, 5)] [InlineData(10, 5)] - public void DequeueTest(int count, int expected) + public async Task DequeueTest(int count, int expected) { for (var i = 0; i < 5; i++) { - _queue.Enqueue(_sampleOutboundMessage); + await _queue.Enqueue(_sampleOutboundMessage); } - _queue.Commit(); + await _queue.Commit(); - var result = _queue.Dequeue(count); + var result = await _queue.Dequeue(count); result.Count().Should().Be(expected); } [Fact] - public void AcknowledgeRetryTest() + public async Task AcknowledgeRetryTest() { for (var i = 0; i < 5; i++) { - _queue.Enqueue(_sampleOutboundMessage); + await _queue.Enqueue(_sampleOutboundMessage); } - _queue.Commit(); + await _queue.Commit(); - var result = _queue.Dequeue(5).ToArray(); + var result = (await _queue.Dequeue(5)).ToArray(); - _queue.Acknowledge(result[0]); - _queue.Retry(result[1]); - _queue.Acknowledge(result[2]); - _queue.Retry(result[3]); - _queue.Acknowledge(result[4]); + await _queue.Acknowledge(result[0]); + await _queue.Retry(result[1]); + await _queue.Acknowledge(result[2]); + await _queue.Retry(result[3]); + await _queue.Acknowledge(result[4]); _queue.Length.Should().Be(2); } diff --git a/tests/Silverback.Integration.Tests/Messaging/ErrorHandling/ErrorPolicyBaseTests.cs b/tests/Silverback.Integration.Tests/Messaging/ErrorHandling/ErrorPolicyBaseTests.cs index d0ae0d781..d7f0099ef 100644 --- a/tests/Silverback.Integration.Tests/Messaging/ErrorHandling/ErrorPolicyBaseTests.cs +++ b/tests/Silverback.Integration.Tests/Messaging/ErrorHandling/ErrorPolicyBaseTests.cs @@ -2,8 +2,12 @@ // This code is licensed under MIT license (see LICENSE file for details) using System; +using System.Collections.Generic; using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using NSubstitute; using Silverback.Messaging.Messages; +using Silverback.Messaging.Publishing; using Silverback.Tests.Integration.TestTypes; using Silverback.Tests.Integration.TestTypes.Domain; using Xunit; @@ -12,45 +16,73 @@ namespace Silverback.Tests.Integration.Messaging.ErrorHandling { public class ErrorPolicyBaseTests { - [Theory, ClassData(typeof(ApplyTo_TestData))] + [Theory, MemberData(nameof(ApplyTo_TestData))] public void ApplyTo_Exception_CanHandleReturnsExpectedResult(Exception exception, bool mustApply) { var policy = new TestErrorPolicy() .ApplyTo() .ApplyTo(); - var canHandle = policy.CanHandle(new FailedMessage(new TestEventOne(), 99), exception); + var canHandle = policy.CanHandle(new InboundMessage { Message = new TestEventOne(), FailedAttempts = 99 }, exception); canHandle.Should().Be(mustApply); } - [Theory, ClassData(typeof(Exclude_TestData))] + public static IEnumerable ApplyTo_TestData => + new[] + { + new object[] { new ArgumentException(), true }, + new object[] { new ArgumentOutOfRangeException(), true }, + new object[] { new InvalidCastException(), true }, + new object[] { new FormatException(), false } + }; + + [Theory, MemberData(nameof(Exclude_TestData))] public void Exclude_Exception_CanHandleReturnsExpectedResult(Exception exception, bool mustApply) { var policy = (TestErrorPolicy)new TestErrorPolicy() .Exclude() .Exclude(); - var canHandle = policy.CanHandle(new FailedMessage(new TestEventOne(), 99), exception); + var canHandle = policy.CanHandle(new InboundMessage { Message = new TestEventOne(), FailedAttempts = 99 }, exception); canHandle.Should().Be(mustApply); } - [Theory, ClassData(typeof(ApplyToAndExclude_TestData))] - public void ApplyToAndExcludeTest(Exception exception, bool mustApply) + public static IEnumerable Exclude_TestData => + new[] + { + new object[] { new ArgumentException(), false }, + new object[] { new ArgumentOutOfRangeException(), false }, + new object[] { new InvalidCastException(), false }, + new object[] { new FormatException(), true } + }; + + [Theory, MemberData(nameof(ApplyToAndExclude_TestData))] + public void ApplyToAndExclude_Exception_CanHandleReturnsExpectedResult(Exception exception, bool mustApply) { - var policy = (TestErrorPolicy)new TestErrorPolicy() + var policy = (TestErrorPolicy) new TestErrorPolicy() .ApplyTo() .Exclude() .ApplyTo(); - var canHandle = policy.CanHandle(new FailedMessage(new TestEventOne(), 99), exception); + var canHandle = policy.CanHandle(new InboundMessage{ Message = new TestEventOne(), FailedAttempts = 99}, exception); canHandle.Should().Be(mustApply); } - [Theory, ClassData(typeof(ApplyWhen_TestData))] - public void ApplyWhenTest(FailedMessage message, Exception exception, bool mustApply) + public static IEnumerable ApplyToAndExclude_TestData => + new[] + { + new object[] { new ArgumentException(), true }, + new object[] { new ArgumentNullException(), true }, + new object[] { new ArgumentOutOfRangeException(), false }, + new object[] { new InvalidCastException(), false }, + new object[] { new FormatException(), true } + }; + + [Theory, MemberData(nameof(ApplyWhen_TestData))] + public void ApplyWhen_Exception_CanHandleReturnsExpectedResult(IInboundMessage message, Exception exception, bool mustApply) { var policy = (TestErrorPolicy)new TestErrorPolicy() .ApplyWhen((msg, ex) => msg.FailedAttempts <= 5 && ex.Message != "no"); @@ -59,5 +91,41 @@ public void ApplyWhenTest(FailedMessage message, Exception exception, bool mustA canHandle.Should().Be(mustApply); } + + public static IEnumerable ApplyWhen_TestData => + new[] + { + new object[] + { + new InboundMessage { Message = new TestEventOne(), FailedAttempts = 3 }, + new ArgumentException(), + true + }, + new object[] + { + new InboundMessage { Message = new TestEventOne(), FailedAttempts = 6 }, + new ArgumentException(), + false + }, + new object[] + { + new InboundMessage { Message = new TestEventOne(), FailedAttempts = 3 }, + new ArgumentException("no"), + false + } + }; + + [Fact] + public void Publish_Exception_MessagePublished() + { + var publisher = Substitute.For(); + var serviceProvider = new ServiceCollection().AddScoped(_ => publisher).BuildServiceProvider(new ServiceProviderOptions { ValidateScopes = true }); + var policy = (TestErrorPolicy) new TestErrorPolicy(serviceProvider).Publish(msg => new TestEventTwo{ Content = msg.FailedAttempts.ToString()}); + var message = new InboundMessage { Message = new TestEventOne(), FailedAttempts = 3 }; + + policy.HandleError(message, new ArgumentNullException()); + + publisher.Received().Publish(Arg.Any()); + } } } \ No newline at end of file diff --git a/tests/Silverback.Integration.Tests/Messaging/ErrorHandling/ErrorPolicyBaseTestsData.cs b/tests/Silverback.Integration.Tests/Messaging/ErrorHandling/ErrorPolicyBaseTestsData.cs deleted file mode 100644 index 738a3f8e4..000000000 --- a/tests/Silverback.Integration.Tests/Messaging/ErrorHandling/ErrorPolicyBaseTestsData.cs +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright (c) 2018-2019 Sergio Aquilini -// This code is licensed under MIT license (see LICENSE file for details) - -using System; -using System.Collections; -using System.Collections.Generic; -using Silverback.Messaging.Messages; -using Silverback.Tests.Integration.TestTypes.Domain; - -namespace Silverback.Tests.Integration.Messaging.ErrorHandling -{ - public class ApplyTo_TestData : IEnumerable - { - public IEnumerator GetEnumerator() - { - yield return new object[] { new ArgumentException(), true }; - yield return new object[] { new ArgumentOutOfRangeException(), true }; - yield return new object[] { new InvalidCastException(), true }; - yield return new object[] { new FormatException(), false }; - } - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - } - - public class Exclude_TestData : IEnumerable - { - public IEnumerator GetEnumerator() - { - yield return new object[] { new ArgumentException(), false }; - yield return new object[] { new ArgumentOutOfRangeException(), false }; - yield return new object[] { new InvalidCastException(), false }; - yield return new object[] { new FormatException(), true }; - } - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - } - - public class ApplyToAndExclude_TestData : IEnumerable - { - public IEnumerator GetEnumerator() - { - yield return new object[] { new ArgumentException(), true }; - yield return new object[] { new ArgumentNullException(), true }; - yield return new object[] { new ArgumentOutOfRangeException(), false }; - yield return new object[] { new InvalidCastException(), false }; - yield return new object[] { new FormatException(), true }; - } - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - } - - public class ApplyWhen_TestData : IEnumerable - { - public IEnumerator GetEnumerator() - { - yield return new object[] { new FailedMessage(new TestEventOne(), 3), new ArgumentException(), true }; - yield return new object[] { new FailedMessage(new TestEventOne(), 6), new ArgumentException(), false }; - yield return new object[] { new FailedMessage(new TestEventOne(), 3), new ArgumentException("no"), false }; - } - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - } -} \ No newline at end of file diff --git a/tests/Silverback.Integration.Tests/Messaging/ErrorHandling/ErrorPolicyChainTests.cs b/tests/Silverback.Integration.Tests/Messaging/ErrorHandling/ErrorPolicyChainTests.cs index 786a5c731..5cc194ad6 100644 --- a/tests/Silverback.Integration.Tests/Messaging/ErrorHandling/ErrorPolicyChainTests.cs +++ b/tests/Silverback.Integration.Tests/Messaging/ErrorHandling/ErrorPolicyChainTests.cs @@ -4,6 +4,7 @@ using System; using FluentAssertions; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Silverback.Messaging.Configuration; using Silverback.Messaging.ErrorHandling; @@ -24,14 +25,17 @@ public ErrorPolicyChainTests() services.AddBus().AddBroker(); - _errorPolicyBuilder = new ErrorPolicyBuilder(services.BuildServiceProvider(), NullLoggerFactory.Instance); + services.AddSingleton(); + services.AddSingleton(typeof(ILogger<>), typeof(NullLogger<>)); + + _errorPolicyBuilder = new ErrorPolicyBuilder(services.BuildServiceProvider(new ServiceProviderOptions { ValidateScopes = true }), NullLoggerFactory.Instance); } [Theory] [InlineData(1)] [InlineData(3)] [InlineData(4)] - public void ChainingTest(int failedAttempts) + public void HandleError_RetryWithMaxFailedAttempts_AppliedAccordingToMaxFailedAttempts(int failedAttempts) { var testPolicy = new TestErrorPolicy(); @@ -39,7 +43,7 @@ public void ChainingTest(int failedAttempts) _errorPolicyBuilder.Retry().MaxFailedAttempts(3), testPolicy); - chain.HandleError(new FailedMessage(new TestEventOne(), failedAttempts), new Exception("test")); + chain.HandleError(new InboundMessage { Message = new TestEventOne(), FailedAttempts = failedAttempts }, new Exception("test")); testPolicy.Applied.Should().Be(failedAttempts > 3); } @@ -49,15 +53,40 @@ public void ChainingTest(int failedAttempts) [InlineData(2, ErrorAction.Retry)] [InlineData(3, ErrorAction.Skip)] [InlineData(4, ErrorAction.Skip)] - public void ChainingTest2(int failedAttempts, ErrorAction expectedAction) + public void HandleError_RetryTwiceThenSkip_CorrectPolicyApplied(int failedAttempts, ErrorAction expectedAction) { var chain = _errorPolicyBuilder.Chain( _errorPolicyBuilder.Retry().MaxFailedAttempts(2), _errorPolicyBuilder.Skip()); - var action = chain.HandleError(new FailedMessage(new TestEventOne(), failedAttempts), new Exception("test")); + var action = chain.HandleError(new InboundMessage { Message = new TestEventOne(), FailedAttempts = failedAttempts }, new Exception("test")); action.Should().Be(expectedAction); } + + [Theory] + [InlineData(1, 0)] + [InlineData(2, 0)] + [InlineData(3, 1)] + [InlineData(4, 1)] + [InlineData(5, 2)] + public void HandleError_MultiplePoliciesWithSetMaxFailedAttempts_CorrectPolicyApplied(int failedAttempts, int expectedAppliedPolicy) + { + var policies = new ErrorPolicyBase[] + { + new TestErrorPolicy().MaxFailedAttempts(2), + new TestErrorPolicy().MaxFailedAttempts(2), + new TestErrorPolicy().MaxFailedAttempts(2) + }; + + var chain = _errorPolicyBuilder.Chain(policies); + + chain.HandleError(new InboundMessage { Message = new TestEventOne(), FailedAttempts = failedAttempts }, new Exception("test")); + + for (int i = 0; i < policies.Length; i++) + { + policies[i].As().Applied.Should().Be(i == expectedAppliedPolicy); + } + } } } diff --git a/tests/Silverback.Integration.Tests/Messaging/ErrorHandling/MoveMessageErrorPolicyTests.cs b/tests/Silverback.Integration.Tests/Messaging/ErrorHandling/MoveMessageErrorPolicyTests.cs index 57c27033e..7d546ce7f 100644 --- a/tests/Silverback.Integration.Tests/Messaging/ErrorHandling/MoveMessageErrorPolicyTests.cs +++ b/tests/Silverback.Integration.Tests/Messaging/ErrorHandling/MoveMessageErrorPolicyTests.cs @@ -2,6 +2,8 @@ // This code is licensed under MIT license (see LICENSE file for details) using System; +using System.Linq; +using System.Text; using FluentAssertions; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -9,7 +11,6 @@ using Silverback.Messaging.Broker; using Silverback.Messaging.Configuration; using Silverback.Messaging.Messages; -using Silverback.Messaging.Publishing; using Silverback.Tests.Integration.TestTypes; using Silverback.Tests.Integration.TestTypes.Domain; using Xunit; @@ -28,11 +29,11 @@ public MoveMessageErrorPolicyTests() services.AddSingleton(); services.AddSingleton(typeof(ILogger<>), typeof(NullLogger<>)); - services.AddSingleton(); + services.AddBus(); services.AddBroker(options => { }); - var serviceProvider = services.BuildServiceProvider(); + var serviceProvider = services.BuildServiceProvider(new ServiceProviderOptions { ValidateScopes = true }); _errorPolicyBuilder = new ErrorPolicyBuilder(serviceProvider, NullLoggerFactory.Instance); @@ -41,11 +42,11 @@ public MoveMessageErrorPolicyTests() } [Fact] - public void TryHandleMessage_Failed_MessageMoved() + public void HandleError_InboundMessage_MessageMoved() { var policy = _errorPolicyBuilder.Move(TestEndpoint.Default); - policy.HandleError(new FailedMessage(new TestEventOne()), new Exception("test")); + policy.HandleError(new InboundMessage { Message = new TestEventOne() }, new Exception("test")); var producer = (TestProducer)_broker.GetProducer(TestEndpoint.Default); @@ -53,16 +54,83 @@ public void TryHandleMessage_Failed_MessageMoved() } [Fact] - public void Transform_Failed_MessageTranslated() + public void HandleError_InboundMessage_MessagePreserved() + { + var policy = _errorPolicyBuilder.Move(TestEndpoint.Default); + + var message = new InboundMessage { Message = new TestEventOne { Content = "hey oh!" } }; + message.Headers.Add("key1", "value1"); + message.Headers.Add("key2", "value2"); + policy.HandleError(message, new Exception("test")); + + var producer = (TestProducer)_broker.GetProducer(TestEndpoint.Default); + + var producedMessage = producer.ProducedMessages.Last(); + var deserializedMessage = producedMessage.Endpoint.Serializer.Deserialize(producedMessage.Message); + deserializedMessage.Should().BeEquivalentTo(message.Message); + } + + [Fact] + public void HandleError_NotDeserializedInboundMessage_MessagePreserved() + { + var policy = _errorPolicyBuilder.Move(TestEndpoint.Default); + + var message = new InboundMessage { Message = Encoding.UTF8.GetBytes("hey oh!") }; + message.Headers.Add("key1", "value1"); + message.Headers.Add("key2", "value2"); + policy.HandleError(message, new Exception("test")); + + var producer = (TestProducer)_broker.GetProducer(TestEndpoint.Default); + var producedMessage = producer.ProducedMessages.Last(); + + producedMessage.Message.Should().Equal(producedMessage.Message); + } + + [Fact] + public void HandleError_InboundMessage_HeadersPreserved() + { + var policy = _errorPolicyBuilder.Move(TestEndpoint.Default); + + var message = new InboundMessage { Message = new TestEventOne() }; + message.Headers.Add("key1", "value1"); + message.Headers.Add("key2", "value2"); + policy.HandleError(message, new Exception("test")); + + var producer = (TestProducer)_broker.GetProducer(TestEndpoint.Default); + + producer.ProducedMessages.Last().Headers.Should().BeEquivalentTo(message.Headers); + } + + [Fact] + public void Transform_InboundMessage_MessageTranslated() { var policy = _errorPolicyBuilder.Move(TestEndpoint.Default) .Transform((msg, ex) => new TestEventTwo()); - policy.HandleError(new FailedMessage(new TestEventOne()), new Exception("test")); + policy.HandleError(new InboundMessage { Message = new TestEventOne() }, new Exception("test")); var producer = (TestProducer)_broker.GetProducer(TestEndpoint.Default); var producedMessage = producer.Endpoint.Serializer.Deserialize(producer.ProducedMessages[0].Message); producedMessage.Should().BeOfType(); } + + [Fact] + public void Transform_InboundMessage_HeadersProperlyModified() + { + var policy = _errorPolicyBuilder.Move(TestEndpoint.Default) + .Transform((msg, ex) => new TestEventTwo(), (headers, ex) => + { + headers.Add("error", ex.GetType().Name); + return headers; + }); + + var message = new InboundMessage { Message = new TestEventOne() }; + message.Headers.Add("key", "value"); + policy.HandleError(message, new Exception("test")); + + var producer = (TestProducer)_broker.GetProducer(TestEndpoint.Default); + var newHeaders = producer.ProducedMessages[0].Headers; + newHeaders.Count().Should().Be(2); + } } } \ No newline at end of file diff --git a/tests/Silverback.Integration.Tests/Messaging/ErrorHandling/RetryErrorPolicyTests.cs b/tests/Silverback.Integration.Tests/Messaging/ErrorHandling/RetryErrorPolicyTests.cs index 17e5a93cf..7298fbc8e 100644 --- a/tests/Silverback.Integration.Tests/Messaging/ErrorHandling/RetryErrorPolicyTests.cs +++ b/tests/Silverback.Integration.Tests/Messaging/ErrorHandling/RetryErrorPolicyTests.cs @@ -3,10 +3,14 @@ using System; using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; +using Silverback.Messaging.Broker; +using Silverback.Messaging.Configuration; using Silverback.Messaging.ErrorHandling; using Silverback.Messaging.Messages; +using Silverback.Tests.Integration.TestTypes; using Silverback.Tests.Integration.TestTypes.Domain; using Xunit; @@ -14,12 +18,26 @@ namespace Silverback.Tests.Integration.Messaging.ErrorHandling { public class RetryErrorPolicyTests { - private RetryErrorPolicy _policy; + private readonly ErrorPolicyBuilder _errorPolicyBuilder; + private readonly IBroker _broker; public RetryErrorPolicyTests() { - _policy = new RetryErrorPolicy(NullLoggerFactory.Instance.CreateLogger(), new MessageLogger(new MessageKeyProvider(new[] { new DefaultPropertiesMessageKeyProvider() }))); - _policy.MaxFailedAttempts(3); + var services = new ServiceCollection(); + + services.AddSingleton(); + services.AddSingleton(typeof(ILogger<>), typeof(NullLogger<>)); + + services.AddBus(); + + services.AddBroker(options => { }); + + var serviceProvider = services.BuildServiceProvider(new ServiceProviderOptions { ValidateScopes = true }); + + _errorPolicyBuilder = new ErrorPolicyBuilder(serviceProvider, NullLoggerFactory.Instance); + + _broker = serviceProvider.GetRequiredService(); + _broker.Connect(); } [Theory] @@ -27,9 +45,11 @@ public RetryErrorPolicyTests() [InlineData(3, true)] [InlineData(4, false)] [InlineData(7, false)] - public void CanHandleTest(int failedAttempts, bool expectedResult) + public void CanHandle_InboundMessageWithDifferentFailedAttemptsCount_ReturnReflectsMaxFailedAttempts(int failedAttempts, bool expectedResult) { - var canHandle = _policy.CanHandle(new FailedMessage(new TestEventOne(), failedAttempts), new Exception("test")); + var policy = _errorPolicyBuilder.Retry().MaxFailedAttempts(3); + + var canHandle = policy.CanHandle(new InboundMessage { Message = new TestEventOne(), FailedAttempts = failedAttempts }, new Exception("test")); canHandle.Should().Be(expectedResult); } diff --git a/tests/Silverback.Integration.Tests/Messaging/ErrorHandling/SkipMessageErrorPolicyTests.cs b/tests/Silverback.Integration.Tests/Messaging/ErrorHandling/SkipMessageErrorPolicyTests.cs index 4aa685bd4..ab61a7460 100644 --- a/tests/Silverback.Integration.Tests/Messaging/ErrorHandling/SkipMessageErrorPolicyTests.cs +++ b/tests/Silverback.Integration.Tests/Messaging/ErrorHandling/SkipMessageErrorPolicyTests.cs @@ -17,7 +17,7 @@ public class SkipMessageErrorPolicyTests public SkipMessageErrorPolicyTests() { - _policy = new SkipMessageErrorPolicy(new NullLogger(), new MessageLogger(new MessageKeyProvider(new[] { new DefaultPropertiesMessageKeyProvider() }))); + _policy = new SkipMessageErrorPolicy(null, new NullLogger(), new MessageLogger(new MessageKeyProvider(new[] { new DefaultPropertiesMessageKeyProvider() }))); } [Theory] @@ -25,7 +25,7 @@ public SkipMessageErrorPolicyTests() [InlineData(333, ErrorAction.Skip)] public void SkipTest(int failedAttempts, ErrorAction expectedAction) { - var action = _policy.HandleError(new FailedMessage(new TestEventOne(), failedAttempts), new Exception("test")); + var action = _policy.HandleError(new InboundMessage { Message = new TestEventOne(), FailedAttempts = failedAttempts }, new Exception("test")); action.Should().Be(expectedAction); } diff --git a/tests/Silverback.Integration.Tests/Messaging/Messages/InboundMessageHelperTests.cs b/tests/Silverback.Integration.Tests/Messaging/Messages/InboundMessageHelperTests.cs new file mode 100644 index 000000000..089e05bff --- /dev/null +++ b/tests/Silverback.Integration.Tests/Messaging/Messages/InboundMessageHelperTests.cs @@ -0,0 +1,49 @@ +// Copyright (c) 2018-2019 Sergio Aquilini +// This code is licensed under MIT license (see LICENSE file for details) + +using FluentAssertions; +using Silverback.Messaging.Messages; +using Silverback.Tests.Integration.TestTypes; +using Xunit; + +namespace Silverback.Tests.Integration.Messaging.Messages +{ + public class InboundMessageHelperTests + { + [Fact] + public void Create_InboundMessage_MessageReplaced() + { + var inboundMessage = new InboundMessage + { + Message = 1, + }; + + var newInboundMessage = InboundMessageHelper.CreateNewInboundMessage(2, inboundMessage); + + newInboundMessage.Message.Should().Be(2); + } + + [Fact] + public void Create_InboundMessage_ValuesPreserved() + { + var inboundMessage = new InboundMessage + { + Message = 1, + Endpoint = TestEndpoint.Default, + FailedAttempts = 3, + Offset = new TestOffset("a", "b"), + MustUnwrap = true + }; + inboundMessage.Headers.Add("h1", "h1"); + inboundMessage.Headers.Add("h2", "h2"); + + var newInboundMessage = InboundMessageHelper.CreateNewInboundMessage(2, inboundMessage); + + newInboundMessage.Endpoint.Should().BeEquivalentTo(inboundMessage.Endpoint); + newInboundMessage.FailedAttempts.Should().Be(inboundMessage.FailedAttempts); + newInboundMessage.Offset.Should().BeEquivalentTo(inboundMessage.Offset); + newInboundMessage.MustUnwrap.Should().Be(inboundMessage.MustUnwrap); + newInboundMessage.Headers.Should().BeEquivalentTo(inboundMessage.Headers); + } + } +} \ No newline at end of file diff --git a/tests/Silverback.Integration.Tests/Messaging/Messages/MessageHeaderCollectionTests.cs b/tests/Silverback.Integration.Tests/Messaging/Messages/MessageHeaderCollectionTests.cs new file mode 100644 index 000000000..804cfa8dd --- /dev/null +++ b/tests/Silverback.Integration.Tests/Messaging/Messages/MessageHeaderCollectionTests.cs @@ -0,0 +1,57 @@ +// Copyright (c) 2018-2019 Sergio Aquilini +// This code is licensed under MIT license (see LICENSE file for details) + +using FluentAssertions; +using Silverback.Messaging.Messages; +using Xunit; + +namespace Silverback.Tests.Integration.Messaging.Messages +{ + public class MessageHeaderCollectionTests + { + [Fact] + public void Add_SomeHeaders_HeadersAdded() + { + var collection = new MessageHeaderCollection(); + + collection.Add("one", "1"); + collection.Add("two", "2"); + collection.Add(new MessageHeader("three", "3")); + + collection.Should().BeEquivalentTo( + new MessageHeader("one", "1"), + new MessageHeader("two", "2"), + new MessageHeader("three", "3")); + } + + [Fact] + public void AddOrReplace_ExistingHeader_ValueReplaced() + { + var collection = new MessageHeaderCollection(); + collection.Add("one", "1"); + collection.Add("two", "2"); + + collection.AddOrReplace("one", "1(2)"); + + + collection.Should().BeEquivalentTo( + new MessageHeader("one", "1(2)"), + new MessageHeader("two", "2")); + } + + [Fact] + public void AddOrReplace_NewHeader_HeaderAdded() + { + var collection = new MessageHeaderCollection(); + collection.Add("one", "1"); + collection.Add("two", "2"); + + collection.AddOrReplace("three", "3"); + + collection.Should().BeEquivalentTo( + new MessageHeader("one", "1"), + new MessageHeader("two", "2"), + new MessageHeader("three", "3")); + } + } +} \ No newline at end of file diff --git a/tests/Silverback.Integration.Tests/Messaging/Publishing/PublisherTests.cs b/tests/Silverback.Integration.Tests/Messaging/Publishing/PublisherTests.cs new file mode 100644 index 000000000..a834546f9 --- /dev/null +++ b/tests/Silverback.Integration.Tests/Messaging/Publishing/PublisherTests.cs @@ -0,0 +1,66 @@ +// Copyright (c) 2018-2019 Sergio Aquilini +// This code is licensed under MIT license (see LICENSE file for details) + +using System; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Silverback.Messaging.Configuration; +using Silverback.Messaging.Publishing; +using Silverback.Tests.Integration.TestTypes; +using Xunit; + +namespace Silverback.Tests.Integration.Messaging.Publishing +{ + /// + /// The purpose of this class is to ensure that the publisher is still working when + /// the broker subsribers are added. + /// + public class PublisherTests + { + private readonly IServiceProvider _serviceProvider; + private readonly IServiceProvider _scopedServiceProvider; + + public PublisherTests() + { + var services = new ServiceCollection(); + + services + .AddSingleton() + .AddSingleton(typeof(ILogger<>), typeof(NullLogger<>)); + + services + .AddBus() + .AddBroker(); + + _serviceProvider = services.BuildServiceProvider(new ServiceProviderOptions { ValidateScopes = true }); + _scopedServiceProvider = _serviceProvider.CreateScope().ServiceProvider; + } + + [Fact] + public void Publish_HandlersReturnValue_ResultsReturned() + { + _serviceProvider.GetService() + .Subscribe(_ => "response") + .Subscribe(_ => "response2"); + + var results = _scopedServiceProvider.GetService().Publish("test"); + + results.Should().Equal("response", "response2"); + } + + [Fact] + public async Task PublishAsync_HandlersReturnValue_ResultsReturned() + { + _serviceProvider.GetService() + .Subscribe(_ => "response") + .Subscribe(_ => "response2"); + + var results = await _scopedServiceProvider.GetService().PublishAsync("test"); + + results.Should().Equal("response", "response2"); + } + } +} diff --git a/tests/Silverback.Integration.Tests/Messaging/Serialization/JsonMessageSerializerTests.cs b/tests/Silverback.Integration.Tests/Messaging/Serialization/JsonMessageSerializerTests.cs index d73e5d2bf..c8b566298 100644 --- a/tests/Silverback.Integration.Tests/Messaging/Serialization/JsonMessageSerializerTests.cs +++ b/tests/Silverback.Integration.Tests/Messaging/Serialization/JsonMessageSerializerTests.cs @@ -42,5 +42,17 @@ public void SerializeDeserialize_HardcodedType_CorrectlyDeserialized() message2.Should().NotBeNull(); message2.Content.Should().Be(message.Content); } + + [Fact] + public void Serialize_ByteArray_ReturnedUnmodified() + { + var messageBytes = Encoding.UTF8.GetBytes("test"); + + var serializer = new JsonMessageSerializer(); + + var serialized = serializer.Serialize(messageBytes); + + serialized.Should().BeSameAs(messageBytes); + } } } diff --git a/tests/Silverback.Integration.Tests/Silverback.Integration.Tests.csproj b/tests/Silverback.Integration.Tests/Silverback.Integration.Tests.csproj index 1f36be6db..c7a4a8490 100644 --- a/tests/Silverback.Integration.Tests/Silverback.Integration.Tests.csproj +++ b/tests/Silverback.Integration.Tests/Silverback.Integration.Tests.csproj @@ -1,7 +1,7 @@  - netcoreapp2.0 + netcoreapp2.2 Silverback.Tests.Integration @@ -10,10 +10,15 @@ - - - - + + all + runtime; build; native; contentfiles; analyzers + + + + + + all diff --git a/tests/Silverback.Integration.Tests/TestTypes/Domain/IEvent.cs b/tests/Silverback.Integration.Tests/TestTypes/Domain/IEvent.cs index d5350a10a..d7dea61ed 100644 --- a/tests/Silverback.Integration.Tests/TestTypes/Domain/IEvent.cs +++ b/tests/Silverback.Integration.Tests/TestTypes/Domain/IEvent.cs @@ -1,5 +1,6 @@ // Copyright (c) 2018-2019 Sergio Aquilini // This code is licensed under MIT license (see LICENSE file for details) + namespace Silverback.Tests.Integration.TestTypes.Domain { public interface IEvent { } diff --git a/tests/Silverback.Integration.Tests/TestTypes/Domain/IIntegrationEvent.cs b/tests/Silverback.Integration.Tests/TestTypes/Domain/IIntegrationEvent.cs index e87743724..63a2b7f91 100644 --- a/tests/Silverback.Integration.Tests/TestTypes/Domain/IIntegrationEvent.cs +++ b/tests/Silverback.Integration.Tests/TestTypes/Domain/IIntegrationEvent.cs @@ -1,5 +1,6 @@ // Copyright (c) 2018-2019 Sergio Aquilini // This code is licensed under MIT license (see LICENSE file for details) + namespace Silverback.Tests.Integration.TestTypes.Domain { public interface IIntegrationEvent : IIntegrationMessage diff --git a/tests/Silverback.Integration.Tests/TestTypes/Domain/TestInternalEventOne.cs b/tests/Silverback.Integration.Tests/TestTypes/Domain/TestInternalEventOne.cs index 92fee06f3..3732dacbe 100644 --- a/tests/Silverback.Integration.Tests/TestTypes/Domain/TestInternalEventOne.cs +++ b/tests/Silverback.Integration.Tests/TestTypes/Domain/TestInternalEventOne.cs @@ -1,5 +1,6 @@ // Copyright (c) 2018-2019 Sergio Aquilini // This code is licensed under MIT license (see LICENSE file for details) + namespace Silverback.Tests.Integration.TestTypes.Domain { public class TestInternalEventOne : IEvent diff --git a/tests/Silverback.Integration.Tests/TestTypes/InMemoryChunkStore.cs b/tests/Silverback.Integration.Tests/TestTypes/InMemoryChunkStore.cs index a61c265e2..c5a2c4429 100644 --- a/tests/Silverback.Integration.Tests/TestTypes/InMemoryChunkStore.cs +++ b/tests/Silverback.Integration.Tests/TestTypes/InMemoryChunkStore.cs @@ -10,6 +10,8 @@ namespace Silverback.Tests.Integration.TestTypes { public class InMemoryChunkStore : TransactionalList, IChunkStore { + private string _pendingCleanup; + public void Store(MessageChunk chunk) => Add(new InMemoryStoredChunk { @@ -34,8 +36,23 @@ public Dictionary GetChunks(string messageId) => public void Cleanup(string messageId) { - Entries.RemoveAll(e => e.MessageId == messageId); - UncommittedEntries.RemoveAll(e => e.MessageId == messageId); + _pendingCleanup = messageId; + } + + public override void Commit() + { + if (!string.IsNullOrEmpty(_pendingCleanup)) + { + Entries.RemoveAll(e => e.MessageId == _pendingCleanup); + UncommittedEntries.RemoveAll(e => e.MessageId == _pendingCleanup); + } + base.Commit(); + } + + public override void Rollback() + { + _pendingCleanup = null; + base.Rollback(); } } } diff --git a/tests/Silverback.Integration.Tests/TestTypes/InMemoryStoredChunk.cs b/tests/Silverback.Integration.Tests/TestTypes/InMemoryStoredChunk.cs index 8a554c471..b75859afe 100644 --- a/tests/Silverback.Integration.Tests/TestTypes/InMemoryStoredChunk.cs +++ b/tests/Silverback.Integration.Tests/TestTypes/InMemoryStoredChunk.cs @@ -1,5 +1,6 @@ // Copyright (c) 2018-2019 Sergio Aquilini // This code is licensed under MIT license (see LICENSE file for details) + namespace Silverback.Tests.Integration.TestTypes { public class InMemoryStoredChunk diff --git a/tests/Silverback.Integration.Tests/TestTypes/TestConsumer.cs b/tests/Silverback.Integration.Tests/TestTypes/TestConsumer.cs index b9757f2ed..c5a2015ad 100644 --- a/tests/Silverback.Integration.Tests/TestTypes/TestConsumer.cs +++ b/tests/Silverback.Integration.Tests/TestTypes/TestConsumer.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Linq; -using Microsoft.Extensions.Logging.Abstractions; using Silverback.Messaging; using Silverback.Messaging.Broker; using Silverback.Messaging.Messages; @@ -15,7 +14,7 @@ namespace Silverback.Tests.Integration.TestTypes public class TestConsumer : Consumer { public TestConsumer(IBroker broker, IEndpoint endpoint) - : base(broker, endpoint, new NullLogger(), new MessageLogger(new MessageKeyProvider(Enumerable.Empty()))) + : base(broker, endpoint) { } diff --git a/tests/Silverback.Integration.Tests/TestTypes/TestErrorPolicy.cs b/tests/Silverback.Integration.Tests/TestTypes/TestErrorPolicy.cs index 48161eb60..147afb055 100644 --- a/tests/Silverback.Integration.Tests/TestTypes/TestErrorPolicy.cs +++ b/tests/Silverback.Integration.Tests/TestTypes/TestErrorPolicy.cs @@ -13,11 +13,11 @@ public class TestErrorPolicy : ErrorPolicyBase { public bool Applied { get; private set; } - public TestErrorPolicy() : base(NullLoggerFactory.Instance.CreateLogger(), new MessageLogger(new MessageKeyProvider(new[] { new DefaultPropertiesMessageKeyProvider() }))) + public TestErrorPolicy(IServiceProvider serviceProvider = null) : base(serviceProvider, NullLoggerFactory.Instance.CreateLogger(), new MessageLogger(new MessageKeyProvider(new[] { new DefaultPropertiesMessageKeyProvider() }))) { } - public override ErrorAction HandleError(FailedMessage failedMessage, Exception exception) + protected override ErrorAction ApplyPolicy(IInboundMessage message, Exception exception) { Applied = true; return ErrorAction.Skip; diff --git a/tests/Silverback.Integration.Tests/TestTypes/TestProducer.cs b/tests/Silverback.Integration.Tests/TestTypes/TestProducer.cs index 985de205a..30bb9a813 100644 --- a/tests/Silverback.Integration.Tests/TestTypes/TestProducer.cs +++ b/tests/Silverback.Integration.Tests/TestTypes/TestProducer.cs @@ -24,15 +24,16 @@ public TestProducer(TestBroker broker, IEndpoint endpoint) ProducedMessages = broker.ProducedMessages; } - protected override void Produce(object message, byte[] serializedMessage, IEnumerable headers) + protected override IOffset Produce(object message, byte[] serializedMessage, IEnumerable headers) { ProducedMessages.Add(new TestBroker.ProducedMessage(serializedMessage, headers, Endpoint)); + return null; } - protected override Task ProduceAsync(object message, byte[] serializedMessage, IEnumerable headers) + protected override Task ProduceAsync(object message, byte[] serializedMessage, IEnumerable headers) { Produce(message, serializedMessage, headers); - return Task.CompletedTask; + return Task.FromResult(null); } } } \ No newline at end of file diff --git a/tests/Silverback.Integration.Tests/TestTypes/TestSerializer.cs b/tests/Silverback.Integration.Tests/TestTypes/TestSerializer.cs new file mode 100644 index 000000000..258617e49 --- /dev/null +++ b/tests/Silverback.Integration.Tests/TestTypes/TestSerializer.cs @@ -0,0 +1,34 @@ +// Copyright (c) 2018-2019 Sergio Aquilini +// This code is licensed under MIT license (see LICENSE file for details) + +using System; +using Silverback.Messaging.LargeMessages; +using Silverback.Messaging.Serialization; + +namespace Silverback.Tests.Integration.TestTypes +{ + public class TestSerializer : IMessageSerializer + { + public int MustFailCount { get; set; } + + public int FailCount { get; private set; } + + public byte[] Serialize(object message) + { + throw new NotImplementedException(); + } + + public object Deserialize(byte[] message) + { + var deserialized = new JsonMessageSerializer().Deserialize(message); + + if (MustFailCount > FailCount && !(deserialized is MessageChunk)) + { + FailCount++; + throw new Exception("Test failure"); + } + + return deserialized; + } + } +} \ No newline at end of file diff --git a/tools/Silverback.Integration.Kafka.ConfigClassGenerator/Messaging/KafkaConsumerConfig.cs b/tools/Silverback.Integration.Kafka.ConfigClassGenerator/Messaging/KafkaConsumerConfig.cs index 03eff4f3f..128d4a9e6 100644 --- a/tools/Silverback.Integration.Kafka.ConfigClassGenerator/Messaging/KafkaConsumerConfig.cs +++ b/tools/Silverback.Integration.Kafka.ConfigClassGenerator/Messaging/KafkaConsumerConfig.cs @@ -1,6 +1,7 @@ // Copyright (c) 2018-2019 Sergio Aquilini // This code is licensed under MIT license (see LICENSE file for details) -namespace Silverback.Messaging + +namespace Silverback.Integration.Kafka.ConfigClassGenerator.Messaging { public class KafkaConsumerConfigGen { diff --git a/tools/Silverback.Integration.Kafka.ConfigClassGenerator/Program.cs b/tools/Silverback.Integration.Kafka.ConfigClassGenerator/Program.cs index a8c56f6ae..eb6b9a39c 100644 --- a/tools/Silverback.Integration.Kafka.ConfigClassGenerator/Program.cs +++ b/tools/Silverback.Integration.Kafka.ConfigClassGenerator/Program.cs @@ -3,16 +3,18 @@ using System; using System.IO; +using System.Reflection; -namespace ConfigClassGenerator +namespace Silverback.Integration.Kafka.ConfigClassGenerator { class Program { static void Main(string[] args) { var xmlDocumentationPath = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), - @".nuget\packages\confluent.kafka\1.0.0-beta2\lib\netstandard1.3\Confluent.Kafka.xml"); + Path.GetDirectoryName( + Assembly.GetAssembly(typeof(Confluent.Kafka.ClientConfig)).Location), + "Confluent.Kafka.xml"); var consumerConfig = new ProxyClassGenerator(typeof(Confluent.Kafka.ConsumerConfig), "ConfluentConsumerConfigProxy", xmlDocumentationPath).Generate(); Console.Write(consumerConfig); diff --git a/tools/Silverback.Integration.Kafka.ConfigClassGenerator/ProxyClassGenerator.cs b/tools/Silverback.Integration.Kafka.ConfigClassGenerator/ProxyClassGenerator.cs index f1ac0f8fe..1b6adee9e 100644 --- a/tools/Silverback.Integration.Kafka.ConfigClassGenerator/ProxyClassGenerator.cs +++ b/tools/Silverback.Integration.Kafka.ConfigClassGenerator/ProxyClassGenerator.cs @@ -10,7 +10,7 @@ using System.Text.RegularExpressions; using System.Xml; -namespace ConfigClassGenerator +namespace Silverback.Integration.Kafka.ConfigClassGenerator { class ProxyClassGenerator { @@ -94,7 +94,7 @@ private string GetSummary(PropertyInfo memberInfo) LoadXmlDoc(); var path = "P:" + memberInfo.DeclaringType.FullName + "." + memberInfo.Name; - var node = _xmlDoc.SelectSingleNode("//member[starts-with(@name, '" + path + "')]"); + var node = _xmlDoc?.SelectSingleNode("//member[starts-with(@name, '" + path + "')]"); if (node == null) return null; diff --git a/tools/Silverback.Integration.Kafka.ConfigClassGenerator/Silverback.Integration.Kafka.ConfigClassGenerator.csproj b/tools/Silverback.Integration.Kafka.ConfigClassGenerator/Silverback.Integration.Kafka.ConfigClassGenerator.csproj index 4aa8b3ed0..937dcc05f 100644 --- a/tools/Silverback.Integration.Kafka.ConfigClassGenerator/Silverback.Integration.Kafka.ConfigClassGenerator.csproj +++ b/tools/Silverback.Integration.Kafka.ConfigClassGenerator/Silverback.Integration.Kafka.ConfigClassGenerator.csproj @@ -7,7 +7,7 @@ - +