From 195b7616cbc85bd16df59cd2c1d57af3b87114aa Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Mon, 6 Nov 2023 08:29:12 +0000 Subject: [PATCH 1/3] Binary payloads (#2560) * First pass with Sqlite; errors * First pass at binary payload with Sqlite * Fix issues with SQL Parameter naming * Problem with Sqlite * Try to get Sqlite tests running * Erros with Sqlite * Fix Sqlite Inbox * Support binary payloads in Sqlite * Fix MySQL to support binary; Remove Sync suffix from Outbox name * Ensure bytes are written raw, not converted to a string, which is lossy * Run SQL Server tests on GA * Binary support for MSSQL; some naming clean up for Outboxes. Ensure that we encode * remove obsolete sql data types * Fix missing partition key header * Sqlite should support partition key * sqlite supports writing kafka partition key * Move Postgres to new format; add binary and partitionkey support * ensure consistency of headers * Fix unix timestamps * Fix issue with order of paged read * Fix tests broken now that headers don't auto-round times. * Fix up sql * Reduce precision of timestamp tests to improve success rate * Add an example for Kafka binary to make my life easier * Clean up errors in SQL tests * fix up sqlite * Fix mysql messages * missing files * Use the CreatedTime to ensure that we get the correct time * Fix SQL lite order by timestamp * Missing direction on sort * Make sure we can build * Kafka and Sqlite working with Serdes and Binary * Use RelationalDatebaseConfiguration not derived classes * Alter the interfaces to outboxes, to prevent need to cast * Fix base type * Move to new model for Dbconnection provision * Breadth of interface for async scenarios * Prevent sweeper running multiple threads * unit of work and connection provider * Ensure we register the new interfaces * entity framework is a type of transaction provider * Add commit and rollback to the provider * Binary not being set; relational db connection not transaction * use transactionconnectionprovider for lifetime * update non-binary Dapper sample * Flow move back to box transaction provider out * Move to a single abstraction for boxtransactionprovider * Modify for generic AddBrighter - need shifting of generic parameters to Outbox * Tests don't apply any more, default to an in memory outbox * Fix failing test * Fix failing mssql tests that open the connection * Make archiver non-generic * Make derived outboxes call useexternaloutbox * safety dance. * Builds; tests and samples need debugging * Ensure that we use the transaction on an external bus with an outbox * use reflection to remove need to pass generic type parameters * Remove DynamoDb extension for creation of outbox * Fix how we get the message mapper and transformer * Sample fixes * Was not marking dispatched as missing background thread * Add missing migrations to support multiple production databases * Dapper support for all dbs * safety dance * remove post overloads * safety dance * Fix issues with PostgresSql generated via Dapper.Extensions and schema created by FluentMigrator * Safety Dance prior to debug changes * Drop Dapper Extensions, simpler debugging * Move to Dapper; fix database access * Fixes to salutations for supporting all databases * Fix naming issues for the Salutations project * safewt dance * Fix DDL for Postgres * Fix exists SQL for inbox * Remove old DDL statements; improved exists checks * Improved outbox creation handling * Fix PostgreSQL Inbox issues * Fix inbox and outbox creation * Allow different transports * Ensure Kafka works on sample * Merged Kafka project, no longer needed * Remove redundant example elements, and improve README.md * Migrate ef core examples;fix issues with target framework type * Get SalutationAnalytics.csproj working with hostbuilder changes * Fix DynamoDb sample and tools * Remove out of date readmes; switch Dapper default to RMQ * ephemera that probably need removing from source control --- .github/workflows/ci.yml | 2 +- .../.idea/httpRequests/http-requests-log.http | 435 +++++++---- Brighter.sln | 76 +- Docker/dynamodb/shared-local-instance.db | Bin 0 -> 552960 bytes docker-compose-mssql.yaml | 6 +- .../GreetingsReceiverConsole/Program.cs | 1 - .../GreetingsScopedReceiverConsole/Program.cs | 1 - .../GreetingsSender.Web/Program.cs | 36 +- .../ASBTaskQueue/GreetingsSender/Program.cs | 43 +- .../ASBTaskQueue/GreetingsWorker/Program.cs | 5 +- .../AWSTaskQueue/GreetingsPumper/Program.cs | 26 +- .../GreetingsReceiverConsole/Program.cs | 1 - .../AWSTaskQueue/GreetingsSender/Program.cs | 26 +- .../GreetingsReceiverConsole/Program.cs | 1 - .../ClaimCheck/GreetingsSender/Program.cs | 32 +- .../GreetingsReceiverConsole/Program.cs | 1 - .../Compression/GreetingsSender/Program.cs | 32 +- samples/HelloAsyncListeners/Program.cs | 1 + samples/HelloWorld/Program.cs | 1 + samples/HelloWorldAsync/Program.cs | 1 + .../GreetingsSender/GreetingsSender.csproj | 1 - .../GreetingsSender/Program.cs | 42 +- .../KafkaTaskQueue/GreetingsSender/Program.cs | 44 +- .../CompetingReceiverConsole/Program.cs | 4 +- .../CompetingSender/Program.cs | 14 +- .../GreetingsReceiverConsole/Program.cs | 3 +- .../GreetingsSender/Program.cs | 19 +- samples/OpenTelemetry/Consumer/Program.cs | 8 +- samples/OpenTelemetry/Producer/Program.cs | 9 +- samples/OpenTelemetry/Sweeper/Program.cs | 16 +- .../GreetingsClient/Program.cs | 32 +- .../GreetingsServer/Program.cs | 30 +- .../GreetingsReceiverConsole/Program.cs | 1 - .../RMQTaskQueue/GreetingsSender/Program.cs | 49 +- .../GreetingsReceiver/Program.cs | 3 +- .../RedisTaskQueue/GreetingsSender/Program.cs | 27 +- .../WebAPI_Dapper/GreetingsEntities/Person.cs | 6 +- .../EntityMappers/GreetingsMapper.cs | 18 - .../EntityMappers/PersonMapper.cs | 17 - .../GreetingsPorts/GreetingsPorts.csproj | 12 +- .../Handlers/AddGreetingHandlerAsync.cs | 62 +- .../Handlers/AddPersonHandlerAsync.cs | 14 +- .../Handlers/DeletePersonHandlerAsync.cs | 52 +- .../FIndGreetingsForPersonHandlerAsync.cs | 27 +- .../Handlers/FindPersonByNameHandlerAsync.cs | 17 +- .../Responses/FindPersonResult.cs | 4 +- .../Controllers/PeopleController.cs | 2 +- .../GreetingsWeb/Database/OutboxExtensions.cs | 94 +-- .../GreetingsWeb/Database/SchemaCreation.cs | 234 ++++-- .../GreetingsWeb/GreetingsWeb.csproj | 18 +- .../Messaging/MessagingTransport.cs | 21 + samples/WebAPI_Dapper/GreetingsWeb/Program.cs | 2 +- .../Properties/launchSettings.json | 34 +- samples/WebAPI_Dapper/GreetingsWeb/Startup.cs | 251 ++++--- .../GreetingsWeb/appsettings.Production.json | 8 +- .../Greetings_Migrations.csproj} | 1 + .../Migrations/20220527_InitialCreate.cs | 19 +- .../Migrations/MigrationConfiguration.cs | 11 + .../Greetings_SqliteMigrations.csproj | 13 - .../Migrations/202204221833_InitialCreate.cs | 30 - samples/WebAPI_Dapper/README.md | 110 ++- .../Database/OutboxExtensions.cs | 67 ++ .../Database/SchemaCreation.cs | 322 +++++++- .../Messaging/MessagingTransport.cs | 21 + .../SalutationAnalytics/Program.cs | 411 ++++++++--- .../Properties/launchSettings.json | 44 +- .../SalutationAnalytics.csproj | 16 +- .../appsettings.Production.json | 8 +- .../SalutationEntities/Salutation.cs | 6 +- .../EntityMappers/SalutationMapper.cs | 15 - .../Handlers/GreetingMadeHandler.cs | 47 +- .../SalutationPorts/Policies/Retry.cs | 12 +- .../Policies/SalutationPolicy.cs | 4 +- .../SalutationPorts/SalutationPorts.csproj | 10 +- .../202205161812_SqlInitialMigrations.cs} | 6 +- .../Salutations_Migrations.csproj} | 0 .../Migrations/20220527_MySqlMigrations.cs | 20 - .../Salutations_mySqlMigrations.csproj | 13 - samples/WebAPI_Dapper/build.sh | 15 - samples/WebAPI_Dapper/docker-compose.yml | 51 -- .../Handlers/AddGreetingHandlerAsync.cs | 40 +- .../Handlers/AddPersonHandlerAsync.cs | 9 +- .../Handlers/DeletePersonHandlerAsync.cs | 9 +- .../FIndGreetingsForPersonHandlerAsync.cs | 9 +- .../Handlers/FindPersonByNameHandlerAsync.cs | 9 +- samples/WebAPI_Dynamo/GreetingsWeb/Startup.cs | 65 +- .../SalutationAnalytics/Program.cs | 58 +- .../Handlers/GreetingMadeHandler.cs | 54 +- .../SalutationPorts/Policies/Retry.cs | 12 +- .../Policies/SalutationPolicy.cs | 4 +- samples/WebAPI_Dynamo/tests.http | 2 +- .../GreetingsEntities.csproj | 2 +- .../GreetingsPorts/GreetingsPorts.csproj | 2 +- .../Handlers/AddGreetingHandlerAsync.cs | 31 +- .../GreetingsWeb/Database/SchemaCreation.cs | 3 +- .../GreetingsWeb/GreetingsWeb.csproj | 4 +- samples/WebAPI_EFCore/GreetingsWeb/Startup.cs | 183 ++--- .../Greetings_MySqlMigrations.csproj | 2 +- .../20210920201732_InitialCreate.Designer.cs | 2 +- .../20210920201732_InitialCreate.cs | 2 +- .../Greetings_SqliteMigrations.csproj | 2 +- .../20210902180249_Initial.Designer.cs | 2 +- .../Migrations/20210902180249_Initial.cs | 2 +- samples/WebAPI_EFCore/README.md | 92 +-- .../SalutationAnalytics/Program.cs | 149 ++-- .../SalutationAnalytics.csproj | 4 +- .../SalutationEntities.csproj | 2 +- .../Handlers/GreetingMadeHandler.cs | 43 +- .../SalutationPorts/Policies/Retry.cs | 12 +- .../Policies/SalutationPolicy.cs | 4 +- .../SalutationPorts/SalutationPorts.csproj | 2 +- .../Salutations_MySqlMigrations.csproj | 2 +- .../Salutations_SqliteMigrations.csproj | 2 +- samples/WebAPI_EFCore/build.sh | 15 - samples/WebAPI_EFCore/docker-compose.yml | 51 -- .../Orders.API/Program.cs | 25 +- .../Orders.Data/SqlConnectionProvider.cs | 19 +- .../Extensions/BrighterExtensions.cs | 20 +- .../Extensions/HealthCheckExtensions.cs | 3 +- .../BrighterOutboxConnectionHealthCheck.cs | 16 +- .../Settings/BrighterBoxSettings.cs | 1 + .../Orders.Worker/Program.cs | 5 +- src/Paramore.Brighter.Dapper/IUnitOfWork.cs | 42 -- .../DynamoDbTableBuilder.cs | 2 +- ...pDbTableQuery.cs => DynamoDbTableQuery.cs} | 2 +- .../DynamoDbUnitOfWork.cs | 104 ++- .../IAmADynamoDbConnectionProvider.cs | 15 + .../IAmADynamoDbTransactionProvider.cs | 10 + .../IDynamoDbClientProvider.cs | 46 -- .../IDynamoDbTransactionProvider.cs | 7 - .../BrighterOptions.cs | 28 +- .../IBrighterBuilder.cs | 13 +- ...hter.Extensions.DependencyInjection.csproj | 1 + .../ServiceCollectionBrighterBuilder.cs | 74 +- .../ServiceCollectionExtensions.cs | 541 +++++++------- .../HostedServiceCollectionExtensions.cs | 4 +- .../TimedOutboxArchiver.cs | 13 +- .../TimedOutboxSweeper.cs | 17 +- .../DDL Scripts/MSSQL/Inbox.sql | 28 - .../MsSqlInbox.cs | 34 +- .../Paramore.Brighter.Inbox.MsSql.csproj | 3 - .../SqlInboxBuilder.cs | 8 +- .../MySqlInbox.cs | 4 +- .../MySqlInboxBuilder.cs | 3 +- .../MySqlInboxConfiguration.cs | 54 -- .../Paramore.Brighter.Inbox.Postgres.csproj | 3 - ...PostgresSqlInbox.cs => PostgreSqlInbox.cs} | 71 +- ...oxBuilder.cs => PostgreSqlInboxBuilder.cs} | 4 +- .../DDL Scripts/SQLite/CommandStore.sql | 13 - src/Paramore.Brighter.Inbox.Sqlite/README.md | 47 -- src/Paramore.Brighter.Inbox.Sqlite/README.txt | 47 -- .../SqliteInbox.cs | 4 +- .../SqliteInboxConfiguration.cs | 54 -- .../KafkaDefaultMessageHeaderBuilder.cs | 14 +- .../KafkaMessageCreator.cs | 2 +- .../UnixTimestamp.cs | 48 -- .../MsSqlMessageConsumer.cs | 10 +- .../MsSqlMessageConsumerFactory.cs | 4 +- .../MsSqlMessageProducer.cs | 10 +- .../MsSqlProducerRegistryFactory.cs | 4 +- .../SqlQueues/MsSqlMessageQueue.cs | 37 +- .../RmqMessagePublisher.cs | 2 +- .../MsSqlAzureConnectionProviderBase.cs | 56 +- .../MsSqlChainedConnectionProvider.cs | 6 +- .../MsSqlDefaultAzureConnectionProvider.cs | 4 +- .../MsSqlManagedIdentityConnectionProvider.cs | 4 +- ...MsSqlSharedTokenCacheConnectionProvider.cs | 6 +- .../MsSqlVisualStudioConnectionProvider.cs | 4 +- .../MsSqlDapperConnectionProvider.cs | 49 -- .../Paramore.Brighter.MsSql.Dapper.csproj | 3 +- .../UnitOfWork.cs | 82 --- ...qlEntityFrameworkCoreConnectionProvider.cs | 73 +- .../IMsSqlConnectionProvider.cs | 15 - .../IMsSqlTransactionConnectionProvider.cs | 9 - .../MsSqlConfiguration.cs | 44 -- .../MsSqlConnectionProvider.cs | 51 ++ .../MsSqlSqlAuthConnectionProvider.cs | 42 -- .../MsSqlUnitOfWork.cs | 76 ++ .../MySqlDapperConnectionProvider.cs | 48 -- .../UnitOfWork.cs | 99 --- .../MySqlEntityFrameworkConnectionProvider.cs | 57 +- .../IMySqlConnectionProvider.cs | 65 -- .../IMySqlTransactionConnectionProvider.cs | 31 - .../MySqlConfiguration.cs | 67 -- .../MySqlConnectionProvider.cs | 90 +-- .../MySqlUnitOfWork.cs | 118 +++ .../DynamoDbOutbox.cs | 42 +- .../MessageItem.cs | 13 +- .../ServiceCollectionExtensions.cs | 64 -- .../EventStoreOutboxSync.cs | 47 +- .../ServiceCollectionExtensions.cs | 5 +- .../DDL_SCRIPTS/MSSQL/Outbox.sql | 46 -- .../MsSqlOutbox.cs | 333 ++++++--- .../MsSqlQueries.cs | 5 +- .../Paramore.Brighter.Outbox.MsSql.csproj | 3 - src/Paramore.Brighter.Outbox.MsSql/README.md | 47 -- src/Paramore.Brighter.Outbox.MsSql/README.txt | 47 -- .../ServiceCollectionExtensions.cs | 68 -- .../SqlOutboxBuilder.cs | 47 +- .../{MySqlOutboxSync.cs => MySqlOutbox.cs} | 273 ++++--- .../MySqlOutboxBuilder.cs | 43 +- .../MySqlQueries.cs | 19 +- .../ServiceCollectionExtensions.cs | 64 -- .../DDL_SCRIPTS/POSTGRESQL/Outbox.sql | 15 - ...Paramore.Brighter.Outbox.PostgreSql.csproj | 3 - .../PostgreSqlOutbox.cs | 470 ++++++++++++ ...oxBuilder.cs => PostgreSqlOutboxBulder.cs} | 44 +- .../PostgreSqlOutboxConfiguration.cs | 56 -- .../PostgreSqlOutboxSync.cs | 693 ------------------ .../PostgreSqlQueries.cs | 17 + .../ServiceCollectionExtensions.cs | 70 -- .../DDL_SCRIPTS/SQLite/Outbox.sql | 27 - src/Paramore.Brighter.Outbox.Sqlite/README.md | 47 -- .../README.txt | 47 -- .../ServiceCollectionExtensions.cs | 64 -- .../{SqliteOutboxSync.cs => SqliteOutbox.cs} | 254 +++++-- .../SqliteOutboxBuilder.cs | 39 +- .../SqliteQueries.cs | 7 +- ...hter.PostgreSql.EntityFrameworkCore.csproj | 3 - ...greSqlEntityFrameworkConnectionProvider.cs | 49 +- .../IPostgreSqlConnectionProvider.cs | 19 - ...PostgreSqlTransactionConnectionProvider.cs | 4 - .../Paramore.Brighter.PostgreSql.csproj | 4 +- .../PostgreSqlConfiguration.cs | 12 - .../PostgreSqlConnectionProvider.cs | 88 +++ .../PostgreSqlNpgsqlConnectionProvider.cs | 42 -- .../PostgreSqlUnitOfWork.cs | 127 ++++ .../ServiceActivatorOptions.cs | 32 +- .../ServiceCollectionExtensions.cs | 34 +- .../ControlBusReceiverBuilder.cs | 11 +- .../MessagePump.cs | 2 +- .../SqliteDapperConnectionProvider.cs | 48 -- .../UnitOfWork.cs | 87 --- ...SqliteEntityFrameworkConnectionProvider.cs | 55 +- .../ISqliteConnectionProvider.cs | 65 -- .../ISqliteTransactionConnectionProvider.cs | 31 - .../SqliteConfiguration.cs | 67 -- .../SqliteConnectionProvider.cs | 51 +- .../SqliteUnitOfWork.cs | 120 +++ src/Paramore.Brighter/CommandProcessor.cs | 540 +++++++------- .../CommandProcessorBuilder.cs | 216 +++--- .../CommittableTransactionProvider.cs | 62 ++ src/Paramore.Brighter/ControlBusSender.cs | 35 +- .../ControlBusSenderFactory.cs | 19 +- .../ExternalBusConfiguration.cs | 127 +++- src/Paramore.Brighter/ExternalBusServices.cs | 640 ++++++++++------ .../ExternalBusType.cs} | 26 +- .../IAmABoxTransactionConnectionProvider.cs | 7 - .../IAmABoxTransactionProvider.cs | 63 ++ src/Paramore.Brighter/IAmABulkOutboxAsync.cs | 14 +- src/Paramore.Brighter/IAmABulkOutboxSync.cs | 9 +- src/Paramore.Brighter/IAmACommandProcessor.cs | 144 +++- src/Paramore.Brighter/IAmAControlBusSender.cs | 31 +- .../IAmAControlBusSenderFactory.cs | 5 +- src/Paramore.Brighter/IAmAMessageRecoverer.cs | 6 +- .../IAmARelationalDatabaseConfiguration.cs | 33 + .../IAmARelationalDbConnectionProvider.cs | 27 + .../IAmATransactionConnectionProvider.cs | 6 + .../IAmAnExternalBusService.cs | 80 ++ src/Paramore.Brighter/IAmAnOutbox.cs | 3 +- src/Paramore.Brighter/IAmAnOutboxAsync.cs | 14 +- src/Paramore.Brighter/IAmAnOutboxSync.cs | 10 +- src/Paramore.Brighter/InMemoryOutbox.cs | 148 +++- src/Paramore.Brighter/Message.cs | 74 +- src/Paramore.Brighter/MessageBody.cs | 15 +- src/Paramore.Brighter/MessageHeader.cs | 15 +- src/Paramore.Brighter/MessageRecovery.cs | 18 +- .../Monitoring/Handlers/MonitorHandler.cs | 13 +- .../Handlers/MonitorHandlerAsync.cs | 9 +- src/Paramore.Brighter/OutboxArchiver.cs | 35 +- .../RelationDatabaseOutbox.cs | 164 +++-- .../RelationalDatabaseConfiguration.cs | 59 ++ .../RelationalDbConnectionProvider.cs | 119 +++ .../RelationalDbTransactionProvider.cs | 168 +++++ src/Paramore.Brighter/inboxConfiguration.cs | 11 + .../TestDoubles/FakeMessageProducer.cs | 6 +- .../{FakeOutboxSync.cs => FakeOutbox.cs} | 59 +- ...ipeline_With_Global_Inbox_And_Use_Inbox.cs | 6 +- ...e_With_Global_Inbox_And_Use_Inbox_Async.cs | 6 +- ..._PostBox_On_The_Command_Processor_Async.cs | 26 +- ...ling_A_Server_Via_The_Command_Processor.cs | 26 +- ...The_Command_Processor_With_No_In_Mapper.cs | 31 +- ...he_Command_Processor_With_No_Out_Mapper.cs | 30 +- ...a_The_Command_Processor_With_No_Timeout.cs | 28 +- ...PostBox_On_The_Command_Processor _Async.cs | 32 +- ...ng_The_PostBox_On_The_Command_Processor.cs | 28 +- ...positing_A_Message_In_The_Message_Store.cs | 28 +- ...ing_A_Message_In_The_Message_StoreAsync.cs | 31 +- ..._Message_In_The_Message_StoreAsync_Bulk.cs | 35 +- ...ing_A_Message_In_The_Message_Store_Bulk.cs | 24 +- ...ng_The_PostBox_On_The_Command_Processor.cs | 27 +- ..._PostBox_On_The_Command_Processor_Async.cs | 28 +- ...Default_Inbox_Into_The_Publish_Pipeline.cs | 1 + ...t_Inbox_Into_The_Publish_Pipeline_Async.cs | 47 +- ..._A_Default_Inbox_Into_The_Send_Pipeline.cs | 2 +- ...ault_Inbox_Into_The_Send_Pipeline_Async.cs | 1 + ...And_There_Is_No_Message_Mapper_Registry.cs | 27 +- ...ere_Is_No_Message_Mapper_Registry_Async.cs | 18 +- ...essage_And_There_Is_No_Message_Producer.cs | 17 +- ...A_Message_And_There_Is_No_Message_Store.cs | 83 --- ...age_And_There_Is_No_Message_Store_Async.cs | 84 --- ...ting_A_Message_To_The_Command_Processor.cs | 17 +- ..._Message_To_The_Command_Processor_Async.cs | 17 +- ..._Limit_Total_Writes_To_OutBox_In_Window.cs | 19 +- .../When_Posting_Via_A_Control_Bus_Sender.cs | 15 +- ..._Posting_Via_A_Control_Bus_Sender_Async.cs | 21 +- .../When_Posting_With_A_Default_Policy.cs | 25 +- ...Posting_With_An_In_Memory_Message_Store.cs | 11 +- ...g_With_An_In_Memory_Message_Store_Async.cs | 11 +- .../When_creating_a_control_bus_sender.cs | 23 +- .../TestDoubles/SpyCommandProcessor.cs | 123 +++- ..._Clearing_The_Outbox_A_Span_Is_Exported.cs | 17 +- ...ing_The_Outbox_async_A_Span_Is_Exported.cs | 19 +- ...a_transaction_between_outbox_and_entity.cs | 12 +- ...en_writing_a_utf8_message_to_the_outbox.cs | 3 +- ...ting_a_utf8_message_to_the_outbox_async.cs | 4 +- .../TestDifferentSetups.cs | 27 +- .../TestTransform.cs | 20 +- .../TestDoubles/FakeCommandProcessor.cs | 32 + .../When_posting_a_message.cs | 15 +- .../MsSqlTestHelper.cs | 65 +- ...are_recievied_and_Dispatched_bulk_Async.cs | 12 +- ...he_message_store_and_a_range_is_fetched.cs | 22 +- ...sage_store_and_a_range_is_fetched_async.cs | 25 +- ..._message_to_a_binary_body_message_store.cs | 107 +++ ..._writing_a_message_to_the_message_store.cs | 14 +- ...ng_a_message_to_the_message_store_async.cs | 3 +- ...ing_messages_to_the_message_store_async.cs | 42 +- .../MySqlTestHelper.cs | 25 +- .../When_removing_messages_from_the_outbox.cs | 32 +- ...en_the_message_is_already_in_the_outbox.cs | 8 +- ..._message_is_already_in_the_outbox_async.cs | 8 +- ...are_receivied_and_Dispatched_bulk_Async.cs | 4 +- ...sage_store_and_a_range_is_fetched_async.cs | 12 +- ...es_in_the_outbox_and_a_range_is_fetched.cs | 12 +- ...messages_within_an_interval_are_fetched.cs | 14 +- ..._message_in_the_sql_message_store_async.cs | 6 +- ...n_there_is_no_message_in_the_sql_outbox.cs | 6 +- ...iting_a_message_to_a_binary_body_outbox.cs | 87 +++ ...ng_a_message_to_the_message_store_async.cs | 11 +- .../When_writing_a_message_to_the_outbox.cs | 15 +- .../When_writing_messages_to_the_outbox.cs | 44 +- ...en_writing_messages_to_the_outbox_async.cs | 44 +- ...e_message_Is_already_in_the_Inbox_async.cs | 4 +- ...hen_the_message_is_already_in_the_inbox.cs | 4 +- ...re_Is_no_message_in_the_sql_inbox_async.cs | 4 +- ...en_there_is_no_message_in_the_sql_inbox.cs | 4 +- .../When_writing_a_message_to_the_inbox.cs | 4 +- ...en_writing_a_message_to_the_inbox_async.cs | 4 +- .../When_Removing_Messages_From_The_Outbox.cs | 20 +- ...en_The_Message_Is_Already_In_The_Outbox.cs | 8 +- ...es_In_The_Outbox_And_A_Range_Is_Fetched.cs | 26 +- ...n_There_Is_No_Message_In_The_Sql_Outbox.cs | 6 +- ...iting_A_Message_To_A_Binary_Body_Outbox.cs | 89 +++ .../When_Writing_A_Message_To_The_Outbox.cs | 16 +- .../When_Writing_Messages_To_The_Outbox.cs | 42 +- ...Paramore.Brighter.PostgresSQL.Tests.csproj | 6 + .../PostgresSqlTestHelper.cs | 18 +- ...layed_message_via_the_messaging_gateway.cs | 2 +- ...e_message_is_already_in_The_inbox_async.cs | 2 +- ...hen_the_message_is_already_in_the_inbox.cs | 2 +- ..._is_no_message_in_the_sql_command_store.cs | 2 +- ..._message_in_the_sql_command_store_async.cs | 2 +- ..._writing_a_message_to_the_command_store.cs | 2 +- ...ng_a_message_to_the_command_store_async.cs | 2 +- .../Outbox/SQlOutboxMigrationTests.cs | 11 +- .../When_Removing_Messages_From_The_Outbox.cs | 20 +- ...en_The_Message_Is_Already_In_The_Outbox.cs | 8 +- ..._Message_Is_Already_In_The_Outbox_Async.cs | 8 +- ...es_In_The_Outbox_And_A_Range_Is_Fetched.cs | 12 +- ...The_Outbox_And_A_Range_Is_Fetched_Async.cs | 12 +- ...n_There_Is_No_Message_In_The_Sql_Outbox.cs | 6 +- ...e_Is_No_Message_In_The_Sql_Outbox_Async.cs | 6 +- ...iting_A_Message_To_A_Binary_Body_Outbox.cs | 120 +++ .../When_Writing_A_Message_To_The_Outbox.cs | 15 +- ...n_Writing_A_Message_To_The_Outbox_Async.cs | 13 +- .../When_Writing_Messages_To_The_Outbox.cs | 42 +- ...en_Writing_Messages_To_The_Outbox_Async.cs | 46 +- ..._are_received_and_Dispatched_bulk_Async.cs | 6 +- .../SqliteTestHelper.cs | 41 +- 380 files changed, 8968 insertions(+), 7077 deletions(-) create mode 100644 Docker/dynamodb/shared-local-instance.db delete mode 100644 samples/WebAPI_Dapper/GreetingsPorts/EntityMappers/GreetingsMapper.cs delete mode 100644 samples/WebAPI_Dapper/GreetingsPorts/EntityMappers/PersonMapper.cs create mode 100644 samples/WebAPI_Dapper/GreetingsWeb/Messaging/MessagingTransport.cs rename samples/WebAPI_Dapper/{Greetings_MySqlMigrations/Greetings_MySqlMigrations.csproj => Greetings_Migrations/Greetings_Migrations.csproj} (83%) rename samples/WebAPI_Dapper/{Greetings_MySqlMigrations => Greetings_Migrations}/Migrations/20220527_InitialCreate.cs (56%) create mode 100644 samples/WebAPI_Dapper/Greetings_Migrations/Migrations/MigrationConfiguration.cs delete mode 100644 samples/WebAPI_Dapper/Greetings_SqliteMigrations/Greetings_SqliteMigrations.csproj delete mode 100644 samples/WebAPI_Dapper/Greetings_SqliteMigrations/Migrations/202204221833_InitialCreate.cs create mode 100644 samples/WebAPI_Dapper/SalutationAnalytics/Database/OutboxExtensions.cs create mode 100644 samples/WebAPI_Dapper/SalutationAnalytics/Messaging/MessagingTransport.cs delete mode 100644 samples/WebAPI_Dapper/SalutationPorts/EntityMappers/SalutationMapper.cs rename samples/WebAPI_Dapper/{Salutations_SqliteMigrations/Migrations/202205161812_SqliteMigrations.cs => Salutations_Migrations/Migrations/202205161812_SqlInitialMigrations.cs} (62%) rename samples/WebAPI_Dapper/{Salutations_SqliteMigrations/Salutations_SqliteMigrations.csproj => Salutations_Migrations/Salutations_Migrations.csproj} (100%) delete mode 100644 samples/WebAPI_Dapper/Salutations_mySqlMigrations/Migrations/20220527_MySqlMigrations.cs delete mode 100644 samples/WebAPI_Dapper/Salutations_mySqlMigrations/Salutations_mySqlMigrations.csproj delete mode 100644 samples/WebAPI_Dapper/build.sh delete mode 100644 samples/WebAPI_Dapper/docker-compose.yml delete mode 100644 samples/WebAPI_EFCore/build.sh delete mode 100644 samples/WebAPI_EFCore/docker-compose.yml delete mode 100644 src/Paramore.Brighter.Dapper/IUnitOfWork.cs rename src/Paramore.Brighter.DynamoDb/{DynampDbTableQuery.cs => DynamoDbTableQuery.cs} (97%) create mode 100644 src/Paramore.Brighter.DynamoDb/IAmADynamoDbConnectionProvider.cs create mode 100644 src/Paramore.Brighter.DynamoDb/IAmADynamoDbTransactionProvider.cs delete mode 100644 src/Paramore.Brighter.DynamoDb/IDynamoDbClientProvider.cs delete mode 100644 src/Paramore.Brighter.DynamoDb/IDynamoDbTransactionProvider.cs delete mode 100644 src/Paramore.Brighter.Inbox.MsSql/DDL Scripts/MSSQL/Inbox.sql delete mode 100644 src/Paramore.Brighter.Inbox.MySql/MySqlInboxConfiguration.cs rename src/Paramore.Brighter.Inbox.Postgres/{PostgresSqlInbox.cs => PostgreSqlInbox.cs} (76%) rename src/Paramore.Brighter.Inbox.Postgres/{PostgresSqlInboxBuilder.cs => PostgreSqlInboxBuilder.cs} (93%) delete mode 100644 src/Paramore.Brighter.Inbox.Sqlite/DDL Scripts/SQLite/CommandStore.sql delete mode 100644 src/Paramore.Brighter.Inbox.Sqlite/README.md delete mode 100644 src/Paramore.Brighter.Inbox.Sqlite/README.txt delete mode 100644 src/Paramore.Brighter.Inbox.Sqlite/SqliteInboxConfiguration.cs delete mode 100644 src/Paramore.Brighter.MessagingGateway.Kafka/UnixTimestamp.cs delete mode 100644 src/Paramore.Brighter.MsSql.Dapper/MsSqlDapperConnectionProvider.cs delete mode 100644 src/Paramore.Brighter.MsSql.Dapper/UnitOfWork.cs delete mode 100644 src/Paramore.Brighter.MsSql/IMsSqlConnectionProvider.cs delete mode 100644 src/Paramore.Brighter.MsSql/IMsSqlTransactionConnectionProvider.cs delete mode 100644 src/Paramore.Brighter.MsSql/MsSqlConfiguration.cs create mode 100644 src/Paramore.Brighter.MsSql/MsSqlConnectionProvider.cs delete mode 100644 src/Paramore.Brighter.MsSql/MsSqlSqlAuthConnectionProvider.cs create mode 100644 src/Paramore.Brighter.MsSql/MsSqlUnitOfWork.cs delete mode 100644 src/Paramore.Brighter.MySql.Dapper/MySqlDapperConnectionProvider.cs delete mode 100644 src/Paramore.Brighter.MySql.Dapper/UnitOfWork.cs delete mode 100644 src/Paramore.Brighter.MySql/IMySqlConnectionProvider.cs delete mode 100644 src/Paramore.Brighter.MySql/IMySqlTransactionConnectionProvider.cs delete mode 100644 src/Paramore.Brighter.MySql/MySqlConfiguration.cs create mode 100644 src/Paramore.Brighter.MySql/MySqlUnitOfWork.cs delete mode 100644 src/Paramore.Brighter.Outbox.DynamoDB/ServiceCollectionExtensions.cs delete mode 100644 src/Paramore.Brighter.Outbox.MsSql/DDL_SCRIPTS/MSSQL/Outbox.sql delete mode 100644 src/Paramore.Brighter.Outbox.MsSql/README.md delete mode 100644 src/Paramore.Brighter.Outbox.MsSql/README.txt delete mode 100644 src/Paramore.Brighter.Outbox.MsSql/ServiceCollectionExtensions.cs rename src/Paramore.Brighter.Outbox.MySql/{MySqlOutboxSync.cs => MySqlOutbox.cs} (54%) delete mode 100644 src/Paramore.Brighter.Outbox.MySql/ServiceCollectionExtensions.cs delete mode 100644 src/Paramore.Brighter.Outbox.PostgreSql/DDL_SCRIPTS/POSTGRESQL/Outbox.sql create mode 100644 src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutbox.cs rename src/Paramore.Brighter.Outbox.PostgreSql/{PostgreSqlOutboxBuilder.cs => PostgreSqlOutboxBulder.cs} (59%) delete mode 100644 src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutboxConfiguration.cs delete mode 100644 src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutboxSync.cs create mode 100644 src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlQueries.cs delete mode 100644 src/Paramore.Brighter.Outbox.PostgreSql/ServiceCollectionExtensions.cs delete mode 100644 src/Paramore.Brighter.Outbox.Sqlite/DDL_SCRIPTS/SQLite/Outbox.sql delete mode 100644 src/Paramore.Brighter.Outbox.Sqlite/README.md delete mode 100644 src/Paramore.Brighter.Outbox.Sqlite/README.txt delete mode 100644 src/Paramore.Brighter.Outbox.Sqlite/ServiceCollectionExtensions.cs rename src/Paramore.Brighter.Outbox.Sqlite/{SqliteOutboxSync.cs => SqliteOutbox.cs} (59%) delete mode 100644 src/Paramore.Brighter.PostgreSql/IPostgreSqlConnectionProvider.cs delete mode 100644 src/Paramore.Brighter.PostgreSql/IPostgreSqlTransactionConnectionProvider.cs delete mode 100644 src/Paramore.Brighter.PostgreSql/PostgreSqlConfiguration.cs create mode 100644 src/Paramore.Brighter.PostgreSql/PostgreSqlConnectionProvider.cs delete mode 100644 src/Paramore.Brighter.PostgreSql/PostgreSqlNpgsqlConnectionProvider.cs create mode 100644 src/Paramore.Brighter.PostgreSql/PostgreSqlUnitOfWork.cs delete mode 100644 src/Paramore.Brighter.Sqlite.Dapper/SqliteDapperConnectionProvider.cs delete mode 100644 src/Paramore.Brighter.Sqlite.Dapper/UnitOfWork.cs delete mode 100644 src/Paramore.Brighter.Sqlite/ISqliteConnectionProvider.cs delete mode 100644 src/Paramore.Brighter.Sqlite/ISqliteTransactionConnectionProvider.cs delete mode 100644 src/Paramore.Brighter.Sqlite/SqliteConfiguration.cs create mode 100644 src/Paramore.Brighter.Sqlite/SqliteUnitOfWork.cs create mode 100644 src/Paramore.Brighter/CommittableTransactionProvider.cs rename src/{Paramore.Brighter.Inbox.Postgres/PostgresSqlInboxConfiguration.cs => Paramore.Brighter/ExternalBusType.cs} (60%) delete mode 100644 src/Paramore.Brighter/IAmABoxTransactionConnectionProvider.cs create mode 100644 src/Paramore.Brighter/IAmABoxTransactionProvider.cs create mode 100644 src/Paramore.Brighter/IAmARelationalDatabaseConfiguration.cs create mode 100644 src/Paramore.Brighter/IAmARelationalDbConnectionProvider.cs create mode 100644 src/Paramore.Brighter/IAmATransactionConnectionProvider.cs create mode 100644 src/Paramore.Brighter/IAmAnExternalBusService.cs create mode 100644 src/Paramore.Brighter/RelationalDatabaseConfiguration.cs create mode 100644 src/Paramore.Brighter/RelationalDbConnectionProvider.cs create mode 100644 src/Paramore.Brighter/RelationalDbTransactionProvider.cs rename tests/Paramore.Brighter.Core.Tests/CommandProcessors/TestDoubles/{FakeOutboxSync.cs => FakeOutbox.cs} (80%) delete mode 100644 tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Posting_A_Message_And_There_Is_No_Message_Store.cs delete mode 100644 tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Posting_A_Message_And_There_Is_No_Message_Store_Async.cs create mode 100644 tests/Paramore.Brighter.MSSQL.Tests/Outbox/When_writing_a_message_to_a_binary_body_message_store.cs create mode 100644 tests/Paramore.Brighter.MySQL.Tests/Outbox/When_writing_a_message_to_a_binary_body_outbox.cs create mode 100644 tests/Paramore.Brighter.PostgresSQL.Tests/Outbox/When_Writing_A_Message_To_A_Binary_Body_Outbox.cs create mode 100644 tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_Writing_A_Message_To_A_Binary_Body_Outbox.cs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7e3fb64d8f..0f5d182a94 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -289,7 +289,7 @@ jobs: - 11433:1433 env: ACCEPT_EULA: Y - SA_PASSWORD: Password1! + SA_PASSWORD: Password123! steps: - uses: actions/checkout@v4 - name: Setup dotnet 6 diff --git a/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http b/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http index c4ff58a6e2..10e80c8e3b 100644 --- a/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http +++ b/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http @@ -1,491 +1,660 @@ -GET http://localhost:5000/People/Tyrion +POST http://localhost:5000/Greetings/Tyrion/new +Content-Type: application/json +Content-Length: 47 Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -### - -POST http://localhost:5000/Greetings/Tyrion/new -Content-Type: application/json - { "Greeting" : "I drink, and I know things" } -<> 2022-11-08T211212.200.json +<> 2023-07-10T160215.200.json ### POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json +Content-Length: 47 +Connection: Keep-Alive +User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) +Accept-Encoding: br,deflate,gzip,x-gzip { "Greeting" : "I drink, and I know things" } -<> 2022-11-08T211208.200.json +<> 2023-07-10T160214.200.json ### POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json +Content-Length: 47 +Connection: Keep-Alive +User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) +Accept-Encoding: br,deflate,gzip,x-gzip { "Greeting" : "I drink, and I know things" } -<> 2022-11-08T211200.200.json +<> 2023-07-10T160213.200.json ### -POST http://localhost:5000/People/new +POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json +Content-Length: 47 +Connection: Keep-Alive +User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) +Accept-Encoding: br,deflate,gzip,x-gzip { - "Name" : "Tyrion" + "Greeting" : "I drink, and I know things" } -<> 2022-11-08T211124.200.json +<> 2023-07-10T160212.200.json ### -POST http://localhost:5000/People/new +POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json +Content-Length: 47 +Connection: Keep-Alive +User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) +Accept-Encoding: br,deflate,gzip,x-gzip { - "Name" : "Tyrion" + "Greeting" : "I drink, and I know things" } -<> 2022-11-08T200350.200.json +<> 2023-07-10T160211-1.200.json ### POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json +Content-Length: 47 +Connection: Keep-Alive +User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) +Accept-Encoding: br,deflate,gzip,x-gzip { "Greeting" : "I drink, and I know things" } -<> 2022-10-21T115924.200.json +<> 2023-07-10T160211.200.json ### -GET http://localhost:5000/People/Tyrion - -<> 2022-10-21T115848.200.json - -### - -POST http://localhost:5000/People/new +POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json +Content-Length: 47 +Connection: Keep-Alive +User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) +Accept-Encoding: br,deflate,gzip,x-gzip { - "Name" : "Tyrion" + "Greeting" : "I drink, and I know things" } -<> 2022-10-21T115844.200.json - -### - -GET http://localhost:5000/People/Tyrion - -<> 2022-10-21T115838.500.json +<> 2023-07-10T160210.200.json ### -POST http://localhost:5000/People/new +POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json +Content-Length: 47 +Connection: Keep-Alive +User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) +Accept-Encoding: br,deflate,gzip,x-gzip { - "Name" : "Tyrion" + "Greeting" : "I drink, and I know things" } -<> 2022-10-19T215040.200.json - -### - -GET http://localhost:5000/People/Tyrion - -<> 2022-10-19T214755.500.json +<> 2023-07-10T160209.200.json ### POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json +Content-Length: 47 +Connection: Keep-Alive +User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) +Accept-Encoding: br,deflate,gzip,x-gzip { "Greeting" : "I drink, and I know things" } -<> 2022-10-19T204831.200.json +<> 2023-07-10T124554.200.json ### POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json +Content-Length: 47 +Connection: Keep-Alive +User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) +Accept-Encoding: br,deflate,gzip,x-gzip { "Greeting" : "I drink, and I know things" } -<> 2022-10-19T204823.200.json +<> 2023-07-10T124553-1.200.json ### POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json +Content-Length: 47 +Connection: Keep-Alive +User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) +Accept-Encoding: br,deflate,gzip,x-gzip { "Greeting" : "I drink, and I know things" } -<> 2022-10-19T204723.200.json +<> 2023-07-10T124553.200.json ### -POST http://localhost:5000/People/new +POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json +Content-Length: 47 +Connection: Keep-Alive +User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) +Accept-Encoding: br,deflate,gzip,x-gzip { - "Name" : "Tyrion" + "Greeting" : "I drink, and I know things" } -<> 2022-10-19T201347.200.json - -### - -GET http://localhost:5000/People/Tyrion - -<> 2022-10-19T201322.500.json +<> 2023-07-10T124551.200.json ### -GET http://localhost:5000/People/Tyrion - -<> 2022-10-19T195123.200.json - -### - -GET http://localhost:5000/People/Tyrion - -<> 2022-10-19T194030.200.json - -### - -POST http://localhost:5000/People/new +POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json +Content-Length: 47 +Connection: Keep-Alive +User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) +Accept-Encoding: br,deflate,gzip,x-gzip { - "Name" : "Tyrion" + "Greeting" : "I drink, and I know things" } -<> 2022-10-19T194012.500.json +<> 2023-07-10T124550.200.json ### -POST http://localhost:5000/People/new +POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json +Content-Length: 47 +Connection: Keep-Alive +User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) +Accept-Encoding: br,deflate,gzip,x-gzip { - "Name" : "Tyrion" + "Greeting" : "I drink, and I know things" } -<> 2022-10-19T190032.500.json +<> 2023-07-10T124549.200.json ### -POST http://localhost:5000/People/new +POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json +Content-Length: 47 +Connection: Keep-Alive +User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) +Accept-Encoding: br,deflate,gzip,x-gzip { - "Name" : "Tyrion" + "Greeting" : "I drink, and I know things" } -<> 2022-10-19T185850.500.json +<> 2023-07-10T124547.200.json ### -POST http://localhost:5000/People/new +POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json +Content-Length: 47 +Connection: Keep-Alive +User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) +Accept-Encoding: br,deflate,gzip,x-gzip { - "Name" : "Tyrion" + "Greeting" : "I drink, and I know things" } -<> 2022-10-19T184041.500.json +<> 2023-07-10T124546.200.json ### -POST http://localhost:5000/People/new +POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json +Content-Length: 47 +Connection: Keep-Alive +User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) +Accept-Encoding: br,deflate,gzip,x-gzip { - "Name" : "Tyrion" + "Greeting" : "I drink, and I know things" } -<> 2022-10-19T183305.500.json +<> 2023-07-10T124544.200.json ### GET http://localhost:5000/People/Tyrion +Connection: Keep-Alive +User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) +Accept-Encoding: br,deflate,gzip,x-gzip -<> 2022-10-19T180348.500.html +<> 2023-07-10T124540.200.json ### -POST http://localhost:5000/People/new +GET http://localhost:5000/Greetings/Tyrion +Connection: Keep-Alive +User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) +Accept-Encoding: br,deflate,gzip,x-gzip + +<> 2023-07-07T200406.200.json + +### + +POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json +Content-Length: 47 +Connection: Keep-Alive +User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) +Accept-Encoding: br,deflate,gzip,x-gzip { - "Name" : "Tyrion" + "Greeting" : "I drink, and I know things" } -<> 2022-10-19T180248.500.html +<> 2023-07-07T200403.200.json ### -DELETE http://localhost:5000/People/Tyrion +GET http://localhost:5000/People/Tyrion +Connection: Keep-Alive +User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) +Accept-Encoding: br,deflate,gzip,x-gzip -<> 2022-10-19T180240.500.html +<> 2023-07-07T200359.200.json ### POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json +Content-Length: 50 +Connection: Keep-Alive +User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) +Accept-Encoding: br,deflate,gzip,x-gzip { - "Greeting" : "I drink, and I know things" + "Greeting" : "I drink, and I know things #1" } -<> 2022-06-25T125829.200.json - ### -POST http://localhost:5000/People/new +POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json +Content-Length: 50 +Connection: Keep-Alive +User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) +Accept-Encoding: br,deflate,gzip,x-gzip { - "Name" : "Tyrion" + "Greeting" : "I drink, and I know things #1" } -<> 2022-06-25T125806.200.json +<> 2023-07-07T195459.200.json ### POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json +Content-Length: 52 +Connection: Keep-Alive +User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) +Accept-Encoding: br,deflate,gzip,x-gzip { - "Greeting" : "I drink, and I know things" + "Greeting" : "I drink, and I know more things" } -<> 2022-06-24T234446.200.json +<> 2023-07-07T192903.200.json ### -POST http://localhost:5000/People/new +POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json +Content-Length: 47 +Connection: Keep-Alive +User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) +Accept-Encoding: br,deflate,gzip,x-gzip { - "Name" : "Tyrion" + "Greeting" : "I drink, and I know things" } -<> 2022-06-24T224322.200.json +<> 2023-07-07T190420.200.json ### -GET http://localhost:5000/People/Tyrion +GET http://localhost:5000/Greetings/Tyrion +Connection: Keep-Alive +User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) +Accept-Encoding: br,deflate,gzip,x-gzip -<> 2022-06-24T224317.500.json +<> 2023-07-07T190355.200.json ### POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json +Content-Length: 47 +Connection: Keep-Alive +User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) +Accept-Encoding: br,deflate,gzip,x-gzip { "Greeting" : "I drink, and I know things" } +<> 2023-07-07T190352.200.json + ### -POST http://localhost:5000/Greetings/Tyrion/new -Content-Type: application/json +GET http://localhost:5000/People/Tyrion +Connection: Keep-Alive +User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) +Accept-Encoding: br,deflate,gzip,x-gzip -{ - "Greeting" : "I drink, and I know things" -} +<> 2023-07-07T190349.200.json ### -POST http://localhost:5000/Greetings/Tyrion/new -Content-Type: application/json +GET http://localhost:5000/Greetings/Tyrion +Connection: Keep-Alive +User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) +Accept-Encoding: br,deflate,gzip,x-gzip -{ - "Greeting" : "I drink, and I know things" -} +<> 2023-07-07T165603.200.json ### POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json +Content-Length: 47 +Connection: Keep-Alive +User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) +Accept-Encoding: br,deflate,gzip,x-gzip { "Greeting" : "I drink, and I know things" } +<> 2023-07-07T165600.200.json + ### POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json +Content-Length: 47 +Connection: Keep-Alive +User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) +Accept-Encoding: br,deflate,gzip,x-gzip { "Greeting" : "I drink, and I know things" } -<> 2022-06-20T193453.200.json +<> 2023-07-07T165559.200.json ### POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json +Content-Length: 47 +Connection: Keep-Alive +User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) +Accept-Encoding: br,deflate,gzip,x-gzip { "Greeting" : "I drink, and I know things" } -<> 2022-06-20T193452.200.json +<> 2023-07-07T165557.200.json ### POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json +Content-Length: 47 +Connection: Keep-Alive +User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) +Accept-Encoding: br,deflate,gzip,x-gzip { "Greeting" : "I drink, and I know things" } -<> 2022-06-20T193451.200.json +<> 2023-07-07T165555.200.json ### -POST http://localhost:5000/Greetings/Tyrion/new -Content-Type: application/json +GET http://localhost:5000/Greetings/Tyrion +Connection: Keep-Alive +User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) +Accept-Encoding: br,deflate,gzip,x-gzip -{ - "Greeting" : "I drink, and I know things" -} +<> 2023-07-07T165552.200.json + +### -<> 2022-06-20T193450.200.json +GET http://localhost:5000/People/Tyrion +Connection: Keep-Alive +User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) +Accept-Encoding: br,deflate,gzip,x-gzip + +<> 2023-07-07T165549.200.json ### -POST http://localhost:5000/Greetings/Tyrion/new +GET http://localhost:5000/People/Tyrion +Connection: Keep-Alive +User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) +Accept-Encoding: br,deflate,gzip,x-gzip + +<> 2023-07-07T165406.200.json + +### + +POST http://localhost:5000/People/new Content-Type: application/json +Content-Length: 23 +Connection: Keep-Alive +User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) +Accept-Encoding: br,deflate,gzip,x-gzip { - "Greeting" : "I drink, and I know things" + "Name" : "Tyrion" } -<> 2022-06-20T193431.200.json +<> 2023-07-07T165401.200.json + +### + +GET http://localhost:5000/People/Tyrion +Connection: Keep-Alive +User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) +Accept-Encoding: br,deflate,gzip,x-gzip + +<> 2023-07-07T165354.500.json ### POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json +Content-Length: 47 +Connection: Keep-Alive +User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) +Accept-Encoding: br,deflate,gzip,x-gzip { "Greeting" : "I drink, and I know things" } -<> 2022-06-20T193430.200.json +<> 2023-07-04T212401.200.json ### POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json +Content-Length: 47 +Connection: Keep-Alive +User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) +Accept-Encoding: br,deflate,gzip,x-gzip { "Greeting" : "I drink, and I know things" } -<> 2022-06-20T193429.200.json +<> 2023-07-04T212358.200.json ### POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json +Content-Length: 47 +Connection: Keep-Alive +User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) +Accept-Encoding: br,deflate,gzip,x-gzip { "Greeting" : "I drink, and I know things" } -<> 2022-06-20T193428.200.json +<> 2023-07-04T212356.200.json ### POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json +Content-Length: 47 +Connection: Keep-Alive +User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) +Accept-Encoding: br,deflate,gzip,x-gzip { "Greeting" : "I drink, and I know things" } -<> 2022-06-20T193427.200.json +<> 2023-07-04T211455.200.json ### POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json +Content-Length: 47 +Connection: Keep-Alive +User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) +Accept-Encoding: br,deflate,gzip,x-gzip { "Greeting" : "I drink, and I know things" } -<> 2022-06-20T193426.200.json +<> 2023-07-04T202333.200.json ### POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json +Content-Length: 47 +Connection: Keep-Alive +User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) +Accept-Encoding: br,deflate,gzip,x-gzip { "Greeting" : "I drink, and I know things" } -<> 2022-06-20T193423.200.json +<> 2023-07-04T202332.200.json ### POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json +Content-Length: 47 +Connection: Keep-Alive +User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) +Accept-Encoding: br,deflate,gzip,x-gzip { "Greeting" : "I drink, and I know things" } -<> 2022-06-20T193420.200.json +<> 2023-07-04T202330.200.json ### -POST http://localhost:5000/Greetings/Tyrion/new -Content-Type: application/json +GET http://localhost:5000/People/Tyrion +Connection: Keep-Alive +User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) +Accept-Encoding: br,deflate,gzip,x-gzip -{ - "Greeting" : "I drink, and I know things" -} +<> 2023-07-04T202327.200.json + +### + +GET http://localhost:5000/People/Tyrion +Connection: Keep-Alive +User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) +Accept-Encoding: br,deflate,gzip,x-gzip + +<> 2023-07-04T201903.200.json ### POST http://localhost:5000/People/new Content-Type: application/json +Content-Length: 23 +Connection: Keep-Alive +User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) +Accept-Encoding: br,deflate,gzip,x-gzip { "Name" : "Tyrion" } -<> 2022-06-20T192613.200.json +<> 2023-07-04T201859.200.json + +### + +GET http://localhost:5000/People/Tyrion +Connection: Keep-Alive +User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) +Accept-Encoding: br,deflate,gzip,x-gzip + +### + +GET http://localhost:5000/People/Tyrion +Connection: Keep-Alive +User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) +Accept-Encoding: br,deflate,gzip,x-gzip ### diff --git a/Brighter.sln b/Brighter.sln index 8edb695c60..b060ce5afc 100644 --- a/Brighter.sln +++ b/Brighter.sln @@ -251,21 +251,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GreetingsPorts", "samples\W EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GreetingsEntities", "samples\WebAPI_Dapper\GreetingsEntities\GreetingsEntities.csproj", "{4164912F-F69E-4AD7-A521-6D58253B5ABC}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Paramore.Brighter.Sqlite.Dapper", "src\Paramore.Brighter.Sqlite.Dapper\Paramore.Brighter.Sqlite.Dapper.csproj", "{1DEBF15F-AA1B-4A9C-B1C3-7190E0988C86}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Paramore.Brighter.MySql.Dapper", "src\Paramore.Brighter.MySql.Dapper\Paramore.Brighter.MySql.Dapper.csproj", "{191A929A-0AE4-4E2A-9608-E47F93FA0004}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Paramore.Brighter.Dapper", "src\Paramore.Brighter.Dapper\Paramore.Brighter.Dapper.csproj", "{5FDA646C-30DA-4F13-8399-A3C533D2D16E}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Greetings_SqliteMigrations", "samples\WebAPI_Dapper\Greetings_SqliteMigrations\Greetings_SqliteMigrations.csproj", "{026230E1-F388-425A-98CB-6E17C174FE62}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Salutations_SqliteMigrations", "samples\WebAPI_Dapper\Salutations_SqliteMigrations\Salutations_SqliteMigrations.csproj", "{C601A031-963B-4EA9-82C7-1221B1EE9E51}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Greetings_MySqlMigrations", "samples\WebAPI_Dapper\Greetings_MySqlMigrations\Greetings_MySqlMigrations.csproj", "{ECD2C752-4E20-4EC5-BB6B-B06731BDE5BD}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Salutations_mySqlMigrations", "samples\WebAPI_Dapper\Salutations_mySqlMigrations\Salutations_mySqlMigrations.csproj", "{F72FF4C5-4CD8-4EFE-B468-5FAE45D20931}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Salutations_Migrations", "samples\WebAPI_Dapper\Salutations_Migrations\Salutations_Migrations.csproj", "{C601A031-963B-4EA9-82C7-1221B1EE9E51}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Paramore.Brighter.MsSql.Dapper", "src\Paramore.Brighter.MsSql.Dapper\Paramore.Brighter.MsSql.Dapper.csproj", "{1E4A5095-2D49-43EF-9628-BF7CE147CAE9}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Greetings_Migrations", "samples\WebAPI_Dapper\Greetings_Migrations\Greetings_Migrations.csproj", "{ECD2C752-4E20-4EC5-BB6B-B06731BDE5BD}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "WebAPI_Dynamo", "WebAPI_Dynamo", "{11935469-A062-4CFF-9F72-F4F41E14C2B4}" EndProject @@ -1506,30 +1496,6 @@ Global {4164912F-F69E-4AD7-A521-6D58253B5ABC}.Release|Mixed Platforms.Build.0 = Release|Any CPU {4164912F-F69E-4AD7-A521-6D58253B5ABC}.Release|x86.ActiveCfg = Release|Any CPU {4164912F-F69E-4AD7-A521-6D58253B5ABC}.Release|x86.Build.0 = Release|Any CPU - {1DEBF15F-AA1B-4A9C-B1C3-7190E0988C86}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1DEBF15F-AA1B-4A9C-B1C3-7190E0988C86}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1DEBF15F-AA1B-4A9C-B1C3-7190E0988C86}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {1DEBF15F-AA1B-4A9C-B1C3-7190E0988C86}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {1DEBF15F-AA1B-4A9C-B1C3-7190E0988C86}.Debug|x86.ActiveCfg = Debug|Any CPU - {1DEBF15F-AA1B-4A9C-B1C3-7190E0988C86}.Debug|x86.Build.0 = Debug|Any CPU - {1DEBF15F-AA1B-4A9C-B1C3-7190E0988C86}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1DEBF15F-AA1B-4A9C-B1C3-7190E0988C86}.Release|Any CPU.Build.0 = Release|Any CPU - {1DEBF15F-AA1B-4A9C-B1C3-7190E0988C86}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {1DEBF15F-AA1B-4A9C-B1C3-7190E0988C86}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {1DEBF15F-AA1B-4A9C-B1C3-7190E0988C86}.Release|x86.ActiveCfg = Release|Any CPU - {1DEBF15F-AA1B-4A9C-B1C3-7190E0988C86}.Release|x86.Build.0 = Release|Any CPU - {191A929A-0AE4-4E2A-9608-E47F93FA0004}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {191A929A-0AE4-4E2A-9608-E47F93FA0004}.Debug|Any CPU.Build.0 = Debug|Any CPU - {191A929A-0AE4-4E2A-9608-E47F93FA0004}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {191A929A-0AE4-4E2A-9608-E47F93FA0004}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {191A929A-0AE4-4E2A-9608-E47F93FA0004}.Debug|x86.ActiveCfg = Debug|Any CPU - {191A929A-0AE4-4E2A-9608-E47F93FA0004}.Debug|x86.Build.0 = Debug|Any CPU - {191A929A-0AE4-4E2A-9608-E47F93FA0004}.Release|Any CPU.ActiveCfg = Release|Any CPU - {191A929A-0AE4-4E2A-9608-E47F93FA0004}.Release|Any CPU.Build.0 = Release|Any CPU - {191A929A-0AE4-4E2A-9608-E47F93FA0004}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {191A929A-0AE4-4E2A-9608-E47F93FA0004}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {191A929A-0AE4-4E2A-9608-E47F93FA0004}.Release|x86.ActiveCfg = Release|Any CPU - {191A929A-0AE4-4E2A-9608-E47F93FA0004}.Release|x86.Build.0 = Release|Any CPU {5FDA646C-30DA-4F13-8399-A3C533D2D16E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5FDA646C-30DA-4F13-8399-A3C533D2D16E}.Debug|Any CPU.Build.0 = Debug|Any CPU {5FDA646C-30DA-4F13-8399-A3C533D2D16E}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU @@ -1542,18 +1508,6 @@ Global {5FDA646C-30DA-4F13-8399-A3C533D2D16E}.Release|Mixed Platforms.Build.0 = Release|Any CPU {5FDA646C-30DA-4F13-8399-A3C533D2D16E}.Release|x86.ActiveCfg = Release|Any CPU {5FDA646C-30DA-4F13-8399-A3C533D2D16E}.Release|x86.Build.0 = Release|Any CPU - {026230E1-F388-425A-98CB-6E17C174FE62}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {026230E1-F388-425A-98CB-6E17C174FE62}.Debug|Any CPU.Build.0 = Debug|Any CPU - {026230E1-F388-425A-98CB-6E17C174FE62}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {026230E1-F388-425A-98CB-6E17C174FE62}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {026230E1-F388-425A-98CB-6E17C174FE62}.Debug|x86.ActiveCfg = Debug|Any CPU - {026230E1-F388-425A-98CB-6E17C174FE62}.Debug|x86.Build.0 = Debug|Any CPU - {026230E1-F388-425A-98CB-6E17C174FE62}.Release|Any CPU.ActiveCfg = Release|Any CPU - {026230E1-F388-425A-98CB-6E17C174FE62}.Release|Any CPU.Build.0 = Release|Any CPU - {026230E1-F388-425A-98CB-6E17C174FE62}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {026230E1-F388-425A-98CB-6E17C174FE62}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {026230E1-F388-425A-98CB-6E17C174FE62}.Release|x86.ActiveCfg = Release|Any CPU - {026230E1-F388-425A-98CB-6E17C174FE62}.Release|x86.Build.0 = Release|Any CPU {C601A031-963B-4EA9-82C7-1221B1EE9E51}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C601A031-963B-4EA9-82C7-1221B1EE9E51}.Debug|Any CPU.Build.0 = Debug|Any CPU {C601A031-963B-4EA9-82C7-1221B1EE9E51}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU @@ -1578,30 +1532,6 @@ Global {ECD2C752-4E20-4EC5-BB6B-B06731BDE5BD}.Release|Mixed Platforms.Build.0 = Release|Any CPU {ECD2C752-4E20-4EC5-BB6B-B06731BDE5BD}.Release|x86.ActiveCfg = Release|Any CPU {ECD2C752-4E20-4EC5-BB6B-B06731BDE5BD}.Release|x86.Build.0 = Release|Any CPU - {F72FF4C5-4CD8-4EFE-B468-5FAE45D20931}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F72FF4C5-4CD8-4EFE-B468-5FAE45D20931}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F72FF4C5-4CD8-4EFE-B468-5FAE45D20931}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {F72FF4C5-4CD8-4EFE-B468-5FAE45D20931}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {F72FF4C5-4CD8-4EFE-B468-5FAE45D20931}.Debug|x86.ActiveCfg = Debug|Any CPU - {F72FF4C5-4CD8-4EFE-B468-5FAE45D20931}.Debug|x86.Build.0 = Debug|Any CPU - {F72FF4C5-4CD8-4EFE-B468-5FAE45D20931}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F72FF4C5-4CD8-4EFE-B468-5FAE45D20931}.Release|Any CPU.Build.0 = Release|Any CPU - {F72FF4C5-4CD8-4EFE-B468-5FAE45D20931}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {F72FF4C5-4CD8-4EFE-B468-5FAE45D20931}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {F72FF4C5-4CD8-4EFE-B468-5FAE45D20931}.Release|x86.ActiveCfg = Release|Any CPU - {F72FF4C5-4CD8-4EFE-B468-5FAE45D20931}.Release|x86.Build.0 = Release|Any CPU - {1E4A5095-2D49-43EF-9628-BF7CE147CAE9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1E4A5095-2D49-43EF-9628-BF7CE147CAE9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1E4A5095-2D49-43EF-9628-BF7CE147CAE9}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {1E4A5095-2D49-43EF-9628-BF7CE147CAE9}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {1E4A5095-2D49-43EF-9628-BF7CE147CAE9}.Debug|x86.ActiveCfg = Debug|Any CPU - {1E4A5095-2D49-43EF-9628-BF7CE147CAE9}.Debug|x86.Build.0 = Debug|Any CPU - {1E4A5095-2D49-43EF-9628-BF7CE147CAE9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1E4A5095-2D49-43EF-9628-BF7CE147CAE9}.Release|Any CPU.Build.0 = Release|Any CPU - {1E4A5095-2D49-43EF-9628-BF7CE147CAE9}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {1E4A5095-2D49-43EF-9628-BF7CE147CAE9}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {1E4A5095-2D49-43EF-9628-BF7CE147CAE9}.Release|x86.ActiveCfg = Release|Any CPU - {1E4A5095-2D49-43EF-9628-BF7CE147CAE9}.Release|x86.Build.0 = Release|Any CPU {8C5F9810-2158-4479-9C0B-E139F2BC8125}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8C5F9810-2158-4479-9C0B-E139F2BC8125}.Debug|Any CPU.Build.0 = Debug|Any CPU {8C5F9810-2158-4479-9C0B-E139F2BC8125}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU @@ -2065,10 +1995,8 @@ Global {526E4E1A-9E8E-4233-B851-D6172D013AF6} = {202BA107-89D5-4868-AC5A-3527114C0109} {79A46154-4754-48B1-849F-374AD729C040} = {202BA107-89D5-4868-AC5A-3527114C0109} {4164912F-F69E-4AD7-A521-6D58253B5ABC} = {202BA107-89D5-4868-AC5A-3527114C0109} - {026230E1-F388-425A-98CB-6E17C174FE62} = {202BA107-89D5-4868-AC5A-3527114C0109} {C601A031-963B-4EA9-82C7-1221B1EE9E51} = {202BA107-89D5-4868-AC5A-3527114C0109} {ECD2C752-4E20-4EC5-BB6B-B06731BDE5BD} = {202BA107-89D5-4868-AC5A-3527114C0109} - {F72FF4C5-4CD8-4EFE-B468-5FAE45D20931} = {202BA107-89D5-4868-AC5A-3527114C0109} {11935469-A062-4CFF-9F72-F4F41E14C2B4} = {235DE1F1-E71B-4817-8E27-3B34FF006E4C} {8C5F9810-2158-4479-9C0B-E139F2BC8125} = {11935469-A062-4CFF-9F72-F4F41E14C2B4} {C0E3FF76-597A-4449-9DBF-18BC76586685} = {11935469-A062-4CFF-9F72-F4F41E14C2B4} diff --git a/Docker/dynamodb/shared-local-instance.db b/Docker/dynamodb/shared-local-instance.db new file mode 100644 index 0000000000000000000000000000000000000000..43008023c6c8cdebec00a1056063ac31524a8916 GIT binary patch literal 552960 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≈M zdj;^&bqBg1=BshWJBMY!5&udPl41#X4sk~ME5M-EW!0&PR@LlK(L!8_7I=};A;z)^ zQu6~MF7^{ycI342@(y%EW=Q95ED+TlItnQU!yX$`v;YE0gfL_2>#8mo*t)D*nrKzi zH=rk`3LAqGS4>ru4=Kpf8fy4Tyn!uDTLF|4d>B(8x;8Y?8^^YYb|WWxB^FRZWhM|H zr0=T-wl1pPvqJ?1u5GjX1Dn&D| zfVu*bmjMN#I0E-<+WLRu{NFaSmy~-()?RI{=Z7s{TP%>$0t?&jl)a!|iPUak^@g=p zjddz#*2GE6X7rg+L;p@yycvSMk^dFTvA(*}SJ!*nwmE~A2)_Vb%la<`1u6@T6`en_ z;MP?&&+#%&Q0`dzc;eK9a7$KIv4Drrlaf3n^UsExkKj02+Yn+nEh+)_@aiC_Nnl

TdYbJBSjPASFFJy`BR2%-c69OSikJjN&lu45zmzy|kW{o>-R^wMeV}Vhu*nKs#uV(co9&Gwm7#P;%umcHm{8cp4mSx@3 z3>#pTr65_30)fzf6_Mmos?q&FPL()IGQ)^Hl+<>Y*|TBrE_RmW!cbSvgkG~EFU<#aJF1qB>u0A zo?SR(S}p%qipJ`*=?a}Ss(4&=;gF$b;gEr%WnyG~XK966S~aGkXwDeANGHvr#er1$ z=-N?Zl__P#r84RBs>1Ry22Xd4hsLgRzF1F%8>oJ5{vU-DJ%OCj?rTZCO=+mPjWe^m0IGp5a~ z_Ja7pgVrvMOs1z3e1fD#d~`!mAoj|5WA7 zvf1=rRa8tW8!-~>?2H)&!-vf*8$4)a!SI@yFgH!|hfn7B{+xorg|iB#)J_>&-(xjf zEHb5Y233`gm1h-IjjN?UM6|otj4Ccr3a2PlrRC$4Q6;5HVO8mz!jgg(`a_ytY$8vK zgvG{G>=qWA`j*_|%eQY$i_J|xElON$$mmis7RpA@P>ThDc1ay^C-nbRsvfXvMOFYv zTEZh+bpsX~DlppG9`r4XO~jxj;$nkow=6cl^Tj47W02HC`nXN!YdSCLtaR>>_eP$V zXXkAtecY)cWBWG$?)Yf#Q@QhU$L99Rc`+wD^YrZ1*?qIO?r>&@L0J!H+?zQdvuox% zZT{Rgx9y2p!#D0v8?WI{RsnyyxBsT?32k#T-)a9uo6hYoZ9k{|=*<4@_i1xVyIE~>WqQy7PLFGT^}jq1F07Y5@3gv2Mxi_uZOgALv#jDze}RaijmWojf||GZHhCa zpmwF7;#UT+*wPY0J2)~Mh)XeM0P{W?oq=xqoIfm8`Z0r#KgIwH?z6tUPi~v$x<(5&`9r7Q8DAWe~NPCJmKcmk%*(@sf36)Nnd2f2Zhpy9F{0*Q-g(m zg@?zC)U_dpdLE{?1=kQv{NxlV1<<>w$c`sHn0cqv(?q9) zS|H55Xme90qw?a2!+H>UA&hi^qk5JvhD4eWB43w|CR-fn9pX(_deQeS3`{vp!R9ysw<+pm4xb#NN28M6l-`fuChf@x#Bh;AXiPOp@IjJIqnLz7 znQKt_QKhrn+$PNz+Orrpq!pH8Cg)5Dj^GI(`KC~eGeXcvhI9f~j*CJsD81S?muq`k z7Wrm{XR=XTq&kXKQPvJ|SPjHPNa|Vf4-q$NS*8QGD?RKXJ?uIRLNtT=B)x4HjRZ1& zw&F;iv^5w~(^OXgzY!)Caeqh|+dzfLhx|yh5s6R?cXu&l6bMm@1sZo_QR8&6l2m^j4UbX0AF#awu^K-S5kr&`2an1 zG(QNWm2HL3BXJYc=&~WW&_!XM&NfQOI-*(zyn^T#qCK=j)=`;flKZC!N@9V`HU3NY z7ITw4GwOzjM2CRTjX=7OE6eFILVY{t;&jYUKF-I7T@WfDghK?2>37z}T}EkH$!N=n zI{=wOXdV5JAbyizocU~4odE7DnbiipHD!``8j1)QLr3v|Nr&H2Ya1#$MV9^++2{

qExJN5gSdC=BIS$?P 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 diff --git a/samples/ASBTaskQueue/GreetingsScopedReceiverConsole/Program.cs b/samples/ASBTaskQueue/GreetingsScopedReceiverConsole/Program.cs index 6472fff9df..33ca33e3dc 100644 --- a/samples/ASBTaskQueue/GreetingsScopedReceiverConsole/Program.cs +++ b/samples/ASBTaskQueue/GreetingsScopedReceiverConsole/Program.cs @@ -62,7 +62,6 @@ public static async Task Main(string[] args) options.ChannelFactory = new AzureServiceBusChannelFactory(asbConsumerFactory); options.UseScoped = true; }) - .UseInMemoryOutbox() .AutoFromAssemblies(); services.AddHostedService(); diff --git a/samples/ASBTaskQueue/GreetingsSender.Web/Program.cs b/samples/ASBTaskQueue/GreetingsSender.Web/Program.cs index 8363e6b58c..c7063d4dbe 100644 --- a/samples/ASBTaskQueue/GreetingsSender.Web/Program.cs +++ b/samples/ASBTaskQueue/GreetingsSender.Web/Program.cs @@ -1,4 +1,5 @@ -using Greetings.Adaptors.Data; +using System.Data.Common; +using Greetings.Adaptors.Data; using Greetings.Adaptors.Services; using Greetings.Ports.Commands; using Microsoft.AspNetCore.Builder; @@ -36,7 +37,18 @@ var asbConnection = new ServiceBusVisualStudioCredentialClientProvider(asbEndpoint); -var outboxConfig = new MsSqlConfiguration(dbConnString, "BrighterOutbox"); +var outboxConfig = new RelationalDatabaseConfiguration(dbConnString, outBoxTableName: "BrighterOutbox"); + +var producerRegistry = new AzureServiceBusProducerRegistryFactory( + asbConnection, + new AzureServiceBusPublication[] + { + new() { Topic = new RoutingKey("greeting.event") }, + new() { Topic = new RoutingKey("greeting.addGreetingCommand") }, + new() { Topic = new RoutingKey("greeting.Asyncevent") } + } + ) + .Create(); builder.Services .AddBrighter(opt => @@ -44,25 +56,17 @@ opt.PolicyRegistry = new DefaultPolicy(); opt.CommandProcessorLifetime = ServiceLifetime.Scoped; }) - .UseExternalBus( - new AzureServiceBusProducerRegistryFactory( - asbConnection, - new AzureServiceBusPublication[] - { - new() { Topic = new RoutingKey("greeting.event") }, - new() { Topic = new RoutingKey("greeting.addGreetingCommand") }, - new() { Topic = new RoutingKey("greeting.Asyncevent") } - } - ) - .Create() - ) - .UseMsSqlOutbox(outboxConfig, typeof(MsSqlSqlAuthConnectionProvider)) - .UseMsSqlTransactionConnectionProvider(typeof(MsSqlEntityFrameworkCoreConnectionProvider)) .MapperRegistry(r => { r.Add(typeof(GreetingEvent), typeof(GreetingEventMessageMapper)); r.Add(typeof(GreetingAsyncEvent), typeof(GreetingEventAsyncMessageMapper)); r.Add(typeof(AddGreetingCommand), typeof(AddGreetingMessageMapper)); + }) + .UseExternalBus((configure) => + { + configure.ProducerRegistry = producerRegistry; + configure.Outbox = new MsSqlOutbox(outboxConfig); + configure.TransactionProvider = typeof(MsSqlEntityFrameworkCoreConnectionProvider); }); diff --git a/samples/ASBTaskQueue/GreetingsSender/Program.cs b/samples/ASBTaskQueue/GreetingsSender/Program.cs index b6d8ff6b97..41d9427e4a 100644 --- a/samples/ASBTaskQueue/GreetingsSender/Program.cs +++ b/samples/ASBTaskQueue/GreetingsSender/Program.cs @@ -1,10 +1,13 @@ using System; +using System.Data.Common; +using System.Transactions; using Greetings.Ports.Events; using Microsoft.Extensions.DependencyInjection; using Paramore.Brighter; using Paramore.Brighter.Extensions.DependencyInjection; using Paramore.Brighter.MessagingGateway.AzureServiceBus; using Paramore.Brighter.MessagingGateway.AzureServiceBus.ClientProvider; +using Polly.Caching; namespace GreetingsSender { @@ -19,26 +22,30 @@ static void Main(string[] args) //TODO: add your ASB qualified name here var asbClientProvider = new ServiceBusVisualStudioCredentialClientProvider("fim-development-bus.servicebus.windows.net"); - serviceCollection.AddBrighter() - .UseInMemoryOutbox() - .UseExternalBus(new AzureServiceBusProducerRegistryFactory( - asbClientProvider, - new AzureServiceBusPublication[] + var producerRegistry = new AzureServiceBusProducerRegistryFactory( + asbClientProvider, + new AzureServiceBusPublication[] + { + new AzureServiceBusPublication + { + Topic = new RoutingKey("greeting.event") + }, + new AzureServiceBusPublication { - new AzureServiceBusPublication - { - Topic = new RoutingKey("greeting.event") - }, - new AzureServiceBusPublication - { - Topic = new RoutingKey("greeting.addGreetingCommand") - }, - new AzureServiceBusPublication - { - Topic = new RoutingKey("greeting.Asyncevent") - } + Topic = new RoutingKey("greeting.addGreetingCommand") + }, + new AzureServiceBusPublication + { + Topic = new RoutingKey("greeting.Asyncevent") } - ).Create()) + } + ).Create(); + + serviceCollection.AddBrighter() + .UseExternalBus((config) => + { + config.ProducerRegistry = producerRegistry; + }) .AutoFromAssemblies(); var serviceProvider = serviceCollection.BuildServiceProvider(); diff --git a/samples/ASBTaskQueue/GreetingsWorker/Program.cs b/samples/ASBTaskQueue/GreetingsWorker/Program.cs index d2b8ab4902..ae1741b586 100644 --- a/samples/ASBTaskQueue/GreetingsWorker/Program.cs +++ b/samples/ASBTaskQueue/GreetingsWorker/Program.cs @@ -71,7 +71,7 @@ o.UseSqlServer(dbConnString); }); -var outboxConfig = new MsSqlConfiguration(dbConnString, "BrighterOutbox"); +var outboxConfig = new RelationalDatabaseConfiguration(dbConnString, outBoxTableName: "BrighterOutbox"); //TODO: add your ASB qualified name here var clientProvider = new ServiceBusVisualStudioCredentialClientProvider(".servicebus.windows.net"); @@ -83,8 +83,7 @@ options.ChannelFactory = new AzureServiceBusChannelFactory(asbConsumerFactory); options.UseScoped = true; - }).UseMsSqlOutbox(outboxConfig, typeof(MsSqlSqlAuthConnectionProvider)) - .UseMsSqlTransactionConnectionProvider(typeof(MsSqlEntityFrameworkCoreConnectionProvider)) + }) .AutoFromAssemblies(); builder.Services.AddHostedService(); diff --git a/samples/AWSTaskQueue/GreetingsPumper/Program.cs b/samples/AWSTaskQueue/GreetingsPumper/Program.cs index bf634f2ed5..0478b71099 100644 --- a/samples/AWSTaskQueue/GreetingsPumper/Program.cs +++ b/samples/AWSTaskQueue/GreetingsPumper/Program.cs @@ -1,6 +1,7 @@ using System; using System.Threading; using System.Threading.Tasks; +using System.Transactions; using Amazon; using Amazon.Runtime.CredentialManagement; using Greetings.Ports.Commands; @@ -31,19 +32,22 @@ private static async Task Main(string[] args) { var awsConnection = new AWSMessagingGatewayConnection(credentials, RegionEndpoint.EUWest1); - services.AddBrighter() - .UseInMemoryOutbox() - .UseExternalBus(new SnsProducerRegistryFactory( - awsConnection, - new SnsPublication[] + var producerRegistry = new SnsProducerRegistryFactory( + awsConnection, + new SnsPublication[] + { + new SnsPublication { - new SnsPublication - { - Topic = new RoutingKey(typeof(GreetingEvent).FullName.ToValidSNSTopicName()) - } + Topic = new RoutingKey(typeof(GreetingEvent).FullName.ToValidSNSTopicName()) } - ).Create() - ) + } + ).Create(); + + services.AddBrighter() + .UseExternalBus((configure) => + { + configure.ProducerRegistry = producerRegistry; + }) .AutoFromAssemblies(typeof(GreetingEvent).Assembly); } diff --git a/samples/AWSTaskQueue/GreetingsReceiverConsole/Program.cs b/samples/AWSTaskQueue/GreetingsReceiverConsole/Program.cs index 3b8d1c5925..2e8d4cc17a 100644 --- a/samples/AWSTaskQueue/GreetingsReceiverConsole/Program.cs +++ b/samples/AWSTaskQueue/GreetingsReceiverConsole/Program.cs @@ -72,7 +72,6 @@ public static async Task Main(string[] args) options.Subscriptions = subscriptions; options.ChannelFactory = new ChannelFactory(awsConnection); }) - .UseInMemoryOutbox() .AutoFromAssemblies(); } diff --git a/samples/AWSTaskQueue/GreetingsSender/Program.cs b/samples/AWSTaskQueue/GreetingsSender/Program.cs index b6f1bac8e7..fcbf665c6d 100644 --- a/samples/AWSTaskQueue/GreetingsSender/Program.cs +++ b/samples/AWSTaskQueue/GreetingsSender/Program.cs @@ -22,6 +22,7 @@ THE SOFTWARE. */ #endregion +using System.Transactions; using Amazon; using Amazon.Runtime.CredentialManagement; using Greetings.Ports.Commands; @@ -52,19 +53,22 @@ static void Main(string[] args) { var awsConnection = new AWSMessagingGatewayConnection(credentials, RegionEndpoint.EUWest1); - serviceCollection.AddBrighter() - .UseInMemoryOutbox() - .UseExternalBus(new SnsProducerRegistryFactory( - awsConnection, - new SnsPublication[] + var producerRegistry = new SnsProducerRegistryFactory( + awsConnection, + new SnsPublication[] + { + new SnsPublication { - new SnsPublication - { - Topic = new RoutingKey(typeof(GreetingEvent).FullName.ToValidSNSTopicName()) - } + Topic = new RoutingKey(typeof(GreetingEvent).FullName.ToValidSNSTopicName()) } - ).Create() - ) + } + ).Create(); + + serviceCollection.AddBrighter() + .UseExternalBus((configure) => + { + configure.ProducerRegistry = producerRegistry; + }) .AutoFromAssemblies(typeof(GreetingEvent).Assembly); var serviceProvider = serviceCollection.BuildServiceProvider(); diff --git a/samples/AWSTransfomers/ClaimCheck/GreetingsReceiverConsole/Program.cs b/samples/AWSTransfomers/ClaimCheck/GreetingsReceiverConsole/Program.cs index 33ed7cfc83..b301913e38 100644 --- a/samples/AWSTransfomers/ClaimCheck/GreetingsReceiverConsole/Program.cs +++ b/samples/AWSTransfomers/ClaimCheck/GreetingsReceiverConsole/Program.cs @@ -76,7 +76,6 @@ public static async Task Main(string[] args) options.Subscriptions = subscriptions; options.ChannelFactory = new ChannelFactory(awsConnection); }) - .UseInMemoryOutbox() .AutoFromAssemblies(); //We need this for the check as to whether an S3 bucket exists diff --git a/samples/AWSTransfomers/ClaimCheck/GreetingsSender/Program.cs b/samples/AWSTransfomers/ClaimCheck/GreetingsSender/Program.cs index b3cdfb116d..67213ae9c3 100644 --- a/samples/AWSTransfomers/ClaimCheck/GreetingsSender/Program.cs +++ b/samples/AWSTransfomers/ClaimCheck/GreetingsSender/Program.cs @@ -24,6 +24,7 @@ THE SOFTWARE. */ using System; using System.Linq; +using System.Transactions; using Amazon; using Amazon.Runtime.CredentialManagement; using Amazon.S3; @@ -57,22 +58,25 @@ static void Main(string[] args) var awsConnection = new AWSMessagingGatewayConnection(credentials, RegionEndpoint.EUWest1); var topic = new RoutingKey(typeof(GreetingEvent).FullName.ToValidSNSTopicName()); - - serviceCollection.AddBrighter() - .UseInMemoryOutbox() - .UseExternalBus(new SnsProducerRegistryFactory( - awsConnection, - new SnsPublication[] + + var producerRegistry = new SnsProducerRegistryFactory( + awsConnection, + new SnsPublication[] + { + new SnsPublication { - new SnsPublication - { - Topic = topic, - FindTopicBy = TopicFindBy.Convention, - MakeChannels = OnMissingChannel.Create - } + Topic = topic, + FindTopicBy = TopicFindBy.Convention, + MakeChannels = OnMissingChannel.Create } - ).Create() - ) + } + ).Create(); + + serviceCollection.AddBrighter() + .UseExternalBus((configure) => + { + configure.ProducerRegistry = producerRegistry; + }) .AutoFromAssemblies(typeof(GreetingEvent).Assembly); //We need this for the check as to whether an S3 bucket exists diff --git a/samples/AWSTransfomers/Compression/GreetingsReceiverConsole/Program.cs b/samples/AWSTransfomers/Compression/GreetingsReceiverConsole/Program.cs index d429ddd3c8..4907a5e3aa 100644 --- a/samples/AWSTransfomers/Compression/GreetingsReceiverConsole/Program.cs +++ b/samples/AWSTransfomers/Compression/GreetingsReceiverConsole/Program.cs @@ -76,7 +76,6 @@ public static async Task Main(string[] args) options.Subscriptions = subscriptions; options.ChannelFactory = new ChannelFactory(awsConnection); }) - .UseInMemoryOutbox() .AutoFromAssemblies(); } diff --git a/samples/AWSTransfomers/Compression/GreetingsSender/Program.cs b/samples/AWSTransfomers/Compression/GreetingsSender/Program.cs index 62b164eef7..6e79ab36e8 100644 --- a/samples/AWSTransfomers/Compression/GreetingsSender/Program.cs +++ b/samples/AWSTransfomers/Compression/GreetingsSender/Program.cs @@ -24,6 +24,7 @@ THE SOFTWARE. */ using System; using System.Linq; +using System.Transactions; using Amazon; using Amazon.Runtime.CredentialManagement; using Amazon.S3; @@ -57,22 +58,25 @@ static void Main(string[] args) var awsConnection = new AWSMessagingGatewayConnection(credentials, RegionEndpoint.EUWest1); var topic = new RoutingKey(typeof(GreetingEvent).FullName.ToValidSNSTopicName()); - - serviceCollection.AddBrighter() - .UseInMemoryOutbox() - .UseExternalBus(new SnsProducerRegistryFactory( - awsConnection, - new SnsPublication[] + + var producerRegistry = new SnsProducerRegistryFactory( + awsConnection, + new SnsPublication[] + { + new SnsPublication { - new SnsPublication - { - Topic = topic, - FindTopicBy = TopicFindBy.Convention, - MakeChannels = OnMissingChannel.Create - } + Topic = topic, + FindTopicBy = TopicFindBy.Convention, + MakeChannels = OnMissingChannel.Create } - ).Create() - ) + } + ).Create(); + + serviceCollection.AddBrighter() + .UseExternalBus((configure) => + { + configure.ProducerRegistry = producerRegistry; + }) .AutoFromAssemblies(typeof(GreetingEvent).Assembly); var serviceProvider = serviceCollection.BuildServiceProvider(); diff --git a/samples/HelloAsyncListeners/Program.cs b/samples/HelloAsyncListeners/Program.cs index 65228c51f6..29e70c5889 100644 --- a/samples/HelloAsyncListeners/Program.cs +++ b/samples/HelloAsyncListeners/Program.cs @@ -1,4 +1,5 @@ using System.Threading.Tasks; +using System.Transactions; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Paramore.Brighter.Extensions.DependencyInjection; diff --git a/samples/HelloWorld/Program.cs b/samples/HelloWorld/Program.cs index 36de3a3172..3fadbd17c7 100644 --- a/samples/HelloWorld/Program.cs +++ b/samples/HelloWorld/Program.cs @@ -24,6 +24,7 @@ THE SOFTWARE. */ #endregion using System; +using System.Transactions; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Paramore.Brighter; diff --git a/samples/HelloWorldAsync/Program.cs b/samples/HelloWorldAsync/Program.cs index 8ac8de44ce..42ecc8fce9 100644 --- a/samples/HelloWorldAsync/Program.cs +++ b/samples/HelloWorldAsync/Program.cs @@ -25,6 +25,7 @@ THE SOFTWARE. */ using System.Threading; using System.Threading.Tasks; +using System.Transactions; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Paramore.Brighter; diff --git a/samples/KafkaSchemaRegistry/GreetingsSender/GreetingsSender.csproj b/samples/KafkaSchemaRegistry/GreetingsSender/GreetingsSender.csproj index 674656d90a..779c7c868c 100644 --- a/samples/KafkaSchemaRegistry/GreetingsSender/GreetingsSender.csproj +++ b/samples/KafkaSchemaRegistry/GreetingsSender/GreetingsSender.csproj @@ -13,7 +13,6 @@ - diff --git a/samples/KafkaSchemaRegistry/GreetingsSender/Program.cs b/samples/KafkaSchemaRegistry/GreetingsSender/Program.cs index 4a1d522de5..027005bbe3 100644 --- a/samples/KafkaSchemaRegistry/GreetingsSender/Program.cs +++ b/samples/KafkaSchemaRegistry/GreetingsSender/Program.cs @@ -27,6 +27,7 @@ THE SOFTWARE. */ using System; using System.IO; using System.Threading.Tasks; +using System.Transactions; using Confluent.SchemaRegistry; using Greetings.Ports.Commands; using Microsoft.Extensions.Configuration; @@ -91,29 +92,32 @@ static async Task Main(string[] args) var cachedSchemaRegistryClient = new CachedSchemaRegistryClient(schemaRegistryConfig); services.AddSingleton(cachedSchemaRegistryClient); + var producerRegistry = new KafkaProducerRegistryFactory( + new KafkaMessagingGatewayConfiguration + { + Name = "paramore.brighter.greetingsender", + BootStrapServers = new[] {"localhost:9092"} + }, + new KafkaPublication[] + { + new KafkaPublication + { + Topic = new RoutingKey("greeting.event"), + MessageSendMaxRetries = 3, + MessageTimeoutMs = 1000, + MaxInFlightRequestsPerConnection = 1 + } + }) + .Create(); + services.AddBrighter(options => { options.PolicyRegistry = policyRegistry; }) - .UseInMemoryOutbox() - .UseExternalBus( - new KafkaProducerRegistryFactory( - new KafkaMessagingGatewayConfiguration - { - Name = "paramore.brighter.greetingsender", - BootStrapServers = new[] {"localhost:9092"} - }, - new KafkaPublication[] - { - new KafkaPublication - { - Topic = new RoutingKey("greeting.event"), - MessageSendMaxRetries = 3, - MessageTimeoutMs = 1000, - MaxInFlightRequestsPerConnection = 1 - } - }) - .Create()) + .UseExternalBus((configure) => + { + configure.ProducerRegistry = producerRegistry; + }) .MapperRegistryFromAssemblies(typeof(GreetingEvent).Assembly); services.AddHostedService(); diff --git a/samples/KafkaTaskQueue/GreetingsSender/Program.cs b/samples/KafkaTaskQueue/GreetingsSender/Program.cs index b3f71c127e..0d0f5493ee 100644 --- a/samples/KafkaTaskQueue/GreetingsSender/Program.cs +++ b/samples/KafkaTaskQueue/GreetingsSender/Program.cs @@ -25,6 +25,7 @@ THE SOFTWARE. */ #endregion using System; +using System.Data.Common; using System.IO; using System.Threading.Tasks; using Greetings.Ports.Commands; @@ -85,30 +86,33 @@ static async Task Main(string[] args) {CommandProcessor.CIRCUITBREAKERASYNC, circuitBreakerPolicyAsync} }; + var producerRegistry = new KafkaProducerRegistryFactory( + new KafkaMessagingGatewayConfiguration + { + Name = "paramore.brighter.greetingsender", + BootStrapServers = new[] {"localhost:9092"} + }, + new KafkaPublication[] + { + new KafkaPublication + { + Topic = new RoutingKey("greeting.event"), + NumPartitions = 3, + MessageSendMaxRetries = 3, + MessageTimeoutMs = 1000, + MaxInFlightRequestsPerConnection = 1 + } + }) + .Create(); + services.AddBrighter(options => { options.PolicyRegistry = policyRegistry; }) - .UseInMemoryOutbox() - .UseExternalBus( - new KafkaProducerRegistryFactory( - new KafkaMessagingGatewayConfiguration - { - Name = "paramore.brighter.greetingsender", - BootStrapServers = new[] {"localhost:9092"} - }, - new KafkaPublication[] - { - new KafkaPublication - { - Topic = new RoutingKey("greeting.event"), - NumPartitions = 3, - MessageSendMaxRetries = 3, - MessageTimeoutMs = 1000, - MaxInFlightRequestsPerConnection = 1 - } - }) - .Create()) + .UseExternalBus((configure) => + { + configure.ProducerRegistry = producerRegistry; + }) .MapperRegistryFromAssemblies(typeof(GreetingEvent).Assembly); services.AddHostedService(); diff --git a/samples/MsSqlMessagingGateway/CompetingReceiverConsole/Program.cs b/samples/MsSqlMessagingGateway/CompetingReceiverConsole/Program.cs index e923358e52..5825614e38 100644 --- a/samples/MsSqlMessagingGateway/CompetingReceiverConsole/Program.cs +++ b/samples/MsSqlMessagingGateway/CompetingReceiverConsole/Program.cs @@ -36,7 +36,7 @@ private static async Task Main() timeoutInMilliseconds: 200) }; - var messagingConfiguration = new MsSqlConfiguration(@"Database=BrighterSqlQueue;Server=.\sqlexpress;Integrated Security=SSPI;", queueStoreTable: "QueueData"); + var messagingConfiguration = new RelationalDatabaseConfiguration(@"Database=BrighterSqlQueue;Server=.\sqlexpress;Integrated Security=SSPI;", queueStoreTable: "QueueData"); var messageConsumerFactory = new MsSqlMessageConsumerFactory(messagingConfiguration); services.AddServiceActivator(options => @@ -44,10 +44,8 @@ private static async Task Main() options.Subscriptions = subscriptions; options.ChannelFactory = new ChannelFactory(messageConsumerFactory); }) - .UseInMemoryOutbox() .AutoFromAssemblies(); - services.AddHostedService(); services.AddHostedService(); diff --git a/samples/MsSqlMessagingGateway/CompetingSender/Program.cs b/samples/MsSqlMessagingGateway/CompetingSender/Program.cs index 7eb99fbea2..4707c8aab7 100644 --- a/samples/MsSqlMessagingGateway/CompetingSender/Program.cs +++ b/samples/MsSqlMessagingGateway/CompetingSender/Program.cs @@ -40,14 +40,18 @@ private static async Task Main(string[] args) .ConfigureServices((hostContext, services) => { //create the gateway - var messagingConfiguration = new MsSqlConfiguration(@"Database=BrighterSqlQueue;Server=.\sqlexpress;Integrated Security=SSPI;", queueStoreTable: "QueueData"); + var messagingConfiguration = new RelationalDatabaseConfiguration(@"Database=BrighterSqlQueue;Server=.\sqlexpress;Integrated Security=SSPI;", queueStoreTable: "QueueData"); - services.AddBrighter() - .UseInMemoryOutbox() - .UseExternalBus(new MsSqlProducerRegistryFactory( + var producerRegistry = new MsSqlProducerRegistryFactory( messagingConfiguration, new Publication[]{new Publication()}) - .Create()) + .Create(); + + services.AddBrighter() + .UseExternalBus((configure) => + { + configure.ProducerRegistry = producerRegistry; + }) .AutoFromAssemblies(); services.AddHostedService(provider => new RunCommandProcessor(provider.GetService(), repeatCount)); diff --git a/samples/MsSqlMessagingGateway/GreetingsReceiverConsole/Program.cs b/samples/MsSqlMessagingGateway/GreetingsReceiverConsole/Program.cs index 344616544c..ec814790da 100644 --- a/samples/MsSqlMessagingGateway/GreetingsReceiverConsole/Program.cs +++ b/samples/MsSqlMessagingGateway/GreetingsReceiverConsole/Program.cs @@ -62,7 +62,7 @@ public static async Task Main(string[] args) //create the gateway var messagingConfiguration = - new MsSqlConfiguration( + new RelationalDatabaseConfiguration( @"Database=BrighterSqlQueue;Server=.\sqlexpress;Integrated Security=SSPI;", queueStoreTable: "QueueData"); var messageConsumerFactory = new MsSqlMessageConsumerFactory(messagingConfiguration); services.AddServiceActivator(options => @@ -70,7 +70,6 @@ public static async Task Main(string[] args) options.Subscriptions = subscriptions; options.ChannelFactory = new ChannelFactory(messageConsumerFactory); }) - .UseInMemoryOutbox() .AutoFromAssemblies(); diff --git a/samples/MsSqlMessagingGateway/GreetingsSender/Program.cs b/samples/MsSqlMessagingGateway/GreetingsSender/Program.cs index 834906986e..b561543d80 100644 --- a/samples/MsSqlMessagingGateway/GreetingsSender/Program.cs +++ b/samples/MsSqlMessagingGateway/GreetingsSender/Program.cs @@ -1,4 +1,5 @@ -using Events.Ports.Commands; +using System.Transactions; +using Events.Ports.Commands; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Paramore.Brighter; @@ -23,15 +24,19 @@ static void Main() var serviceCollection = new ServiceCollection(); serviceCollection.AddSingleton(new SerilogLoggerFactory()); - var messagingConfiguration = new MsSqlConfiguration(@"Database=BrighterSqlQueue;Server=.\sqlexpress;Integrated Security=SSPI;", queueStoreTable: "QueueData"); + var messagingConfiguration = new RelationalDatabaseConfiguration(@"Database=BrighterSqlQueue;Server=.\sqlexpress;Integrated Security=SSPI;", queueStoreTable: "QueueData"); - serviceCollection.AddBrighter() - .UseInMemoryOutbox() - .UseExternalBus(new MsSqlProducerRegistryFactory( + var producerRegistry = new MsSqlProducerRegistryFactory( messagingConfiguration, new Publication[] {new Publication()} - ) - .Create()) + ) + .Create(); + + serviceCollection.AddBrighter() + .UseExternalBus((configure) => + { + configure.ProducerRegistry = producerRegistry; + }) .AutoFromAssemblies(); var serviceProvider = serviceCollection.BuildServiceProvider(); diff --git a/samples/OpenTelemetry/Consumer/Program.cs b/samples/OpenTelemetry/Consumer/Program.cs index f8c6915e4a..00b7733086 100644 --- a/samples/OpenTelemetry/Consumer/Program.cs +++ b/samples/OpenTelemetry/Consumer/Program.cs @@ -41,6 +41,8 @@ var rmqMessageConsumerFactory = new RmqMessageConsumerFactory(rmqConnection); +var producerRegistry = Helpers.GetProducerRegistry(rmqConnection); + builder.Services.AddServiceActivator(options => { options.Subscriptions = new Subscription[] @@ -64,8 +66,6 @@ }; options.ChannelFactory = new ChannelFactory(rmqMessageConsumerFactory); }) - .UseExternalBus(Helpers.GetProducerRegistry(rmqConnection)) - .UseInMemoryOutbox() .MapperRegistry(r => { r.Register>(); @@ -78,6 +78,10 @@ r.Register(); r.Register(); }) + .UseExternalBus((configure) => + { + configure.ProducerRegistry = producerRegistry; + }) .UseOutboxSweeper(options => { options.TimerInterval = 30; diff --git a/samples/OpenTelemetry/Producer/Program.cs b/samples/OpenTelemetry/Producer/Program.cs index 248fbbaf97..34cfe93c16 100644 --- a/samples/OpenTelemetry/Producer/Program.cs +++ b/samples/OpenTelemetry/Producer/Program.cs @@ -1,3 +1,4 @@ +using System.Transactions; using OpenTelemetry; using OpenTelemetry.Resources; using OpenTelemetry.Shared.Commands; @@ -27,17 +28,21 @@ Exchange = new Exchange("paramore.brighter.exchange"), }; +var producerRegistry = Helpers.GetProducerRegistry(rmqConnection); + builder.Services.AddBrighter(options => { options.CommandProcessorLifetime = ServiceLifetime.Scoped; }) - .UseExternalBus(Helpers.GetProducerRegistry(rmqConnection)) - .UseInMemoryOutbox() .MapperRegistry(r => { r.Register>(); r.Register>(); r.Register>(); + }) + .UseExternalBus((configure) => + { + configure.ProducerRegistry = producerRegistry; }); builder.Services.AddSingleton(); diff --git a/samples/OpenTelemetry/Sweeper/Program.cs b/samples/OpenTelemetry/Sweeper/Program.cs index 60dcc1bfb6..110c88bdad 100644 --- a/samples/OpenTelemetry/Sweeper/Program.cs +++ b/samples/OpenTelemetry/Sweeper/Program.cs @@ -1,5 +1,6 @@ // See https://aka.ms/new-console-template for more information +using System.Transactions; using OpenTelemetry; using OpenTelemetry.Resources; using OpenTelemetry.Trace; @@ -23,13 +24,6 @@ using var tracerProvider = Sdk.CreateTracerProviderBuilder() .SetResourceBuilder(ResourceBuilder.CreateDefault().AddService("Brighter Sweeper Sample")) .AddSource("Paramore.Brighter") - // .AddZipkinExporter(o => o.HttpClientFactory = () => - // { - // HttpClient client = new HttpClient(); - // client.DefaultRequestHeaders.Add("X-MyCustomHeader", "value"); - // return client; - // o.Endpoint = new Uri("http://localhost:9411/api/v2/spans"); - // }) .AddJaegerExporter() .Build(); @@ -39,8 +33,10 @@ }); builder.Services.AddBrighter() - .UseExternalBus(producerRegistry) - .UseInMemoryOutbox() + .UseExternalBus((configure) => + { + configure.ProducerRegistry = producerRegistry; + }) .UseOutboxSweeper(options => { options.TimerInterval = 5; @@ -49,7 +45,7 @@ var app = builder.Build(); -var outBox = app.Services.GetService>(); +var outBox = app.Services.GetService>(); outBox.Add(new Message(new MessageHeader(Guid.NewGuid(), "Test.Topic", MessageType.MT_COMMAND, DateTime.UtcNow), new MessageBody("Hello"))); diff --git a/samples/RMQRequestReply/GreetingsClient/Program.cs b/samples/RMQRequestReply/GreetingsClient/Program.cs index 8770bddfcc..d58ac3765d 100644 --- a/samples/RMQRequestReply/GreetingsClient/Program.cs +++ b/samples/RMQRequestReply/GreetingsClient/Program.cs @@ -23,6 +23,7 @@ THE SOFTWARE. */ #endregion using System; +using System.Transactions; using Greetings.Ports.Commands; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -61,24 +62,25 @@ static void Main(string[] args) new RmqSubscription(typeof(GreetingReply)) }; + var producerRegistry = new RmqProducerRegistryFactory( + rmqConnection, + new RmqPublication[] + { + new RmqPublication + { + Topic = new RoutingKey("Greeting.Request") + } + }).Create(); + serviceCollection - .AddBrighter(options => + .AddBrighter() + .UseExternalBus((configure) => { - options.ChannelFactory = new ChannelFactory(rmqMessageConsumerFactory); + configure.ProducerRegistry = producerRegistry; + configure.UseRpc = true; + configure.ReplyQueueSubscriptions = replySubscriptions; + configure.ResponseChannelFactory = new ChannelFactory(rmqMessageConsumerFactory); }) - .UseInMemoryOutbox() - .UseExternalBus( - new RmqProducerRegistryFactory( - rmqConnection, - new RmqPublication[] - { - new RmqPublication - { - Topic = new RoutingKey("Greeting.Request") - } - }).Create(), - true, - replySubscriptions) .AutoFromAssemblies(); var serviceProvider = serviceCollection.BuildServiceProvider(); diff --git a/samples/RMQRequestReply/GreetingsServer/Program.cs b/samples/RMQRequestReply/GreetingsServer/Program.cs index 16bc4760ea..d7afebae1b 100644 --- a/samples/RMQRequestReply/GreetingsServer/Program.cs +++ b/samples/RMQRequestReply/GreetingsServer/Program.cs @@ -70,25 +70,27 @@ public static async Task Main(string[] args) ChannelFactory amAChannelFactory = new ChannelFactory(rmqMessageConsumerFactory); var producer = new RmqMessageProducer(rmqConnection); + var producerRegistry = new RmqProducerRegistryFactory( + rmqConnection, + new RmqPublication[] + { + new() + { + //TODO: We don't know the reply routing key, but need a topic name, we could make this simpler + Topic = new RoutingKey("Reply"), + MakeChannels = OnMissingChannel.Assume + } + }).Create(); + services.AddServiceActivator(options => { options.Subscriptions = subscriptions; options.ChannelFactory = amAChannelFactory; }) - .UseInMemoryOutbox() - .UseExternalBus( - new RmqProducerRegistryFactory( - rmqConnection, - new RmqPublication[] - { - new() - { - //TODO: We don't know the reply routing key, but need a topic name, we could make this simpler - Topic = new RoutingKey("Reply"), - MakeChannels = OnMissingChannel.Assume - } - }).Create(), - true) + .UseExternalBus((configure) => + { + configure.ProducerRegistry = producerRegistry; + }) .AutoFromAssemblies(); diff --git a/samples/RMQTaskQueue/GreetingsReceiverConsole/Program.cs b/samples/RMQTaskQueue/GreetingsReceiverConsole/Program.cs index 85959e5b57..135c7faf38 100644 --- a/samples/RMQTaskQueue/GreetingsReceiverConsole/Program.cs +++ b/samples/RMQTaskQueue/GreetingsReceiverConsole/Program.cs @@ -83,7 +83,6 @@ public static async Task Main(string[] args) options.Subscriptions = subscriptions; options.ChannelFactory = new ChannelFactory(rmqMessageConsumerFactory); }) - .UseInMemoryOutbox() .AutoFromAssemblies(); diff --git a/samples/RMQTaskQueue/GreetingsSender/Program.cs b/samples/RMQTaskQueue/GreetingsSender/Program.cs index 12bb05570f..59fb8d000e 100644 --- a/samples/RMQTaskQueue/GreetingsSender/Program.cs +++ b/samples/RMQTaskQueue/GreetingsSender/Program.cs @@ -23,6 +23,7 @@ THE SOFTWARE. */ #endregion using System; +using System.Transactions; using Greetings.Ports.Commands; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -52,30 +53,34 @@ static void Main(string[] args) AmpqUri = new AmqpUriSpecification(new Uri("amqp://guest:guest@localhost:5672")), Exchange = new Exchange("paramore.brighter.exchange"), }; + + var producerRegistry = new RmqProducerRegistryFactory( + rmqConnection, + new RmqPublication[] + { + new() + { + MaxOutStandingMessages = 5, + MaxOutStandingCheckIntervalMilliSeconds = 500, + WaitForConfirmsTimeOutInMilliseconds = 1000, + MakeChannels =OnMissingChannel.Create, + Topic = new RoutingKey("greeting.event") + }, + new() + { + MaxOutStandingMessages = 5, + MaxOutStandingCheckIntervalMilliSeconds = 500, + WaitForConfirmsTimeOutInMilliseconds = 1000, + MakeChannels =OnMissingChannel.Create, + Topic = new RoutingKey("farewell.event") + } + }).Create(); serviceCollection.AddBrighter() - .UseInMemoryOutbox() - .UseExternalBus(new RmqProducerRegistryFactory( - rmqConnection, - new RmqPublication[] - { - new() - { - MaxOutStandingMessages = 5, - MaxOutStandingCheckIntervalMilliSeconds = 500, - WaitForConfirmsTimeOutInMilliseconds = 1000, - MakeChannels =OnMissingChannel.Create, - Topic = new RoutingKey("greeting.event") - }, - new() - { - MaxOutStandingMessages = 5, - MaxOutStandingCheckIntervalMilliSeconds = 500, - WaitForConfirmsTimeOutInMilliseconds = 1000, - MakeChannels =OnMissingChannel.Create, - Topic = new RoutingKey("farewell.event") - } - }).Create()) + .UseExternalBus((configure) => + { + configure.ProducerRegistry = producerRegistry; + }) .AutoFromAssemblies(); var serviceProvider = serviceCollection.BuildServiceProvider(); diff --git a/samples/RedisTaskQueue/GreetingsReceiver/Program.cs b/samples/RedisTaskQueue/GreetingsReceiver/Program.cs index ef4daddcff..2e7288d371 100644 --- a/samples/RedisTaskQueue/GreetingsReceiver/Program.cs +++ b/samples/RedisTaskQueue/GreetingsReceiver/Program.cs @@ -49,8 +49,7 @@ public static async Task Main(string[] args) options.Subscriptions = subscriptions; options.ChannelFactory = new ChannelFactory(redisConsumerFactory); }) - .UseInMemoryOutbox() - .AutoFromAssemblies(); + .AutoFromAssemblies(); services.AddHostedService(); diff --git a/samples/RedisTaskQueue/GreetingsSender/Program.cs b/samples/RedisTaskQueue/GreetingsSender/Program.cs index 1a318dc712..30d7cd2b74 100644 --- a/samples/RedisTaskQueue/GreetingsSender/Program.cs +++ b/samples/RedisTaskQueue/GreetingsSender/Program.cs @@ -24,6 +24,7 @@ THE SOFTWARE. */ #endregion using System; +using System.Transactions; using Greetings.Ports.Events; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -90,19 +91,23 @@ private static IHostBuilder CreateHostBuilder(string[] args) => MaxPoolSize = 10, MessageTimeToLive = TimeSpan.FromMinutes(10) }; + + var producerRegistry = new RedisProducerRegistryFactory( + redisConnection, + new RedisMessagePublication[] + { + new RedisMessagePublication + { + Topic = new RoutingKey("greeting.event") + } + } + ).Create(); collection.AddBrighter() - .UseInMemoryOutbox() - .UseExternalBus(new RedisProducerRegistryFactory( - redisConnection, - new RedisMessagePublication[] - { - new RedisMessagePublication - { - Topic = new RoutingKey("greeting.event") - } - } - ).Create()) + .UseExternalBus((configure) => + { + configure.ProducerRegistry = producerRegistry; + }) .AutoFromAssemblies(); }); } diff --git a/samples/WebAPI_Dapper/GreetingsEntities/Person.cs b/samples/WebAPI_Dapper/GreetingsEntities/Person.cs index 10dce975d4..0c5aac0483 100644 --- a/samples/WebAPI_Dapper/GreetingsEntities/Person.cs +++ b/samples/WebAPI_Dapper/GreetingsEntities/Person.cs @@ -5,12 +5,12 @@ namespace GreetingsEntities { public class Person { - public byte[] TimeStamp { get; set; } - public long Id { get; set; } + public DateTime TimeStamp { get; set; } + public int Id { get; set; } public string Name { get; set; } public IList Greetings { get; set; } = new List(); - public Person(){ /*Required for DapperExtensions*/} + public Person(){ /*Required for Dapper*/} public Person(string name) { diff --git a/samples/WebAPI_Dapper/GreetingsPorts/EntityMappers/GreetingsMapper.cs b/samples/WebAPI_Dapper/GreetingsPorts/EntityMappers/GreetingsMapper.cs deleted file mode 100644 index ede3108fab..0000000000 --- a/samples/WebAPI_Dapper/GreetingsPorts/EntityMappers/GreetingsMapper.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Data; -using DapperExtensions.Mapper; -using GreetingsEntities; - -namespace GreetingsPorts.EntityMappers; - -public class GreetingsMapper : ClassMapper -{ - public GreetingsMapper() - { - TableName = nameof(Greeting); - Map(g=> g.Id).Column("Id").Key(KeyType.Identity); - Map(g => g.Message).Column("Message"); - Map(g => g.RecipientId).Column("Recipient_Id").Key(KeyType.ForeignKey); - } - -} - diff --git a/samples/WebAPI_Dapper/GreetingsPorts/EntityMappers/PersonMapper.cs b/samples/WebAPI_Dapper/GreetingsPorts/EntityMappers/PersonMapper.cs deleted file mode 100644 index 7a29337bbb..0000000000 --- a/samples/WebAPI_Dapper/GreetingsPorts/EntityMappers/PersonMapper.cs +++ /dev/null @@ -1,17 +0,0 @@ -using DapperExtensions.Mapper; -using GreetingsEntities; - -namespace GreetingsPorts.EntityMappers; - - public class PersonMapper : ClassMapper - { - public PersonMapper() - { - TableName = nameof(Person); - Map(p => p.Id).Column("Id").Key(KeyType.Identity); - Map(p => p.Name).Column("Name"); - Map(p => p.TimeStamp).Column("TimeStamp").Ignore(); - Map(p => p.Greetings).Ignore(); - ReferenceMap(p => p.Greetings).Reference((g, p) => g.RecipientId == p.Id); - } - } diff --git a/samples/WebAPI_Dapper/GreetingsPorts/GreetingsPorts.csproj b/samples/WebAPI_Dapper/GreetingsPorts/GreetingsPorts.csproj index b013116f90..424a59e904 100644 --- a/samples/WebAPI_Dapper/GreetingsPorts/GreetingsPorts.csproj +++ b/samples/WebAPI_Dapper/GreetingsPorts/GreetingsPorts.csproj @@ -4,12 +4,12 @@ net6.0 - - - - - - + + + + + + diff --git a/samples/WebAPI_Dapper/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs b/samples/WebAPI_Dapper/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs index c4f768a767..88e87c9c07 100644 --- a/samples/WebAPI_Dapper/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs +++ b/samples/WebAPI_Dapper/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs @@ -3,16 +3,13 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -using DapperExtensions; -using DapperExtensions.Predicate; +using Dapper; using Paramore.Brighter; -using Paramore.Brighter.Dapper; using GreetingsEntities; using GreetingsPorts.Requests; using Microsoft.Extensions.Logging; using Paramore.Brighter.Logging.Attributes; using Paramore.Brighter.Policies.Attributes; -using Paramore.Brighter.Sqlite.Dapper; namespace GreetingsPorts.Handlers { @@ -20,12 +17,12 @@ public class AddGreetingHandlerAsync: RequestHandlerAsync { private readonly IAmACommandProcessor _postBox; private readonly ILogger _logger; - private readonly SqliteDapperConnectionProvider _uow; + private readonly IAmATransactionConnectionProvider _transactionProvider; - public AddGreetingHandlerAsync(IAmABoxTransactionConnectionProvider uow, IAmACommandProcessor postBox, ILogger logger) + public AddGreetingHandlerAsync(IAmATransactionConnectionProvider transactionProvider, IAmACommandProcessor postBox, ILogger logger) { - _uow = (SqliteDapperConnectionProvider)uow; //We want to take the dependency on the same instance that will be used via the Outbox, so use the marker interface + _transactionProvider = transactionProvider; //We want to take the dependency on the same instance that will be used via the Outbox, so use the marker interface _postBox = postBox; _logger = logger; } @@ -39,33 +36,48 @@ public override async Task HandleAsync(AddGreeting addGreeting, Can //We use the unit of work to grab connection and transaction, because Outbox needs //to share them 'behind the scenes' - var conn = await _uow.GetConnectionAsync(cancellationToken); - await conn.OpenAsync(cancellationToken); - var tx = _uow.GetTransaction(); + var conn = await _transactionProvider.GetConnectionAsync(cancellationToken); + var tx = await _transactionProvider.GetTransactionAsync(cancellationToken); try { - var searchbyName = Predicates.Field(p => p.Name, Operator.Eq, addGreeting.Name); - var people = await conn.GetListAsync(searchbyName, transaction: tx); - var person = people.Single(); - - var greeting = new Greeting(addGreeting.Greeting, person); - - //write the added child entity to the Db - await conn.InsertAsync(greeting, tx); + var people = await conn.QueryAsync( + "select * from Person where name = @name", + new {name = addGreeting.Name}, + tx + ); + var person = people.SingleOrDefault(); - //Now write the message we want to send to the Db in the same transaction. - posts.Add(await _postBox.DepositPostAsync(new GreetingMade(greeting.Greet()), cancellationToken: cancellationToken)); - - //commit both new greeting and outgoing message - await tx.CommitAsync(cancellationToken); + if (person != null) + { + var greeting = new Greeting(addGreeting.Greeting, person); + + //write the added child entity to the Db + await conn.ExecuteAsync( + "insert into Greeting (Message, Recipient_Id) values (@Message, @RecipientId)", + new { greeting.Message, RecipientId = greeting.RecipientId }, + tx); + + //Now write the message we want to send to the Db in the same transaction. + posts.Add(await _postBox.DepositPostAsync( + new GreetingMade(greeting.Greet()), + _transactionProvider, + cancellationToken: cancellationToken)); + + //commit both new greeting and outgoing message + await _transactionProvider.CommitAsync(cancellationToken); + } } catch (Exception e) - { + { _logger.LogError(e, "Exception thrown handling Add Greeting request"); //it went wrong, rollback the entity change and the downstream message - await tx.RollbackAsync(cancellationToken); + await _transactionProvider.RollbackAsync(cancellationToken); return await base.HandleAsync(addGreeting, cancellationToken); } + finally + { + _transactionProvider.Close(); + } //Send this message via a transport. We need the ids to send just the messages here, not all outstanding ones. //Alternatively, you can let the Sweeper do this, but at the cost of increased latency diff --git a/samples/WebAPI_Dapper/GreetingsPorts/Handlers/AddPersonHandlerAsync.cs b/samples/WebAPI_Dapper/GreetingsPorts/Handlers/AddPersonHandlerAsync.cs index 21baf781e5..f5e0d276d9 100644 --- a/samples/WebAPI_Dapper/GreetingsPorts/Handlers/AddPersonHandlerAsync.cs +++ b/samples/WebAPI_Dapper/GreetingsPorts/Handlers/AddPersonHandlerAsync.cs @@ -1,10 +1,8 @@ using System.Threading; using System.Threading.Tasks; -using DapperExtensions; -using GreetingsEntities; +using Dapper; using GreetingsPorts.Requests; using Paramore.Brighter; -using Paramore.Brighter.Dapper; using Paramore.Brighter.Logging.Attributes; using Paramore.Brighter.Policies.Attributes; @@ -12,19 +10,19 @@ namespace GreetingsPorts.Handlers { public class AddPersonHandlerAsync : RequestHandlerAsync { - private readonly IUnitOfWork _uow; + private readonly IAmARelationalDbConnectionProvider _relationalDbConnectionProvider; - public AddPersonHandlerAsync(IUnitOfWork uow) + public AddPersonHandlerAsync(IAmARelationalDbConnectionProvider relationalDbConnectionProvider) { - _uow = uow; + _relationalDbConnectionProvider = relationalDbConnectionProvider; } [RequestLoggingAsync(0, HandlerTiming.Before)] [UsePolicyAsync(step:1, policy: Policies.Retry.EXPONENTIAL_RETRYPOLICYASYNC)] public override async Task HandleAsync(AddPerson addPerson, CancellationToken cancellationToken = default) { - await _uow.Database.InsertAsync(new Person(addPerson.Name)); - + await using var connection = await _relationalDbConnectionProvider.GetConnectionAsync(cancellationToken); + await connection.ExecuteAsync("insert into Person (Name) values (@Name)", new {Name = addPerson.Name}); return await base.HandleAsync(addPerson, cancellationToken); } } diff --git a/samples/WebAPI_Dapper/GreetingsPorts/Handlers/DeletePersonHandlerAsync.cs b/samples/WebAPI_Dapper/GreetingsPorts/Handlers/DeletePersonHandlerAsync.cs index f3b300dcd7..fe2731a534 100644 --- a/samples/WebAPI_Dapper/GreetingsPorts/Handlers/DeletePersonHandlerAsync.cs +++ b/samples/WebAPI_Dapper/GreetingsPorts/Handlers/DeletePersonHandlerAsync.cs @@ -2,12 +2,11 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -using DapperExtensions; -using DapperExtensions.Predicate; +using Dapper; using GreetingsEntities; using GreetingsPorts.Requests; +using Microsoft.Extensions.Logging; using Paramore.Brighter; -using Paramore.Brighter.Dapper; using Paramore.Brighter.Logging.Attributes; using Paramore.Brighter.Policies.Attributes; @@ -15,36 +14,57 @@ namespace GreetingsPorts.Handlers { public class DeletePersonHandlerAsync : RequestHandlerAsync { - private readonly IUnitOfWork _uow; + private readonly IAmARelationalDbConnectionProvider _relationalDbConnectionProvider; + private readonly ILogger _logger; - public DeletePersonHandlerAsync(IUnitOfWork uow) + public DeletePersonHandlerAsync(IAmARelationalDbConnectionProvider relationalDbConnectionProvider, ILogger logger) { - _uow = uow; + _relationalDbConnectionProvider = relationalDbConnectionProvider; + _logger = logger; } [RequestLoggingAsync(0, HandlerTiming.Before)] [UsePolicyAsync(step:1, policy: Policies.Retry.EXPONENTIAL_RETRYPOLICYASYNC)] - public async override Task HandleAsync(DeletePerson deletePerson, CancellationToken cancellationToken = default) + public override async Task HandleAsync(DeletePerson deletePerson, CancellationToken cancellationToken = default) { - var tx = await _uow.BeginOrGetTransactionAsync(cancellationToken); + var connection = await _relationalDbConnectionProvider.GetConnectionAsync(cancellationToken); + var tx = await connection.BeginTransactionAsync(cancellationToken); try { + var people = await connection.QueryAsync( + "select * from Person where name = @name", + new {name = deletePerson.Name}, + tx + ); + var person = people.SingleOrDefault(); - var searchbyName = Predicates.Field(p => p.Name, Operator.Eq, deletePerson.Name); - var people = await _uow.Database.GetListAsync(searchbyName, transaction: tx); - var person = people.Single(); + if (person != null) + { + await connection.ExecuteAsync( + "delete from Greeting where Recipient_Id = @PersonId", + new { PersonId = person.Id }, + tx); + + await connection.ExecuteAsync("delete from Person where Id = @Id", + new {Id = person.Id}, + tx); - var deleteById = Predicates.Field(g => g.RecipientId, Operator.Eq, person.Id); - await _uow.Database.DeleteAsync(deleteById, tx); - - await tx.CommitAsync(cancellationToken); + await tx.CommitAsync(cancellationToken); + } } - catch (Exception) + catch (Exception e) { + _logger.LogError(e, "Exception thrown handling Add Greeting request"); //it went wrong, rollback the entity change and the downstream message await tx.RollbackAsync(cancellationToken); return await base.HandleAsync(deletePerson, cancellationToken); } + finally + { + await connection.DisposeAsync(); + await tx.DisposeAsync(); + + } return await base.HandleAsync(deletePerson, cancellationToken); } diff --git a/samples/WebAPI_Dapper/GreetingsPorts/Handlers/FIndGreetingsForPersonHandlerAsync.cs b/samples/WebAPI_Dapper/GreetingsPorts/Handlers/FIndGreetingsForPersonHandlerAsync.cs index d908df5c2c..45752cff49 100644 --- a/samples/WebAPI_Dapper/GreetingsPorts/Handlers/FIndGreetingsForPersonHandlerAsync.cs +++ b/samples/WebAPI_Dapper/GreetingsPorts/Handlers/FIndGreetingsForPersonHandlerAsync.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Dapper; @@ -6,7 +7,7 @@ using GreetingsPorts.Policies; using GreetingsPorts.Requests; using GreetingsPorts.Responses; -using Paramore.Brighter.Dapper; +using Paramore.Brighter; using Paramore.Darker; using Paramore.Darker.Policies; using Paramore.Darker.QueryLogging; @@ -15,11 +16,11 @@ namespace GreetingsPorts.Handlers { public class FIndGreetingsForPersonHandlerAsync : QueryHandlerAsync { - private readonly IUnitOfWork _uow; + private readonly IAmARelationalDbConnectionProvider _relationalDbConnectionProvider; - public FIndGreetingsForPersonHandlerAsync(IUnitOfWork uow) + public FIndGreetingsForPersonHandlerAsync(IAmARelationalDbConnectionProvider relationalDbConnectionProvider) { - _uow = uow; + _relationalDbConnectionProvider = relationalDbConnectionProvider; } [QueryLogging(0)] @@ -33,11 +34,17 @@ public FIndGreetingsForPersonHandlerAsync(IUnitOfWork uow) var sql = @"select p.Id, p.Name, g.Id, g.Message from Person p inner join Greeting g on g.Recipient_Id = p.Id"; - var people = await _uow.Database.QueryAsync(sql, (person, greeting) => + await using var connection = await _relationalDbConnectionProvider.GetConnectionAsync(cancellationToken); + var people = await connection.QueryAsync(sql, (person, greeting) => { - person.Greetings.Add(greeting); - return person; + person.Greetings.Add(greeting); + return person; }, splitOn: "Id"); + + if (!people.Any()) + { + return new FindPersonsGreetings(){Name = query.Name, Greetings = Array.Empty()}; + } var peopleGreetings = people.GroupBy(p => p.Id).Select(grp => { @@ -50,10 +57,8 @@ from Person p return new FindPersonsGreetings { - Name = person.Name, - Greetings = person.Greetings.Select(g => new Salutation(g.Greet())) + Name = person.Name, Greetings = person.Greetings.Select(g => new Salutation(g.Greet())) }; - } } diff --git a/samples/WebAPI_Dapper/GreetingsPorts/Handlers/FindPersonByNameHandlerAsync.cs b/samples/WebAPI_Dapper/GreetingsPorts/Handlers/FindPersonByNameHandlerAsync.cs index 1ab5898541..7ada5290dc 100644 --- a/samples/WebAPI_Dapper/GreetingsPorts/Handlers/FindPersonByNameHandlerAsync.cs +++ b/samples/WebAPI_Dapper/GreetingsPorts/Handlers/FindPersonByNameHandlerAsync.cs @@ -1,13 +1,12 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -using DapperExtensions; -using DapperExtensions.Predicate; +using Dapper; using GreetingsEntities; using GreetingsPorts.Policies; using GreetingsPorts.Requests; using GreetingsPorts.Responses; -using Paramore.Brighter.Dapper; +using Paramore.Brighter; using Paramore.Darker; using Paramore.Darker.Policies; using Paramore.Darker.QueryLogging; @@ -16,20 +15,20 @@ namespace GreetingsPorts.Handlers { public class FindPersonByNameHandlerAsync : QueryHandlerAsync { - private readonly IUnitOfWork _uow; + private readonly IAmARelationalDbConnectionProvider _relationalDbConnectionProvider; - public FindPersonByNameHandlerAsync(IUnitOfWork uow) + public FindPersonByNameHandlerAsync(IAmARelationalDbConnectionProvider relationalDbConnectionProvider) { - _uow = uow; + _relationalDbConnectionProvider = relationalDbConnectionProvider; } [QueryLogging(0)] [RetryableQuery(1, Retry.EXPONENTIAL_RETRYPOLICYASYNC)] public override async Task ExecuteAsync(FindPersonByName query, CancellationToken cancellationToken = new CancellationToken()) { - var searchbyName = Predicates.Field(p => p.Name, Operator.Eq, query.Name); - var people = await _uow.Database.GetListAsync(searchbyName); - var person = people.Single(); + await using var connection = await _relationalDbConnectionProvider .GetConnectionAsync(cancellationToken); + var people = await connection.QueryAsync("select * from Person where name = @name", new {name = query.Name}); + var person = people.SingleOrDefault(); return new FindPersonResult(person); } diff --git a/samples/WebAPI_Dapper/GreetingsPorts/Responses/FindPersonResult.cs b/samples/WebAPI_Dapper/GreetingsPorts/Responses/FindPersonResult.cs index ea50242c8c..69529a9803 100644 --- a/samples/WebAPI_Dapper/GreetingsPorts/Responses/FindPersonResult.cs +++ b/samples/WebAPI_Dapper/GreetingsPorts/Responses/FindPersonResult.cs @@ -4,10 +4,10 @@ namespace GreetingsPorts.Responses { public class FindPersonResult { - public string Name { get; private set; } + public Person Person { get; private set; } public FindPersonResult(Person person) { - Name = person.Name; + Person = person; } } diff --git a/samples/WebAPI_Dapper/GreetingsWeb/Controllers/PeopleController.cs b/samples/WebAPI_Dapper/GreetingsWeb/Controllers/PeopleController.cs index 0ce06da254..7feab500f7 100644 --- a/samples/WebAPI_Dapper/GreetingsWeb/Controllers/PeopleController.cs +++ b/samples/WebAPI_Dapper/GreetingsWeb/Controllers/PeopleController.cs @@ -29,7 +29,7 @@ public async Task> Get(string name) { var foundPerson = await _queryProcessor.ExecuteAsync(new FindPersonByName(name)); - if (foundPerson == null) return new NotFoundResult(); + if (foundPerson.Person == null) return new NotFoundResult(); return Ok(foundPerson); } diff --git a/samples/WebAPI_Dapper/GreetingsWeb/Database/OutboxExtensions.cs b/samples/WebAPI_Dapper/GreetingsWeb/Database/OutboxExtensions.cs index 80f172a2be..ddaae874fe 100644 --- a/samples/WebAPI_Dapper/GreetingsWeb/Database/OutboxExtensions.cs +++ b/samples/WebAPI_Dapper/GreetingsWeb/Database/OutboxExtensions.cs @@ -1,63 +1,73 @@ using System; +using GreetingsEntities; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Npgsql; +using Npgsql.NameTranslation; +using Paramore.Brighter; using Paramore.Brighter.Extensions.DependencyInjection; using Paramore.Brighter.Extensions.Hosting; +using Paramore.Brighter.MsSql; using Paramore.Brighter.MySql; -using Paramore.Brighter.MySql.Dapper; +using Paramore.Brighter.Outbox.MsSql; using Paramore.Brighter.Outbox.MySql; +using Paramore.Brighter.Outbox.PostgreSql; using Paramore.Brighter.Outbox.Sqlite; +using Paramore.Brighter.PostgreSql; using Paramore.Brighter.Sqlite; -using Paramore.Brighter.Sqlite.Dapper; -using UnitOfWork = Paramore.Brighter.MySql.Dapper.UnitOfWork; -namespace GreetingsWeb.Database; - -public static class OutboxExtensions +namespace GreetingsWeb.Database { - public static IBrighterBuilder AddOutbox(this IBrighterBuilder brighterBuilder, IWebHostEnvironment env, DatabaseType databaseType, - string dbConnectionString, string outBoxTableName) + + public class OutboxExtensions { - if (env.IsDevelopment()) - { - AddSqliteOutBox(brighterBuilder, dbConnectionString, outBoxTableName); - } - else + public static (IAmAnOutbox, Type, Type) MakeOutbox( + IWebHostEnvironment env, + DatabaseType databaseType, + RelationalDatabaseConfiguration configuration, + IServiceCollection services) { - switch (databaseType) + (IAmAnOutbox, Type, Type) outbox; + if (env.IsDevelopment()) + { + outbox = MakeSqliteOutBox(configuration); + } + else { - case DatabaseType.MySql: - AddMySqlOutbox(brighterBuilder, dbConnectionString, outBoxTableName); - break; - default: - throw new InvalidOperationException("Unknown Db type for Outbox configuration"); + outbox = databaseType switch + { + DatabaseType.MySql => MakeMySqlOutbox(configuration), + DatabaseType.MsSql => MakeMsSqlOutbox(configuration), + DatabaseType.Postgres => MakePostgresSqlOutbox(configuration, services), + DatabaseType.Sqlite => MakeSqliteOutBox(configuration), + _ => throw new InvalidOperationException("Unknown Db type for Outbox configuration") + }; } + + return outbox; } - return brighterBuilder; - } - private static void AddMySqlOutbox(IBrighterBuilder brighterBuilder, string dbConnectionString, string outBoxTableName) - { - brighterBuilder.UseMySqlOutbox( - new MySqlConfiguration(dbConnectionString, outBoxTableName), - typeof(MySqlConnectionProvider), - ServiceLifetime.Singleton) - .UseMySqTransactionConnectionProvider(typeof(Paramore.Brighter.MySql.Dapper.MySqlDapperConnectionProvider), ServiceLifetime.Scoped) - .UseOutboxSweeper(); - } + private static (IAmAnOutbox, Type, Type) MakePostgresSqlOutbox( + RelationalDatabaseConfiguration configuration, + IServiceCollection services) + { + return (new PostgreSqlOutbox(configuration), typeof(PostgreSqlConnectionProvider), typeof(PostgreSqlUnitOfWork)); + } - private static void AddSqliteOutBox(IBrighterBuilder brighterBuilder, string dbConnectionString, string outBoxTableName) - { - brighterBuilder.UseSqliteOutbox( - new SqliteConfiguration(dbConnectionString, outBoxTableName), - typeof(SqliteConnectionProvider), - ServiceLifetime.Singleton) - .UseSqliteTransactionConnectionProvider(typeof(Paramore.Brighter.Sqlite.Dapper.SqliteDapperConnectionProvider), ServiceLifetime.Scoped) - .UseOutboxSweeper(options => - { - options.TimerInterval = 5; - options.MinimumMessageAge = 5000; - }); + private static (IAmAnOutbox, Type, Type) MakeMsSqlOutbox(RelationalDatabaseConfiguration configuration) + { + return new(new MsSqlOutbox(configuration), typeof(MsSqlConnectionProvider), typeof(MsSqlUnitOfWork)); + } + + private static (IAmAnOutbox, Type, Type) MakeMySqlOutbox(RelationalDatabaseConfiguration configuration) + { + return (new MySqlOutbox(configuration), typeof (MySqlConnectionProvider), typeof(MySqlUnitOfWork)); + } + + private static (IAmAnOutbox, Type, Type) MakeSqliteOutBox(RelationalDatabaseConfiguration configuration) + { + return (new SqliteOutbox(configuration), typeof(SqliteConnectionProvider), typeof(SqliteUnitOfWork)); + } } } diff --git a/samples/WebAPI_Dapper/GreetingsWeb/Database/SchemaCreation.cs b/samples/WebAPI_Dapper/GreetingsWeb/Database/SchemaCreation.cs index d628c0bdb4..639a46424d 100644 --- a/samples/WebAPI_Dapper/GreetingsWeb/Database/SchemaCreation.cs +++ b/samples/WebAPI_Dapper/GreetingsWeb/Database/SchemaCreation.cs @@ -2,6 +2,7 @@ using System.Data; using System.Data.Common; using FluentMigrator.Runner; +using GreetingsWeb.Messaging; using Microsoft.AspNetCore.Hosting; using Microsoft.Data.SqlClient; using Microsoft.Data.Sqlite; @@ -10,7 +11,10 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using MySqlConnector; +using Npgsql; +using Paramore.Brighter.Outbox.MsSql; using Paramore.Brighter.Outbox.MySql; +using Paramore.Brighter.Outbox.PostgreSql; using Paramore.Brighter.Outbox.Sqlite; using Polly; @@ -27,16 +31,33 @@ public static IHost CheckDbIsUp(this IHost webHost) var services = scope.ServiceProvider; var env = services.GetService(); var config = services.GetService(); - string connectionString = DbServerConnectionString(config, env); + var (dbType, connectionString) = DbServerConnectionString(config, env); - //We don't check in development as using Sqlite + //We don't check db availability in development as we always use Sqlite which is a file not a server if (env.IsDevelopment()) return webHost; - WaitToConnect(connectionString); - CreateDatabaseIfNotExists(GetDbConnection(GetDatabaseType(config), connectionString)); + WaitToConnect(dbType, connectionString); + CreateDatabaseIfNotExists(dbType, GetDbConnection(dbType, connectionString)); return webHost; } + + public static IHost CreateOutbox(this IHost webHost, bool hasBinaryPayload) + { + using var scope = webHost.Services.CreateScope(); + var services = scope.ServiceProvider; + var env = services.GetService(); + var config = services.GetService(); + + CreateOutbox(config, env, hasBinaryPayload); + + return webHost; + } + + public static bool HasBinaryMessagePayload(this IHost webHost) + { + return GetTransportType(Environment.GetEnvironmentVariable("BRIGHTER_TRANSPORT")) == MessagingTransport.Kafka; + } public static IHost MigrateDatabase(this IHost webHost) { @@ -61,40 +82,60 @@ public static IHost MigrateDatabase(this IHost webHost) return webHost; } - public static IHost CreateOutbox(this IHost webHost) - { - using (var scope = webHost.Services.CreateScope()) - { - var services = scope.ServiceProvider; - var env = services.GetService(); - var config = services.GetService(); - CreateOutbox(config, env); - } - - return webHost; - } - - private static void CreateDatabaseIfNotExists(DbConnection conn) + private static void CreateDatabaseIfNotExists(DatabaseType databaseType, DbConnection conn) { //The migration does not create the Db, so we need to create it sot that it will add it conn.Open(); using var command = conn.CreateCommand(); - command.CommandText = "CREATE DATABASE IF NOT EXISTS Greetings"; - command.ExecuteScalar(); - } + command.CommandText = databaseType switch + { + DatabaseType.Sqlite => "CREATE DATABASE IF NOT EXISTS Greetings", + DatabaseType.MySql => "CREATE DATABASE IF NOT EXISTS Greetings", + DatabaseType.Postgres => "CREATE DATABASE Greetings", + DatabaseType.MsSql => + "IF NOT EXISTS(SELECT * FROM sys.databases WHERE name = 'Greetings') CREATE DATABASE Greetings", + _ => throw new InvalidOperationException("Could not create instance of Greetings for unknown Db type") + }; + + try + { + command.ExecuteScalar(); + } + catch (NpgsqlException pe) + { + //Ignore if the Db already exists - we can't test for this in the SQL for Postgres + if (!pe.Message.Contains("already exists")) + throw; + } + catch (System.Exception e) + { + Console.WriteLine($"Issue with creating Greetings tables, {e.Message}"); + //Rethrow, if we can't create the Outbox, shut down + throw; + } + } - private static void CreateOutbox(IConfiguration config, IWebHostEnvironment env) + private static void CreateOutbox(IConfiguration config, IWebHostEnvironment env, bool hasBinaryPayload) { try { var connectionString = DbConnectionString(config, env); if (env.IsDevelopment()) - CreateOutboxDevelopment(connectionString); + CreateOutboxDevelopment(connectionString, hasBinaryPayload); else - CreateOutboxProduction(GetDatabaseType(config), connectionString); + CreateOutboxProduction(GetDatabaseType(config), connectionString, hasBinaryPayload); + } + catch (NpgsqlException pe) + { + //Ignore if the Db already exists - we can't test for this in the SQL for Postgres + if (!pe.Message.Contains("already exists")) + { + Console.WriteLine($"Issue with creating Outbox table, {pe.Message}"); + throw; + } } catch (System.Exception e) { @@ -104,69 +145,111 @@ private static void CreateOutbox(IConfiguration config, IWebHostEnvironment env) } } - private static void CreateOutboxDevelopment(string connectionString) + private static void CreateOutboxDevelopment(string connectionString, bool hasBinaryPayload) { - CreateOutboxSqlite(connectionString); - } - - private static void CreateOutboxSqlite(string connectionString) - { - using var sqlConnection = new SqliteConnection(connectionString); - sqlConnection.Open(); - - using var exists = sqlConnection.CreateCommand(); - exists.CommandText = SqliteOutboxBuilder.GetExistsQuery(OUTBOX_TABLE_NAME); - using var reader = exists.ExecuteReader(CommandBehavior.SingleRow); - - if (reader.HasRows) return; - - using var command = sqlConnection.CreateCommand(); - command.CommandText = SqliteOutboxBuilder.GetDDL(OUTBOX_TABLE_NAME); - command.ExecuteScalar(); + CreateOutboxSqlite(connectionString, hasBinaryPayload); } - private static void CreateOutboxProduction(DatabaseType databaseType, string connectionString) - { + private static void CreateOutboxProduction(DatabaseType databaseType, string connectionString, bool hasBinaryPayload) + { switch (databaseType) { case DatabaseType.MySql: - CreateOutboxMySql(connectionString); + CreateOutboxMySql(connectionString, hasBinaryPayload); + break; + case DatabaseType.MsSql: + CreateOutboxMsSql(connectionString, hasBinaryPayload); + break; + case DatabaseType.Postgres: + CreateOutboxPostgres(connectionString, hasBinaryPayload); + break; + case DatabaseType.Sqlite: + CreateOutboxSqlite(connectionString, hasBinaryPayload); break; default: throw new InvalidOperationException("Could not create instance of Outbox for unknown Db type"); } } - private static void CreateOutboxMySql(string connectionString) + private static void CreateOutboxMsSql(string connectionString, bool hasBinaryPayload) + { + using var sqlConnection = new SqlConnection(connectionString); + sqlConnection.Open(); + + using var existsQuery = sqlConnection.CreateCommand(); + existsQuery.CommandText = SqlOutboxBuilder.GetExistsQuery(OUTBOX_TABLE_NAME); + var findOutbox = existsQuery.ExecuteScalar(); + bool exists = findOutbox is > 0; + + if (exists) return; + + using var command = sqlConnection.CreateCommand(); + command.CommandText = SqlOutboxBuilder.GetDDL(OUTBOX_TABLE_NAME, hasBinaryPayload); + command.ExecuteScalar(); + + } + + private static void CreateOutboxMySql(string connectionString, bool hasBinaryPayload) { using var sqlConnection = new MySqlConnection(connectionString); sqlConnection.Open(); using var existsQuery = sqlConnection.CreateCommand(); existsQuery.CommandText = MySqlOutboxBuilder.GetExistsQuery(OUTBOX_TABLE_NAME); - bool exists = existsQuery.ExecuteScalar() != null; + var findOutbox = existsQuery.ExecuteScalar(); + bool exists = findOutbox is long and > 0; if (exists) return; using var command = sqlConnection.CreateCommand(); - command.CommandText = MySqlOutboxBuilder.GetDDL(OUTBOX_TABLE_NAME); + command.CommandText = MySqlOutboxBuilder.GetDDL(OUTBOX_TABLE_NAME, hasBinaryPayload); command.ExecuteScalar(); } + + private static void CreateOutboxPostgres(string connectionString, bool hasBinaryPayload) + { + using var sqlConnection = new NpgsqlConnection(connectionString); + sqlConnection.Open(); + + using var existsQuery = sqlConnection.CreateCommand(); + existsQuery.CommandText = PostgreSqlOutboxBulder.GetExistsQuery(OUTBOX_TABLE_NAME); + var findOutbox = existsQuery.ExecuteScalar(); + bool exists = findOutbox is long and > 0; + + if (exists) return; + + using var command = sqlConnection.CreateCommand(); + command.CommandText = PostgreSqlOutboxBulder.GetDDL(OUTBOX_TABLE_NAME, hasBinaryPayload); + command.ExecuteScalar(); + } - private static string DbConnectionString(IConfiguration config, IWebHostEnvironment env) + private static void CreateOutboxSqlite(string connectionString, bool hasBinaryPayload) { - //NOTE: Sqlite needs to use a shared cache to allow Db writes to the Outbox as well as entities - return env.IsDevelopment() ? GetDevDbConnectionString() : GetProductionDbConnectionString(config, GetDatabaseType(config)); + using var sqlConnection = new SqliteConnection(connectionString); + sqlConnection.Open(); + + using var exists = sqlConnection.CreateCommand(); + exists.CommandText = SqliteOutboxBuilder.GetExistsQuery(OUTBOX_TABLE_NAME); + using var reader = exists.ExecuteReader(CommandBehavior.SingleRow); + + if (reader.HasRows) return; + + using var command = sqlConnection.CreateCommand(); + command.CommandText = SqliteOutboxBuilder.GetDDL(OUTBOX_TABLE_NAME, hasBinaryPayload); + command.ExecuteScalar(); } - private static string GetDevDbConnectionString() + private static string DbConnectionString(IConfiguration config, IWebHostEnvironment env) { - return "Filename=Greetings.db;Cache=Shared"; + //NOTE: Sqlite needs to use a shared cache to allow Db writes to the Outbox as well as entities + return env.IsDevelopment() ? GetDevConnectionString() : GetProductionDbConnectionString(config, GetDatabaseType(config)); } - private static string DbServerConnectionString(IConfiguration config, IWebHostEnvironment env) + private static (DatabaseType, string) DbServerConnectionString(IConfiguration config, IWebHostEnvironment env) { - return env.IsDevelopment() ? GetDevConnectionString() : GetProductionConnectionString(config, GetDatabaseType(config)); + var databaseType = GetDatabaseType(config); + var connectionString = env.IsDevelopment() ? GetDevConnectionString() : GetProductionConnectionString(config, databaseType); + return (databaseType, connectionString); } private static string GetDevConnectionString() @@ -179,6 +262,9 @@ private static DbConnection GetDbConnection(DatabaseType databaseType, string co return databaseType switch { DatabaseType.MySql => new MySqlConnection(connectionString), + DatabaseType.MsSql => new SqlConnection(connectionString), + DatabaseType.Postgres => new NpgsqlConnection(connectionString), + DatabaseType.Sqlite => new SqliteConnection(connectionString), _ => throw new InvalidOperationException("Could not determine the database type") }; } @@ -187,7 +273,10 @@ private static string GetProductionConnectionString(IConfiguration config, Datab { return databaseType switch { - DatabaseType.MySql => config.GetConnectionString("GreetingsMySql"), + DatabaseType.MySql => config.GetConnectionString("MySqlDb"), + DatabaseType.MsSql => config.GetConnectionString("MsSqlDb"), + DatabaseType.Postgres => config.GetConnectionString("PostgreSqlDb"), + DatabaseType.Sqlite => GetDevConnectionString(), _ => throw new InvalidOperationException("Could not determine the database type") }; } @@ -197,6 +286,9 @@ private static string GetProductionDbConnectionString(IConfiguration config, Dat return databaseType switch { DatabaseType.MySql => config.GetConnectionString("GreetingsMySql"), + DatabaseType.MsSql => config.GetConnectionString("GreetingsMsSql"), + DatabaseType.Postgres => config.GetConnectionString("GreetingsPostgreSql"), + DatabaseType.Sqlite => GetDevConnectionString(), _ => throw new InvalidOperationException("Could not determine the database type") }; } @@ -213,9 +305,9 @@ private static DatabaseType GetDatabaseType(IConfiguration config) }; } - private static void WaitToConnect(string connectionString) + private static void WaitToConnect(DatabaseType dbType, string connectionString) { - var policy = Policy.Handle().WaitAndRetryForever( + var policy = Policy.Handle().WaitAndRetryForever( retryAttempt => TimeSpan.FromSeconds(2), (exception, timespan) => { @@ -224,9 +316,33 @@ private static void WaitToConnect(string connectionString) policy.Execute(() => { - using var conn = new MySqlConnection(connectionString); + using var conn = GetConnection(dbType, connectionString); conn.Open(); }); } + + private static DbConnection GetConnection(DatabaseType databaseType, string connectionString) + { + return databaseType switch + { + DatabaseType.MySql => new MySqlConnection(connectionString), + DatabaseType.MsSql => new SqlConnection(connectionString), + DatabaseType.Postgres => new NpgsqlConnection(connectionString), + DatabaseType.Sqlite => new SqliteConnection(connectionString), + _ => throw new ArgumentOutOfRangeException(nameof(databaseType), databaseType, null) + }; + } + + private static MessagingTransport GetTransportType(string brighterTransport) + { + return brighterTransport switch + { + MessagingGlobals.RMQ => MessagingTransport.Rmq, + MessagingGlobals.KAFKA => MessagingTransport.Kafka, + _ => throw new ArgumentOutOfRangeException(nameof(MessagingGlobals.BRIGHTER_TRANSPORT), + "Messaging transport is not supported") + }; + } + } } diff --git a/samples/WebAPI_Dapper/GreetingsWeb/GreetingsWeb.csproj b/samples/WebAPI_Dapper/GreetingsWeb/GreetingsWeb.csproj index 3d30f65e5d..74cb5cb80a 100644 --- a/samples/WebAPI_Dapper/GreetingsWeb/GreetingsWeb.csproj +++ b/samples/WebAPI_Dapper/GreetingsWeb/GreetingsWeb.csproj @@ -8,6 +8,10 @@ + + + + @@ -16,14 +20,14 @@ + - + + - - - + @@ -51,4 +55,10 @@ <_ContentIncludedByDefault Remove="out\GreetingsAdapters.runtimeconfig.json" /> + + + ..\..\..\libs\Npgsql\net6.0\Npgsql.dll + + + diff --git a/samples/WebAPI_Dapper/GreetingsWeb/Messaging/MessagingTransport.cs b/samples/WebAPI_Dapper/GreetingsWeb/Messaging/MessagingTransport.cs new file mode 100644 index 0000000000..c4811dcf1b --- /dev/null +++ b/samples/WebAPI_Dapper/GreetingsWeb/Messaging/MessagingTransport.cs @@ -0,0 +1,21 @@ +namespace GreetingsWeb.Messaging; + +public static class MessagingGlobals +{ + //environment string key + public const string BRIGHTER_TRANSPORT = "BRIGHTER_TRANSPORT"; + + public const string RMQ = "RabbitMQ"; + public const string KAFKA = "Kafka"; +} + + +///

+/// Which messaging transport are you using? +/// +public enum MessagingTransport +{ + Rmq, + Kafka +} + diff --git a/samples/WebAPI_Dapper/GreetingsWeb/Program.cs b/samples/WebAPI_Dapper/GreetingsWeb/Program.cs index 749522914a..8e6dfbe0b6 100644 --- a/samples/WebAPI_Dapper/GreetingsWeb/Program.cs +++ b/samples/WebAPI_Dapper/GreetingsWeb/Program.cs @@ -16,7 +16,7 @@ public static void Main(string[] args) host.CheckDbIsUp(); host.MigrateDatabase(); - host.CreateOutbox(); + host.CreateOutbox(host.HasBinaryMessagePayload()); host.Run(); } diff --git a/samples/WebAPI_Dapper/GreetingsWeb/Properties/launchSettings.json b/samples/WebAPI_Dapper/GreetingsWeb/Properties/launchSettings.json index 1186c52e1f..14641d72fe 100644 --- a/samples/WebAPI_Dapper/GreetingsWeb/Properties/launchSettings.json +++ b/samples/WebAPI_Dapper/GreetingsWeb/Properties/launchSettings.json @@ -9,7 +9,7 @@ } }, "profiles": { - "Development": { + "Development": { "commandName": "Project", "dotnetRunMessages": "true", "launchBrowser": true, @@ -17,10 +17,11 @@ "applicationUrl": "https://localhost:5001;http://localhost:5000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", - "BRIGHTER_GREETINGS_DATABASE": "Sqlite" + "BRIGHTER_GREETINGS_DATABASE": "Sqlite", + "BRIGHTER_TRANSPORT": "Kafka" } }, - "Production": { + "ProductionMySql": { "commandName": "Project", "dotnetRunMessages": "true", "launchBrowser": true, @@ -28,7 +29,32 @@ "applicationUrl": "https://localhost:5001;http://localhost:5000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Production", - "BRIGHTER_GREETINGS_DATABASE": "MySQL" + "BRIGHTER_GREETINGS_DATABASE": "MySQL", + "BRIGHTER_TRANSPORT": "RabbitMQ" + } + }, + "ProductionPostgres": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Production", + "BRIGHTER_GREETINGS_DATABASE": "PostgresSQL", + "BRIGHTER_TRANSPORT": "RabbitMQ" + } + }, + "ProductionMsSql": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Production", + "BRIGHTER_GREETINGS_DATABASE": "MsSQL", + "BRIGHTER_TRANSPORT": "RabbitMQ" } } } diff --git a/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs b/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs index 5a16c8a4a3..b6193994fe 100644 --- a/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs +++ b/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs @@ -1,13 +1,11 @@ using System; -using DapperExtensions; -using DapperExtensions.Sql; +using Confluent.SchemaRegistry; using FluentMigrator.Runner; using Greetings_MySqlMigrations.Migrations; -using Greetings_SqliteMigrations.Migrations; -using GreetingsPorts.EntityMappers; using GreetingsPorts.Handlers; using GreetingsPorts.Policies; using GreetingsWeb.Database; +using GreetingsWeb.Messaging; using Hellang.Middleware.ProblemDetails; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; @@ -15,9 +13,12 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.OpenApi.Models; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; using Paramore.Brighter; -using Paramore.Brighter.Dapper; using Paramore.Brighter.Extensions.DependencyInjection; +using Paramore.Brighter.Extensions.Hosting; +using Paramore.Brighter.MessagingGateway.Kafka; using Paramore.Brighter.MessagingGateway.RMQ; using Paramore.Darker.AspNetCore; using Paramore.Darker.Policies; @@ -27,16 +28,24 @@ namespace GreetingsWeb { public class Startup { - private const string _outBoxTableName = "Outbox"; - private IWebHostEnvironment _env; + + private readonly IConfiguration _configuration; + private readonly IWebHostEnvironment _env; public Startup(IConfiguration configuration, IWebHostEnvironment env) { - Configuration = configuration; + _configuration = configuration; _env = env; } - public IConfiguration Configuration { get; } + private void AddSchemaRegistryMaybe(IServiceCollection services, MessagingTransport messagingTransport) + { + if (messagingTransport != MessagingTransport.Kafka) return; + + var schemaRegistryConfig = new SchemaRegistryConfig { Url = "http://localhost:8081" }; + var cachedSchemaRegistryClient = new CachedSchemaRegistryClient(schemaRegistryConfig); + services.AddSingleton(cachedSchemaRegistryClient); + } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) @@ -71,29 +80,26 @@ public void ConfigureServices(IServiceCollection services) c.SwaggerDoc("v1", new OpenApiInfo { Title = "GreetingsAPI", Version = "v1" }); }); + services.AddOpenTelemetry() + .WithTracing(builder => builder + .AddAspNetCoreInstrumentation() + .AddConsoleExporter()) + .WithMetrics(builder => builder + .AddAspNetCoreInstrumentation() + .AddConsoleExporter()); + ConfigureMigration(services); - ConfigureDapper(services); ConfigureBrighter(services); ConfigureDarker(services); } private void ConfigureMigration(IServiceCollection services) { + //dev is always Sqlite if (_env.IsDevelopment()) - { - services - .AddFluentMigratorCore() - .ConfigureRunner(c => - { - c.AddSQLite() - .WithGlobalConnectionString(DbConnectionString()) - .ScanIn(typeof(SqlliteInitialCreate).Assembly).For.Migrations(); - }); - } + ConfigureSqlite(services); else - { ConfigureProductionDatabase(GetDatabaseType(), services); - } } private void ConfigureProductionDatabase(DatabaseType databaseType, IServiceCollection services) @@ -103,64 +109,80 @@ private void ConfigureProductionDatabase(DatabaseType databaseType, IServiceColl case DatabaseType.MySql: ConfigureMySql(services); break; + case DatabaseType.MsSql: + ConfigureMsSql(services); + break; + case DatabaseType.Postgres: + ConfigurePostgreSql(services); + break; + case DatabaseType.Sqlite: + ConfigureSqlite(services); + break; default: throw new ArgumentOutOfRangeException(nameof(databaseType), "Database type is not supported"); } } - private void ConfigureMySql(IServiceCollection services) + private void ConfigureMsSql(IServiceCollection services) { services .AddFluentMigratorCore() - .ConfigureRunner(c => c.AddMySql5() + .ConfigureRunner(c => c.AddSqlServer() .WithGlobalConnectionString(DbConnectionString()) - .ScanIn(typeof(MySqlInitialCreate).Assembly).For.Migrations() - ); - } - - private void ConfigureDapper(IServiceCollection services) - { - services.AddSingleton(new DbConnectionStringProvider(DbConnectionString())); - - ConfigureDapperByHost(GetDatabaseType(), services); - - DapperExtensions.DapperExtensions.SetMappingAssemblies(new[] { typeof(PersonMapper).Assembly }); - DapperAsyncExtensions.SetMappingAssemblies(new[] { typeof(PersonMapper).Assembly }); + .ScanIn(typeof(SqlInitialCreate).Assembly).For.Migrations() + ) + .AddSingleton(new MigrationConfiguration(){DbType = DatabaseType.MsSql.ToString()}); } - private static void ConfigureDapperByHost(DatabaseType databaseType, IServiceCollection services) + private void ConfigureMySql(IServiceCollection services) { - switch (databaseType) - { - case DatabaseType.Sqlite: - ConfigureDapperSqlite(services); - break; - case DatabaseType.MySql: - ConfigureDapperMySql(services); - break; - default: - throw new ArgumentOutOfRangeException(nameof(databaseType), "Database type is not supported"); - } + services + .AddFluentMigratorCore() + .ConfigureRunner(c => c.AddMySql5() + .WithGlobalConnectionString(DbConnectionString()) + .ScanIn(typeof(SqlInitialCreate).Assembly).For.Migrations() + ) + .AddSingleton(new MigrationConfiguration(){DbType = DatabaseType.MySql.ToString()}); } - private static void ConfigureDapperSqlite(IServiceCollection services) + private void ConfigurePostgreSql(IServiceCollection services) { - DapperExtensions.DapperExtensions.SqlDialect = new SqliteDialect(); - DapperAsyncExtensions.SqlDialect = new SqliteDialect(); - services.AddScoped(); + services + .AddFluentMigratorCore() + .ConfigureRunner(c => c.AddPostgres() + .ConfigureGlobalProcessorOptions(opt => opt.ProviderSwitches = "Force Quote=false") + .WithGlobalConnectionString(DbConnectionString()) + .ScanIn(typeof(SqlInitialCreate).Assembly).For.Migrations() + ) + .AddSingleton(new MigrationConfiguration(){DbType = DatabaseType.Postgres.ToString()}); } - private static void ConfigureDapperMySql(IServiceCollection services) + private void ConfigureSqlite(IServiceCollection services) { - DapperExtensions.DapperExtensions.SqlDialect = new MySqlDialect(); - DapperAsyncExtensions.SqlDialect = new MySqlDialect(); - services.AddScoped(); + services + .AddFluentMigratorCore() + .ConfigureRunner(c => c.AddSQLite() + .WithGlobalConnectionString(DbConnectionString()) + .ScanIn(typeof(SqlInitialCreate).Assembly).For.Migrations() + ) + .AddSingleton(new MigrationConfiguration(){DbType = DatabaseType.Sqlite.ToString()}); } - + private void ConfigureBrighter(IServiceCollection services) { - services.AddSingleton(new DbConnectionStringProvider(DbConnectionString())); + var messagingTransport = GetTransportType(); + + AddSchemaRegistryMaybe(services, messagingTransport); + + var outboxConfiguration = new RelationalDatabaseConfiguration( + DbConnectionString(), + binaryMessagePayload: messagingTransport == MessagingTransport.Kafka + ); + services.AddSingleton(outboxConfiguration); + (IAmAnOutbox outbox, Type connectionProvider, Type transactionProvider) makeOutbox = + OutboxExtensions.MakeOutbox(_env, GetDatabaseType(), outboxConfiguration, services); + services.AddBrighter(options => { //we want to use scoped, so make sure everything understands that which needs to @@ -169,30 +191,17 @@ private void ConfigureBrighter(IServiceCollection services) options.MapperLifetime = ServiceLifetime.Singleton; options.PolicyRegistry = new GreetingsPolicy(); }) - .UseExternalBus(new RmqProducerRegistryFactory( - new RmqMessagingGatewayConnection - { - AmpqUri = new AmqpUriSpecification(new Uri("amqp://guest:guest@localhost:5672")), - Exchange = new Exchange("paramore.brighter.exchange"), - }, - new RmqPublication[] - { - new RmqPublication - { - Topic = new RoutingKey("GreetingMade"), - MaxOutStandingMessages = 5, - MaxOutStandingCheckIntervalMilliSeconds = 500, - WaitForConfirmsTimeOutInMilliseconds = 1000, - MakeChannels = OnMissingChannel.Create - } - } - ).Create() - ) - //NOTE: The extension method AddOutbox is defined locally to the sample, to allow us to switch between outbox - //types easily. You may just choose to call the methods directly if you do not need to support multiple - //db types (which we just need to allow you to see how to configure your outbox type). - //It's also an example of how you can extend the DSL here easily if you have this kind of variability - .AddOutbox(_env, GetDatabaseType(), DbConnectionString(), _outBoxTableName) + .UseExternalBus((configure) => + { + configure.ProducerRegistry = ConfigureProducerRegistry(messagingTransport); + configure.Outbox = makeOutbox.outbox; + configure.TransactionProvider = makeOutbox.transactionProvider; + configure.ConnectionProvider = makeOutbox.connectionProvider; + }) + .UseOutboxSweeper(options => { + options.TimerInterval = 5; + options.MinimumMessageAge = 5000; + }) .AutoFromAssemblies(typeof(AddPersonHandlerAsync).Assembly); } @@ -208,6 +217,16 @@ private void ConfigureDarker(IServiceCollection services) .AddPolicies(new GreetingsPolicy()); } + private static IAmAProducerRegistry ConfigureProducerRegistry(MessagingTransport messagingTransport) + { + return messagingTransport switch + { + MessagingTransport.Rmq => GetRmqProducerRegistry(), + MessagingTransport.Kafka => GetKafkaProducerRegistry(), + _ => throw new ArgumentOutOfRangeException(nameof(messagingTransport), "Messaging transport is not supported") + }; + } + private string DbConnectionString() { //NOTE: Sqlite needs to use a shared cache to allow Db writes to the Outbox as well as entities @@ -216,13 +235,13 @@ private string DbConnectionString() private DatabaseType GetDatabaseType() { - return Configuration[DatabaseGlobals.DATABASE_TYPE_ENV] switch + return _configuration[DatabaseGlobals.DATABASE_TYPE_ENV] switch { DatabaseGlobals.MYSQL => DatabaseType.MySql, DatabaseGlobals.MSSQL => DatabaseType.MsSql, DatabaseGlobals.POSTGRESSQL => DatabaseType.Postgres, DatabaseGlobals.SQLITE => DatabaseType.Sqlite, - _ => throw new InvalidOperationException("Could not determine the database type") + _ => throw new ArgumentOutOfRangeException(nameof(DatabaseGlobals.DATABASE_TYPE_ENV), "Database type is not supported") }; } @@ -235,9 +254,69 @@ private string GetConnectionString(DatabaseType databaseType) { return databaseType switch { - DatabaseType.MySql => Configuration.GetConnectionString("GreetingsMySql"), - _ => throw new InvalidOperationException("Could not determine the database type") + DatabaseType.MySql => _configuration.GetConnectionString("GreetingsMySql"), + DatabaseType.MsSql => _configuration.GetConnectionString("GreetingsMsSql"), + DatabaseType.Postgres => _configuration.GetConnectionString("GreetingsPostgreSql"), + DatabaseType.Sqlite => GetDevDbConnectionString(), + _ => throw new ArgumentOutOfRangeException(nameof(databaseType), "Database type is not supported") }; } + + private static IAmAProducerRegistry GetKafkaProducerRegistry() + { + var producerRegistry = new KafkaProducerRegistryFactory( + new KafkaMessagingGatewayConfiguration + { + Name = "paramore.brighter.greetingsender", BootStrapServers = new[] { "localhost:9092" } + }, + new KafkaPublication[] + { + new KafkaPublication + { + Topic = new RoutingKey("GreetingMade"), + MessageSendMaxRetries = 3, + MessageTimeoutMs = 1000, + MaxInFlightRequestsPerConnection = 1, + MakeChannels = OnMissingChannel.Create + } + }) + .Create(); + + return producerRegistry; + } + + private static IAmAProducerRegistry GetRmqProducerRegistry() + { + var producerRegistry = new RmqProducerRegistryFactory( + new RmqMessagingGatewayConnection + { + AmpqUri = new AmqpUriSpecification(new Uri("amqp://guest:guest@localhost:5672")), + Exchange = new Exchange("paramore.brighter.exchange"), + }, + new RmqPublication[] + { + new RmqPublication + { + Topic = new RoutingKey("GreetingMade"), + MaxOutStandingMessages = 5, + MaxOutStandingCheckIntervalMilliSeconds = 500, + WaitForConfirmsTimeOutInMilliseconds = 1000, + MakeChannels = OnMissingChannel.Create + } + } + ).Create(); + return producerRegistry; + } + + private MessagingTransport GetTransportType() + { + return _configuration[MessagingGlobals.BRIGHTER_TRANSPORT] switch + { + MessagingGlobals.RMQ => MessagingTransport.Rmq, + MessagingGlobals.KAFKA => MessagingTransport.Kafka, + _ => throw new ArgumentOutOfRangeException(nameof(MessagingGlobals.BRIGHTER_TRANSPORT), + "Messaging transport is not supported") + }; + } } } diff --git a/samples/WebAPI_Dapper/GreetingsWeb/appsettings.Production.json b/samples/WebAPI_Dapper/GreetingsWeb/appsettings.Production.json index c02f13f346..43ad056184 100644 --- a/samples/WebAPI_Dapper/GreetingsWeb/appsettings.Production.json +++ b/samples/WebAPI_Dapper/GreetingsWeb/appsettings.Production.json @@ -7,7 +7,11 @@ } }, "ConnectionStrings": { - "GreetingsMySql": "server=localhost; port=3306; uid=root; pwd=root; database=Greetings", - "GreetingsMySqlDb": "server=localhost; port=3306; uid=root; pwd=root" + "GreetingsMySql": "server=localhost; port=3306; uid=root; pwd=root; database=Greetings; Allow User Variables=True", + "MySqlDb": "server=localhost; port=3306; uid=root; pwd=root", + "GreetingsPostgreSql": "Server=localhost; Port=5432; Database=greetings; Username=postgres; Password=password", + "PostgreSqlDb": "Server=localhost; Port=5432; Username=postgres; Password=password", + "GreetingsMsSql": "Server=localhost,11433;User Id=sa;Password=Password123!;Database=Greetings;TrustServerCertificate=true;Encrypt=false", + "MsSqlDb": "Server=localhost,11433;User Id=sa;Password=Password123!;TrustServerCertificate=true;Encrypt=false" } } \ No newline at end of file diff --git a/samples/WebAPI_Dapper/Greetings_MySqlMigrations/Greetings_MySqlMigrations.csproj b/samples/WebAPI_Dapper/Greetings_Migrations/Greetings_Migrations.csproj similarity index 83% rename from samples/WebAPI_Dapper/Greetings_MySqlMigrations/Greetings_MySqlMigrations.csproj rename to samples/WebAPI_Dapper/Greetings_Migrations/Greetings_Migrations.csproj index be526d4af0..d8b786d3e4 100644 --- a/samples/WebAPI_Dapper/Greetings_MySqlMigrations/Greetings_MySqlMigrations.csproj +++ b/samples/WebAPI_Dapper/Greetings_Migrations/Greetings_Migrations.csproj @@ -4,6 +4,7 @@ net6.0 enable disable + Greetings_MySqlMigrations diff --git a/samples/WebAPI_Dapper/Greetings_MySqlMigrations/Migrations/20220527_InitialCreate.cs b/samples/WebAPI_Dapper/Greetings_Migrations/Migrations/20220527_InitialCreate.cs similarity index 56% rename from samples/WebAPI_Dapper/Greetings_MySqlMigrations/Migrations/20220527_InitialCreate.cs rename to samples/WebAPI_Dapper/Greetings_Migrations/Migrations/20220527_InitialCreate.cs index 9fa274705a..2957c97da3 100644 --- a/samples/WebAPI_Dapper/Greetings_MySqlMigrations/Migrations/20220527_InitialCreate.cs +++ b/samples/WebAPI_Dapper/Greetings_Migrations/Migrations/20220527_InitialCreate.cs @@ -3,16 +3,25 @@ namespace Greetings_MySqlMigrations.Migrations; [Migration(1)] -public class MySqlInitialCreate : Migration +public class SqlInitialCreate : Migration { + private readonly IAmAMigrationConfiguration _configuration; + + public SqlInitialCreate(IAmAMigrationConfiguration configuration) + { + _configuration = configuration; + } + public override void Up() { - Create.Table("Person") + var timestampColumn = _configuration.DbType == "Postgres" ? "timeStamp" : "TimeStamp"; + + var person = Create.Table("Person") .WithColumn("Id").AsInt32().NotNullable().PrimaryKey().Identity() .WithColumn("Name").AsString().Unique() - .WithColumn("TimeStamp").AsDateTime().Nullable().WithDefault(SystemMethods.CurrentDateTime); - - Create.Table("Greeting") + .WithColumn(timestampColumn).AsDateTime().Nullable().WithDefault(SystemMethods.CurrentDateTime); + + var greeting = Create.Table("Greeting") .WithColumn("Id").AsInt32().NotNullable().PrimaryKey().Identity() .WithColumn("Message").AsString() .WithColumn("Recipient_Id").AsInt32(); diff --git a/samples/WebAPI_Dapper/Greetings_Migrations/Migrations/MigrationConfiguration.cs b/samples/WebAPI_Dapper/Greetings_Migrations/Migrations/MigrationConfiguration.cs new file mode 100644 index 0000000000..04c69cd421 --- /dev/null +++ b/samples/WebAPI_Dapper/Greetings_Migrations/Migrations/MigrationConfiguration.cs @@ -0,0 +1,11 @@ +namespace Greetings_MySqlMigrations.Migrations; + +public interface IAmAMigrationConfiguration +{ + string DbType { get; set; } +} + +public class MigrationConfiguration : IAmAMigrationConfiguration +{ + public string DbType { get; set; } +} diff --git a/samples/WebAPI_Dapper/Greetings_SqliteMigrations/Greetings_SqliteMigrations.csproj b/samples/WebAPI_Dapper/Greetings_SqliteMigrations/Greetings_SqliteMigrations.csproj deleted file mode 100644 index eb42023016..0000000000 --- a/samples/WebAPI_Dapper/Greetings_SqliteMigrations/Greetings_SqliteMigrations.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - - net6.0 - enable - disable - - - - - - - diff --git a/samples/WebAPI_Dapper/Greetings_SqliteMigrations/Migrations/202204221833_InitialCreate.cs b/samples/WebAPI_Dapper/Greetings_SqliteMigrations/Migrations/202204221833_InitialCreate.cs deleted file mode 100644 index 1b1edf2cbf..0000000000 --- a/samples/WebAPI_Dapper/Greetings_SqliteMigrations/Migrations/202204221833_InitialCreate.cs +++ /dev/null @@ -1,30 +0,0 @@ -using FluentMigrator; - -namespace Greetings_SqliteMigrations.Migrations; - -[Migration(1)] -public class SqlliteInitialCreate : Migration -{ - public override void Up() - { - Create.Table("Person") - .WithColumn("Id").AsInt32().NotNullable().PrimaryKey().Identity() - .WithColumn("Name").AsString().Unique() - .WithColumn("TimeStamp").AsBinary().WithDefault(SystemMethods.CurrentDateTime); - - Create.Table("Greeting") - .WithColumn("Id").AsInt32().NotNullable().PrimaryKey().Identity() - .WithColumn("Message").AsString() - .WithColumn("Recipient_Id").AsInt32(); - - Create.ForeignKey() - .FromTable("Greeting").ForeignColumn("Recipient_Id") - .ToTable("Person").PrimaryColumn("Id"); - } - - public override void Down() - { - Delete.Table("Greeting"); - Delete.Table("Person"); - } -} diff --git a/samples/WebAPI_Dapper/README.md b/samples/WebAPI_Dapper/README.md index b78be28dd2..de34cf2c7d 100644 --- a/samples/WebAPI_Dapper/README.md +++ b/samples/WebAPI_Dapper/README.md @@ -1,124 +1,104 @@ -# Table of content +# Table of contents - [Web API and Dapper Example](#web-api-and-dapper-example) * [Environments](#environments) * [Architecture](#architecture) - + [Outbox](#outbox) + [GreetingsAPI](#greetingsapi) + [SalutationAnalytics](#salutationanalytics) - * [Build and Deploy](#build-and-deploy) - + [Building](#building) - + [Deploy](#deploy) - + [Possible issues](#possible-issues) - - [Sqlite Database Read-Only Errors](#sqlite-database-read-only-errors) - - [Queue Creation and Dropped Messages](#queue-creation-and-dropped-messages) - - [Connection issue with the RabbitMQ](#connection-issue-with-the-rabbitmq) - - [Helpful documentation links](#helpful-documentation-links) - * [Tests](#tests) + * [Acceptance Tests](#tests) + * [Possible issues](#possible-issues) + +[Sqlite Database Read-Only Errors](#sqlite-database-read-only-errors) + + [RabbitMQ Queue Creation and Dropped Messages](#queue-creation-and-dropped-messages) + + [Helpful documentation links](#helpful-documentation-links) + # Web API and Dapper Example This sample shows a typical scenario when using WebAPI and Brighter/Darker. It demonstrates both using Brighter and Darker to implement the API endpoints, and using a work queue to handle asynchronous work that results from handling the API call. ## Environments -*Development* - runs locally on your machine, uses Sqlite as a data store; uses RabbitMQ for messaging, can be launched individually from the docker compose file; it represents a typical setup for development. +*Development* + +- Uses a local Sqlite instance for the data store. +- We support Docker hosted messaging brokers, either RabbitMQ or Kafka. + +*Production* +- We offer support for a range of common SQL stores (MySQL, PostgreSQL, SQL Server) using Docker. +- We support Docker hosted messaging brokers, either RabbitMQ or Kafka. + +### Configuration -*Production* - runs in Docker;uses RabbitMQ for messaging; it emulates a possible production environment. We offer support for a range of common SQL stores in this example. We determine which SQL store to use via an environment -variable. The process is: (1) determine we are running in a non-development environment (2) lookup the type of database we want to support (3) initialise an enum to identify that. +Configuration is via Environment variables. The following are supported: -We provide launchSetting.json files for all of these, which allows you to run Production with the appropriate db; you should launch your SQL data store and RabbitMQ from the docker compose file. +- BRIGHTER_GREETINGS_DATABASE => "Sqlite", "MySql", "Postgres", "MsSQL" +- BRIGHTER_TRANSPORT => "RabbitMQ", "Kafka" + +We provide launchSetting.json files for all of these, which allows you to run Production with the appropriate db; you should launch your SQL data store and broker from the docker compose file. In case you are using Command Line Interface for running the project, consider adding --launch-profile: ```sh dotnet run --launch-profile XXXXXX -d ``` + ## Architecture -### Outbox -Brighter does have an [Outbox pattern support](https://paramore.readthedocs.io/en/latest/OutboxPattern.html). In case you are new to it, consider reading it before diving deeper. + ### GreetingsAPI We follow a _ports and adapters_ architectural style, dividing the app into the following modules: * **GreetingsAdapters**: The adapters' module, handles the primary adapter of HTTP requests and responses to the app -* **GreetingsPorts**: the ports' module, handles requests from the primary adapter (HTTP) to the domain, and requests to secondary adapters. In a fuller app, the handlers for the primary adapter would correspond to our use case boundaries. The secondary port of the EntityGateway handles access to the DB via EF Core. We choose to treat EF Core as a port, not an adapter itself, here, as it wraps our underlying adapters for Sqlite or MySql. +* **GreetingsPorts**: the ports' module, handles requests from the primary adapter (HTTP) to the domain, and requests to secondary adapters. +In a fuller app, the handlers for the primary adapter would correspond to our use case boundaries. The secondary port uses either an IAmARelationalDbConnectionProvider or an IAmATransactionConnectionProvider. +Both of these are required for Brighter's Outbox. If you register the former with ServiceCollection, you can use it use it for your own queries; we use Dapper with that connection. +The latter is used by the Outbox to ensure that the message is sent within the same transaction as your writes to the entity and you should use its transaction support for transactional messaging. * **GreetingsEntities**: the domain model (or application in ports & adapters). In a fuller app, this would contain the logic that has a dependency on entity state. We 'depend on inwards' i.e. **GreetingsAdapters -> GreetingsPorts -> GreetingsEntities** -The assemblies migrations: **Greetings_MySqlMigrations** and **Greetings_SqliteMigrations** hold generated code to configure the Db. Consider this adapter layer code - the use of separate modules allows us to switch migration per environment. - -### SalutationAnalytics - -This listens for a GreetingMade message and stores it. It demonstrates listening to a queue. It also demonstrates the use of scopes provided by Brighter's ServiceActivator, which work with EFCore. These support writing to an Outbox when this component raises a message in turn. - -We don't listen to that message, and without any listeners the RabbitMQ will drop the message we send, as it has no queues to give it to. We don't listen because we would just be repeating what we have shown here. If you want to see the messages produced, use the RMQ Management Console (localhost:15672) to create a queue and then bind it to the paramore.binding.exchange with the routingkey of SalutationReceived. - -We also add an Inbox here. The Inbox can be used to de-duplicate messages. In messaging, the guarantee is 'at least once' if you use a technique such as an Outbox to ensure sending. This means we may receive a message twice. We either need, as in this case, to use an Inbox to de-duplicate, or we need to be idempotent such that receiving the message multiple times would result in the same outcome. - +The assemblies migrations: **Greetings_Migrations** hold code to configure the Db. -## Build and Deploy +GreetingsAPI uses an Outbox for Transactional Messaging - the write to the entity and the message store are within the same transaction and the message is posted from the message store. -### Building - -Use the build.sh file to: +### SalutationAnalytics -- Build both GreetingsAdapters and SalutationAnalytics and publish it to the /out directory. The Dockerfile assumes the app will be published here. -- Build the Docker image from the Dockerfile for each. +* **SalutationAnalytics** The adapter subscribes to GreetingMade messages. It demonstrates listening to a queue. It also demonstrates the use of scopes provided by Brighter's ServiceActivator, which work with Dapper. These support writing to an Outbox when this component raises a message in turn. + +* **SalutationPorts** The ports' module, handles requests from the primary adapter to the domain, and requests to secondary adapters. It writes to the entity store and sends another message. We don't listen to that message. Note that without any listeners RabbitMQ will drop the message we send, as it has no queues to give it to. +If you want to see the messages produced, use the RMQ Management Console (localhost:15672) or Kafka Console (localhost:9021). (You will need to create a subscribing queue in RabbitMQ) -(Why not use a multi-stage Docker build? We can't do this as the projects here reference projects not NuGet packages for Brighter libraries and there are not in the Docker build context.) +* **SalutationEntities** The domain model (or application in ports & adapters). In a fuller app, this would contain the logic that has a dependency on entity state. -A common error is to change something, forget to run build.sh and use an old Docker image. +We add an Inbox as well as the Outbox here. The Inbox can be used to de-duplicate messages. In messaging, the guarantee is 'at least once' if you use a technique such as an Outbox to ensure sending. This means we may receive a message twice. We either need, as in this case, to use an Inbox to de-duplicate, or we need to be idempotent such that receiving the message multiple times would result in the same outcome. -### Deploy +The assemblies migrations: **Salutations_Migrations** hold code to configure the Db. -We provide a docker compose file to allow you to run a 'Production' environment or to startup RabbitMQ for production: -```sh -docker compose up -d rabbitmq # will just start rabbitmq -``` +## Acceptance Tests -```sh -docker compose up -d mysql # will just start mysql -``` +We provide a tests.http file (supported by both JetBrains Rider and VS Code with the REST Client plugin) to allow you to test operations on the API. -and so on. +## Possible issues -### Possible issues #### Sqlite Database Read-Only Errors A Sqlite database will only have permissions for the process that created it. This can result in you receiving read-only errors between invocations of the sample. You either need to alter the permissions on your Db, or delete it between runs. Maintainers, please don't check the Sqlite files into source control. -#### Queue Creation and Dropped Messages +#### RabbitMQ Queue Creation and Dropped Messages -Queues are created by consumers. This is because publishers don't know who consumes them, and thus don't create their queues. This means that if you run a producer, such as GreetingsWeb, and use tests.http to push in greetings, although a message will be published to RabbitMQ, it won't have a queue to be delivered to and will be dropped, unless you have first run the SalutationAnalytics worker to create the queue. +For Rabbit MQ, queues are created by consumers. This is because publishers don't know who consumes them, and thus don't create their queues. This means that if you run a producer, such as GreetingsWeb, and use tests.http to push in greetings, although a message will be published to RabbitMQ, it won't have a queue to be delivered to and will be dropped, unless you have first run the SalutationAnalytics worker to create the queue. Generally, the rule of thumb is: start the consumer and *then* start the producer. -You can spot this by looking in the [RabbitMQ Management console](http://localhost:1567) and noting that no queue is bound to the routing key in the exchange. +You can spot this by looking in the [RabbitMQ Management console](http://localhost:15672) and noting that no queue is bound to the routing key in the exchange. You can use default credentials for the RabbitMQ Management console: ```sh user :guest passowrd: guest ``` -#### Connection issue with the RabbitMQ -When running RabbitMQ from the docker compose file (without any additional network setup, etc.) your RabbitMQ instance in docker will still be accessible by **localhost** as a host name. Consider this when running your application in the Production environment. -In Production, the application by default will have: -```sh -amqp://guest:guest@rabbitmq:5672 -``` - -as an Advanced Message Queuing Protocol (AMQP) connection string. -So one of the options will be replacing AMQP connection string with: -```sh -amqp://guest:guest@localhost:5672 -``` -In case you still struggle, consider following these steps: [RabbitMQ Troubleshooting Networking](https://www.rabbitmq.com/troubleshooting-networking.html) #### Helpful documentation links * [Brighter technical documentation](https://paramore.readthedocs.io/en/latest/index.html) * [Rabbit Message Queue (RMQ) documentation](https://www.rabbitmq.com/documentation.html) +* [Kafka documentation](https://kafka.apache.org/documentation/) -## Tests - -We provide a tests.http file (supported by both JetBrains Rider and VS Code with the REST Client plugin) to allow you to test operations. \ No newline at end of file diff --git a/samples/WebAPI_Dapper/SalutationAnalytics/Database/OutboxExtensions.cs b/samples/WebAPI_Dapper/SalutationAnalytics/Database/OutboxExtensions.cs new file mode 100644 index 0000000000..428c3bfda7 --- /dev/null +++ b/samples/WebAPI_Dapper/SalutationAnalytics/Database/OutboxExtensions.cs @@ -0,0 +1,67 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Paramore.Brighter; +using Paramore.Brighter.MsSql; +using Paramore.Brighter.MySql; +using Paramore.Brighter.Outbox.MsSql; +using Paramore.Brighter.Outbox.MySql; +using Paramore.Brighter.Outbox.PostgreSql; +using Paramore.Brighter.Outbox.Sqlite; +using Paramore.Brighter.PostgreSql; +using Paramore.Brighter.Sqlite; + +namespace SalutationAnalytics.Database +{ + + public class OutboxExtensions + { + public static (IAmAnOutbox, Type, Type) MakeOutbox( + HostBuilderContext hostContext, + DatabaseType databaseType, + RelationalDatabaseConfiguration configuration, + IServiceCollection services) + { + (IAmAnOutbox, Type, Type) outbox; + if (hostContext.HostingEnvironment.IsDevelopment()) + { + outbox = MakeSqliteOutBox(configuration); + } + else + { + outbox = databaseType switch + { + DatabaseType.MySql => MakeMySqlOutbox(configuration), + DatabaseType.MsSql => MakeMsSqlOutbox(configuration), + DatabaseType.Postgres => MakePostgresSqlOutbox(configuration, services), + DatabaseType.Sqlite => MakeSqliteOutBox(configuration), + _ => throw new InvalidOperationException("Unknown Db type for Outbox configuration") + }; + } + + return outbox; + } + + private static (IAmAnOutbox, Type, Type) MakePostgresSqlOutbox( + RelationalDatabaseConfiguration configuration, + IServiceCollection services) + { + return (new PostgreSqlOutbox(configuration), typeof(PostgreSqlConnectionProvider), typeof(PostgreSqlUnitOfWork)); + } + + private static (IAmAnOutbox, Type, Type) MakeMsSqlOutbox(RelationalDatabaseConfiguration configuration) + { + return new(new MsSqlOutbox(configuration), typeof(MsSqlConnectionProvider), typeof(MsSqlUnitOfWork)); + } + + private static (IAmAnOutbox, Type, Type) MakeMySqlOutbox(RelationalDatabaseConfiguration configuration) + { + return (new MySqlOutbox(configuration), typeof (MySqlConnectionProvider), typeof(MySqlUnitOfWork)); + } + + private static (IAmAnOutbox, Type, Type) MakeSqliteOutBox(RelationalDatabaseConfiguration configuration) + { + return (new SqliteOutbox(configuration), typeof(SqliteConnectionProvider), typeof(SqliteUnitOfWork)); + } + } +} diff --git a/samples/WebAPI_Dapper/SalutationAnalytics/Database/SchemaCreation.cs b/samples/WebAPI_Dapper/SalutationAnalytics/Database/SchemaCreation.cs index f727b948a3..9ddeeaf24e 100644 --- a/samples/WebAPI_Dapper/SalutationAnalytics/Database/SchemaCreation.cs +++ b/samples/WebAPI_Dapper/SalutationAnalytics/Database/SchemaCreation.cs @@ -1,15 +1,22 @@ using System; using System.Data; +using System.Data.Common; using FluentMigrator.Runner; +using Microsoft.Data.SqlClient; using Microsoft.Data.Sqlite; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using MySqlConnector; +using Npgsql; +using Paramore.Brighter.Inbox.MsSql; using Paramore.Brighter.Inbox.MySql; +using Paramore.Brighter.Inbox.Postgres; using Paramore.Brighter.Inbox.Sqlite; +using Paramore.Brighter.Outbox.MsSql; using Paramore.Brighter.Outbox.MySql; +using Paramore.Brighter.Outbox.PostgreSql; using Paramore.Brighter.Outbox.Sqlite; using Polly; @@ -27,13 +34,13 @@ public static IHost CheckDbIsUp(this IHost host) var services = scope.ServiceProvider; var env = services.GetService(); var config = services.GetService(); - string connectionString = DbServerConnectionString(config, env); + var (dbType, connectionString) = DbServerConnectionString(config, env); - //We don't check in development as using Sqlite + //We don't check db availability in development as we always use Sqlite which is a file not a server if (env.IsDevelopment()) return host; - WaitToConnect(connectionString); - CreateDatabaseIfNotExists(connectionString); + WaitToConnect(dbType, connectionString); + CreateDatabaseIfNotExists(dbType, GetDbConnection(dbType, connectionString)); return host; } @@ -51,6 +58,20 @@ public static IHost CreateInbox(this IHost host) return host; } + + public static IHost CreateOutbox(this IHost webHost, bool hasBinaryMessagePayload) + { + using (var scope = webHost.Services.CreateScope()) + { + var services = scope.ServiceProvider; + var env = services.GetService(); + var config = services.GetService(); + + CreateOutbox(config, env, hasBinaryMessagePayload); + } + + return webHost; + } public static IHost MigrateDatabase(this IHost host) { @@ -75,16 +96,39 @@ public static IHost MigrateDatabase(this IHost host) return host; } - private static void CreateDatabaseIfNotExists(string connectionString) + private static void CreateDatabaseIfNotExists(DatabaseType databaseType, DbConnection conn) { //The migration does not create the Db, so we need to create it sot that it will add it - using var conn = new MySqlConnection(connectionString); conn.Open(); using var command = conn.CreateCommand(); - command.CommandText = "CREATE DATABASE IF NOT EXISTS Salutations"; - command.ExecuteScalar(); - } + command.CommandText = databaseType switch + { + DatabaseType.Sqlite => "CREATE DATABASE IF NOT EXISTS Salutations", + DatabaseType.MySql => "CREATE DATABASE IF NOT EXISTS Salutations", + DatabaseType.Postgres => "CREATE DATABASE Salutations", + DatabaseType.MsSql => + "IF NOT EXISTS(SELECT * FROM sys.databases WHERE name = 'Salutations') CREATE DATABASE Salutations", + _ => throw new InvalidOperationException("Could not create instance of Salutations for unknown Db type") + }; + + try + { + command.ExecuteScalar(); + } + catch (NpgsqlException pe) + { + //Ignore if the Db already exists - we can't test for this in the SQL for Postgres + if (!pe.Message.Contains("already exists")) + throw; + } + catch (System.Exception e) + { + Console.WriteLine($"Issue with creating Greetings tables, {e.Message}"); + //Rethrow, if we can't create the Outbox, shut down + throw; + } + } private static void CreateInbox(IConfiguration config, IHostEnvironment env) { try @@ -94,9 +138,9 @@ private static void CreateInbox(IConfiguration config, IHostEnvironment env) if (env.IsDevelopment()) CreateInboxDevelopment(connectionString); else - CreateInboxProduction(connectionString); + CreateInboxProduction(GetDatabaseType(config), connectionString); } - catch (System.Exception e) + catch (Exception e) { Console.WriteLine($"Issue with creating Inbox table, {e.Message}"); throw; @@ -104,6 +148,32 @@ private static void CreateInbox(IConfiguration config, IHostEnvironment env) } private static void CreateInboxDevelopment(string connectionString) + { + CreateInboxSqlite(connectionString); + } + + private static void CreateInboxProduction(DatabaseType databaseType, string connectionString) + { + switch (databaseType) + { + case DatabaseType.MySql: + CreateInboxMySql(connectionString); + break; + case DatabaseType.MsSql: + CreateInboxMsSql(connectionString); + break; + case DatabaseType.Postgres: + CreateInboxPostgres(connectionString); + break; + case DatabaseType.Sqlite: + CreateInboxSqlite(connectionString); + break; + default: + throw new InvalidOperationException("Could not create instance of Outbox for unknown Db type"); + } + } + + private static void CreateInboxSqlite(string connectionString) { using var sqlConnection = new SqliteConnection(connectionString); sqlConnection.Open(); @@ -119,14 +189,15 @@ private static void CreateInboxDevelopment(string connectionString) command.ExecuteScalar(); } - private static void CreateInboxProduction(string connectionString) + private static void CreateInboxMySql(string connectionString) { using var sqlConnection = new MySqlConnection(connectionString); sqlConnection.Open(); using var existsQuery = sqlConnection.CreateCommand(); existsQuery.CommandText = MySqlInboxBuilder.GetExistsQuery(INBOX_TABLE_NAME); - bool exists = existsQuery.ExecuteScalar() != null; + var findInbox = existsQuery.ExecuteScalar(); + bool exists = findInbox is long and > 0; if (exists) return; @@ -134,63 +205,124 @@ private static void CreateInboxProduction(string connectionString) command.CommandText = MySqlInboxBuilder.GetDDL(INBOX_TABLE_NAME); command.ExecuteScalar(); } + + private static void CreateInboxMsSql(string connectionString) + { + using var sqlConnection = new SqlConnection(connectionString); + sqlConnection.Open(); + + using var existsQuery = sqlConnection.CreateCommand(); + existsQuery.CommandText = SqlInboxBuilder.GetExistsQuery(INBOX_TABLE_NAME); + var findInbox = existsQuery.ExecuteScalar(); + bool exists = findInbox is > 0; + + if (exists) return; - public static IHost CreateOutbox(this IHost webHost) + using var command = sqlConnection.CreateCommand(); + command.CommandText = SqlInboxBuilder.GetDDL(INBOX_TABLE_NAME); + command.ExecuteScalar(); + } + + private static void CreateInboxPostgres(string connectionString) { - using (var scope = webHost.Services.CreateScope()) - { - var services = scope.ServiceProvider; - var env = services.GetService(); - var config = services.GetService(); + using var sqlConnection = new NpgsqlConnection(connectionString); + sqlConnection.Open(); - CreateOutbox(config, env); - } + using var existsQuery = sqlConnection.CreateCommand(); + existsQuery.CommandText = PostgreSqlInboxBuilder.GetExistsQuery(INBOX_TABLE_NAME.ToLower()); + + var findInbox = existsQuery.ExecuteScalar(); + bool exists = findInbox is true; - return webHost; + if (exists) return; + + + using var command = sqlConnection.CreateCommand(); + command.CommandText = PostgreSqlInboxBuilder.GetDDL(INBOX_TABLE_NAME); + command.ExecuteScalar(); } - private static void CreateOutbox(IConfiguration config, IHostEnvironment env) + private static void CreateOutbox(IConfiguration config, IHostEnvironment env, bool hasBinaryMessagePayload) { try { var connectionString = DbConnectionString(config, env); if (env.IsDevelopment()) - CreateOutboxDevelopment(connectionString); + CreateOutboxDevelopment(connectionString, hasBinaryMessagePayload); else - CreateOutboxProduction(connectionString); + CreateOutboxProduction(GetDatabaseType(config), connectionString, hasBinaryMessagePayload); + } + catch (NpgsqlException pe) + { + //Ignore if the Db already exists - we can't test for this in the SQL for Postgres + if (!pe.Message.Contains("already exists")) + { + Console.WriteLine($"Issue with creating Outbox table, {pe.Message}"); + throw; + } } catch (System.Exception e) { Console.WriteLine($"Issue with creating Outbox table, {e.Message}"); + //Rethrow, if we can't create the Outbox, shut down throw; } } - private static void CreateOutboxDevelopment(string connectionString) + private static void CreateOutboxDevelopment(string connectionString, bool hasBinaryMessagePayload) { - using var sqlConnection = new SqliteConnection(connectionString); + CreateOutboxSqlite(connectionString, hasBinaryMessagePayload); + } + + private static void CreateOutboxProduction(DatabaseType databaseType, string connectionString, bool hasBinaryMessagePayload) + { + switch (databaseType) + { + case DatabaseType.MySql: + CreateOutboxMySql(connectionString, hasBinaryMessagePayload); + break; + case DatabaseType.MsSql: + CreateOutboxMsSql(connectionString, hasBinaryMessagePayload); + break; + case DatabaseType.Postgres: + CreateOutboxPostgres(connectionString, hasBinaryMessagePayload); + break; + case DatabaseType.Sqlite: + CreateOutboxSqlite(connectionString, hasBinaryMessagePayload); + break; + default: + throw new InvalidOperationException("Could not create instance of Outbox for unknown Db type"); + } + } + + private static void CreateOutboxMsSql(string connectionString, bool hasBinaryMessagePayload) + { + using var sqlConnection = new SqlConnection(connectionString); sqlConnection.Open(); - using var exists = sqlConnection.CreateCommand(); - exists.CommandText = SqliteOutboxBuilder.GetExistsQuery(OUTBOX_TABLE_NAME); - using var reader = exists.ExecuteReader(CommandBehavior.SingleRow); + using var existsQuery = sqlConnection.CreateCommand(); + existsQuery.CommandText = SqlOutboxBuilder.GetExistsQuery(OUTBOX_TABLE_NAME); + var findOutbox = existsQuery.ExecuteScalar(); + bool exists = findOutbox is > 0; - if (reader.HasRows) return; + if (exists) return; using var command = sqlConnection.CreateCommand(); - command.CommandText = SqliteOutboxBuilder.GetDDL(OUTBOX_TABLE_NAME); + command.CommandText = SqlOutboxBuilder.GetDDL(OUTBOX_TABLE_NAME); command.ExecuteScalar(); + } - private static void CreateOutboxProduction(string connectionString) + private static void CreateOutboxMySql(string connectionString, bool hasBinaryMessagePayload) { using var sqlConnection = new MySqlConnection(connectionString); sqlConnection.Open(); using var existsQuery = sqlConnection.CreateCommand(); existsQuery.CommandText = MySqlOutboxBuilder.GetExistsQuery(OUTBOX_TABLE_NAME); - bool exists = existsQuery.ExecuteScalar() != null; + var findOutbox = existsQuery.ExecuteScalar(); + bool exists = findOutbox is long and > 0; if (exists) return; @@ -198,16 +330,132 @@ private static void CreateOutboxProduction(string connectionString) command.CommandText = MySqlOutboxBuilder.GetDDL(OUTBOX_TABLE_NAME); command.ExecuteScalar(); } + + private static void CreateOutboxPostgres(string connectionString, bool hasBinaryMessagePayload) + { + using var sqlConnection = new NpgsqlConnection(connectionString); + sqlConnection.Open(); + + using var existsQuery = sqlConnection.CreateCommand(); + existsQuery.CommandText = PostgreSqlOutboxBulder.GetExistsQuery(OUTBOX_TABLE_NAME.ToLower()); + var findOutbox = existsQuery.ExecuteScalar(); + bool exists = findOutbox is true; + + if (exists) return; + + using var command = sqlConnection.CreateCommand(); + command.CommandText = PostgreSqlOutboxBulder.GetDDL(OUTBOX_TABLE_NAME); + command.ExecuteScalar(); + } + + private static void CreateOutboxSqlite(string connectionString, bool hasBinaryMessagePayload) + { + using var sqlConnection = new SqliteConnection(connectionString); + sqlConnection.Open(); + + using var exists = sqlConnection.CreateCommand(); + exists.CommandText = SqliteOutboxBuilder.GetExistsQuery(OUTBOX_TABLE_NAME); + using var reader = exists.ExecuteReader(CommandBehavior.SingleRow); + + if (reader.HasRows) return; + + using var command = sqlConnection.CreateCommand(); + command.CommandText = SqliteOutboxBuilder.GetDDL(OUTBOX_TABLE_NAME, hasBinaryMessagePayload); + command.ExecuteScalar(); + } private static string DbConnectionString(IConfiguration config, IHostEnvironment env) { //NOTE: Sqlite needs to use a shared cache to allow Db writes to the Outbox as well as entities - return env.IsDevelopment() ? "Filename=Salutations.db;Cache=Shared" : config.GetConnectionString("Salutations"); + return env.IsDevelopment() ? GetDevConnectionString() : GetProductionDbConnectionString(config, GetDatabaseType(config)); + } + + private static (DatabaseType, string) DbServerConnectionString(IConfiguration config, IHostEnvironment env) + { + var databaseType = GetDatabaseType(config); + var connectionString = env.IsDevelopment() ? GetDevConnectionString() : GetProductionConnectionString(config, databaseType); + return (databaseType, connectionString); } - private static string DbServerConnectionString(IConfiguration config, IHostEnvironment env) + private static string GetDevConnectionString() { - return env.IsDevelopment() ? "Filename=Salutations.db;Cache=Shared" : config.GetConnectionString("SalutationsDb"); + return "Filename=Salutations.db;Cache=Shared"; + } + + private static DbConnection GetDbConnection(DatabaseType databaseType, string connectionString) + { + return databaseType switch + { + DatabaseType.MySql => new MySqlConnection(connectionString), + DatabaseType.MsSql => new SqlConnection(connectionString), + DatabaseType.Postgres => new NpgsqlConnection(connectionString), + DatabaseType.Sqlite => new SqliteConnection(connectionString), + _ => throw new InvalidOperationException("Could not determine the database type") + }; + } + + private static string GetProductionConnectionString(IConfiguration config, DatabaseType databaseType) + { + return databaseType switch + { + DatabaseType.MySql => config.GetConnectionString("MySqlDb"), + DatabaseType.MsSql => config.GetConnectionString("MsSqlDb"), + DatabaseType.Postgres => config.GetConnectionString("PostgreSqlDb"), + DatabaseType.Sqlite => GetDevConnectionString(), + _ => throw new InvalidOperationException("Could not determine the database type") + }; + } + + private static string GetProductionDbConnectionString(IConfiguration config, DatabaseType databaseType) + { + return databaseType switch + { + DatabaseType.MySql => config.GetConnectionString("SalutationsMySql"), + DatabaseType.MsSql => config.GetConnectionString("SalutationsMsSql"), + DatabaseType.Postgres => config.GetConnectionString("SalutationsPostgreSql"), + DatabaseType.Sqlite => GetDevConnectionString(), + _ => throw new InvalidOperationException("Could not determine the database type") + }; + } + + private static DatabaseType GetDatabaseType(IConfiguration config) + { + return config[DatabaseGlobals.DATABASE_TYPE_ENV] switch + { + DatabaseGlobals.MYSQL => DatabaseType.MySql, + DatabaseGlobals.MSSQL => DatabaseType.MsSql, + DatabaseGlobals.POSTGRESSQL => DatabaseType.Postgres, + DatabaseGlobals.SQLITE => DatabaseType.Sqlite, + _ => throw new InvalidOperationException("Could not determine the database type") + }; + } + + private static void WaitToConnect(DatabaseType dbType, string connectionString) + { + var policy = Policy.Handle().WaitAndRetryForever( + retryAttempt => TimeSpan.FromSeconds(2), + (exception, timespan) => + { + Console.WriteLine($"Healthcheck: Waiting for the database {connectionString} to come online - {exception.Message}"); + }); + + policy.Execute(() => + { + using var conn = GetConnection(dbType, connectionString); + conn.Open(); + }); + } + + private static DbConnection GetConnection(DatabaseType databaseType, string connectionString) + { + return databaseType switch + { + DatabaseType.MySql => new MySqlConnection(connectionString), + DatabaseType.MsSql => new SqlConnection(connectionString), + DatabaseType.Postgres => new NpgsqlConnection(connectionString), + DatabaseType.Sqlite => new SqliteConnection(connectionString), + _ => throw new ArgumentOutOfRangeException(nameof(databaseType), databaseType, null) + }; } private static void WaitToConnect(string connectionString) diff --git a/samples/WebAPI_Dapper/SalutationAnalytics/Messaging/MessagingTransport.cs b/samples/WebAPI_Dapper/SalutationAnalytics/Messaging/MessagingTransport.cs new file mode 100644 index 0000000000..45f7af1bde --- /dev/null +++ b/samples/WebAPI_Dapper/SalutationAnalytics/Messaging/MessagingTransport.cs @@ -0,0 +1,21 @@ +namespace SalutationAnalytics.Messaging; + +public static class MessagingGlobals +{ + //environment string key + public const string BRIGHTER_TRANSPORT = "BRIGHTER_TRANSPORT"; + + public const string RMQ = "RabbitMQ"; + public const string KAFKA = "Kafka"; +} + + +/// +/// Which messaging transport are you using? +/// +public enum MessagingTransport +{ + Rmq, + Kafka +} + diff --git a/samples/WebAPI_Dapper/SalutationAnalytics/Program.cs b/samples/WebAPI_Dapper/SalutationAnalytics/Program.cs index a221b2604d..26d7119a90 100644 --- a/samples/WebAPI_Dapper/SalutationAnalytics/Program.cs +++ b/samples/WebAPI_Dapper/SalutationAnalytics/Program.cs @@ -1,27 +1,33 @@ using System; using System.IO; using System.Threading.Tasks; -using DapperExtensions; -using DapperExtensions.Sql; +using Confluent.Kafka; +using Confluent.SchemaRegistry; using FluentMigrator.Runner; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Paramore.Brighter; -using Paramore.Brighter.Dapper; using Paramore.Brighter.Extensions.DependencyInjection; using Paramore.Brighter.Inbox; +using Paramore.Brighter.Inbox.MsSql; using Paramore.Brighter.Inbox.MySql; +using Paramore.Brighter.Inbox.Postgres; using Paramore.Brighter.Inbox.Sqlite; +using Paramore.Brighter.MessagingGateway.Kafka; using Paramore.Brighter.MessagingGateway.RMQ; +using Paramore.Brighter.MsSql; +using Paramore.Brighter.MySql; +using Paramore.Brighter.PostgreSql; using Paramore.Brighter.ServiceActivator.Extensions.DependencyInjection; using Paramore.Brighter.ServiceActivator.Extensions.Hosting; +using Paramore.Brighter.Sqlite; using SalutationAnalytics.Database; -using SalutationPorts.EntityMappers; +using SalutationAnalytics.Messaging; using SalutationPorts.Policies; using SalutationPorts.Requests; -using Salutations_SqliteMigrations.Migrations; +using ChannelFactory = Paramore.Brighter.MessagingGateway.RMQ.ChannelFactory; namespace SalutationAnalytics { @@ -33,10 +39,19 @@ public static async Task Main(string[] args) host.CheckDbIsUp(); host.MigrateDatabase(); host.CreateInbox(); - host.CreateOutbox(); + host.CreateOutbox(HasBinaryMessagePayload()); await host.RunAsync(); } + private static void AddSchemaRegistryMaybe(IServiceCollection services, MessagingTransport messagingTransport) + { + if (messagingTransport != MessagingTransport.Kafka) return; + + var schemaRegistryConfig = new SchemaRegistryConfig { Url = "http://localhost:8081" }; + var cachedSchemaRegistryClient = new CachedSchemaRegistryClient(schemaRegistryConfig); + services.AddSingleton(cachedSchemaRegistryClient); + } + private static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureHostConfiguration(configurationBuilder => @@ -63,100 +78,137 @@ private static IHostBuilder CreateHostBuilder(string[] args) => private static void ConfigureBrighter(HostBuilderContext hostContext, IServiceCollection services) { - var subscriptions = new Subscription[] - { - new RmqSubscription( - new SubscriptionName("paramore.sample.salutationanalytics"), - new ChannelName("SalutationAnalytics"), - new RoutingKey("GreetingMade"), - runAsync: true, - timeoutInMilliseconds: 200, - isDurable: true, - makeChannels: OnMissingChannel.Create), //change to OnMissingChannel.Validate if you have infrastructure declared elsewhere - }; - - var host = hostContext.HostingEnvironment.IsDevelopment() ? "localhost" : "rabbitmq"; + var messagingTransport = GetTransportType(hostContext.Configuration[MessagingGlobals.BRIGHTER_TRANSPORT]); + + AddSchemaRegistryMaybe(services, messagingTransport); + + Subscription[] subscriptions = GetSubscriptions(messagingTransport); - var rmqConnection = new RmqMessagingGatewayConnection - { - AmpqUri = new AmqpUriSpecification(new Uri($"amqp://guest:guest@{host}:5672")), Exchange = new Exchange("paramore.brighter.exchange") - }; - - var rmqMessageConsumerFactory = new RmqMessageConsumerFactory(rmqConnection); + var relationalDatabaseConfiguration = new RelationalDatabaseConfiguration(DbConnectionString(hostContext)); + services.AddSingleton(relationalDatabaseConfiguration); + + var outboxConfiguration = new RelationalDatabaseConfiguration( + DbConnectionString(hostContext), + binaryMessagePayload: messagingTransport == MessagingTransport.Kafka + ); + services.AddSingleton(outboxConfiguration); + + (IAmAnOutbox outbox, Type connectionProvider, Type transactionProvider) makeOutbox = + OutboxExtensions.MakeOutbox(hostContext, GetDatabaseType(hostContext), outboxConfiguration, services); services.AddServiceActivator(options => { options.Subscriptions = subscriptions; - options.ChannelFactory = new ChannelFactory(rmqMessageConsumerFactory); + options.ChannelFactory = GetChannelFactory(messagingTransport); options.UseScoped = true; options.HandlerLifetime = ServiceLifetime.Scoped; options.MapperLifetime = ServiceLifetime.Singleton; options.CommandProcessorLifetime = ServiceLifetime.Scoped; options.PolicyRegistry = new SalutationPolicy(); + options.InboxConfiguration = new InboxConfiguration( + CreateInbox(hostContext, relationalDatabaseConfiguration), + scope: InboxScope.Commands, + onceOnly: true, + actionOnExists: OnceOnlyAction.Throw + + ); }) .ConfigureJsonSerialisation((options) => { //We don't strictly need this, but added as an example options.PropertyNameCaseInsensitive = true; }) - .UseExternalBus(new RmqProducerRegistryFactory( - rmqConnection, - new RmqPublication[] - { - new RmqPublication - { - Topic = new RoutingKey("SalutationReceived"), - MaxOutStandingMessages = 5, - MaxOutStandingCheckIntervalMilliSeconds = 500, - WaitForConfirmsTimeOutInMilliseconds = 1000, - MakeChannels = OnMissingChannel.Create - } - } - ).Create() - ) - .AutoFromAssemblies() - .UseExternalInbox( - ConfigureInbox(hostContext), - new InboxConfiguration( - scope: InboxScope.Commands, - onceOnly: true, - actionOnExists: OnceOnlyAction.Throw - ) - ); + .UseExternalBus((config) => + { + config.ProducerRegistry = ConfigureProducerRegistry(messagingTransport); + config.Outbox = makeOutbox.outbox; + config.ConnectionProvider = makeOutbox.connectionProvider; + config.TransactionProvider = makeOutbox.transactionProvider; + }) + .AutoFromAssemblies(); services.AddHostedService(); } - + private static void ConfigureMigration(HostBuilderContext hostBuilderContext, IServiceCollection services) { + //dev is always Sqlite if (hostBuilderContext.HostingEnvironment.IsDevelopment()) - { - services - .AddFluentMigratorCore() - .ConfigureRunner(c => - { - c.AddSQLite() - .WithGlobalConnectionString(DbConnectionString(hostBuilderContext)) - .ScanIn(typeof(Salutations_SqliteMigrations.Migrations.SqliteInitialCreate).Assembly).For.Migrations(); - }); - } + ConfigureSqlite(hostBuilderContext, services); else + ConfigureProductionDatabase(hostBuilderContext, GetDatabaseType(hostBuilderContext), services); + } + + private static void ConfigureProductionDatabase( + HostBuilderContext hostBuilderContext, + DatabaseType databaseType, + IServiceCollection services) + { + switch (databaseType) { - services - .AddFluentMigratorCore() - .ConfigureRunner(c => c.AddMySql5() - .WithGlobalConnectionString(DbConnectionString(hostBuilderContext)) - .ScanIn(typeof(Salutations_mySqlMigrations.Migrations.MySqlInitialCreate).Assembly).For.Migrations() - ); + case DatabaseType.MySql: + ConfigureMySql(hostBuilderContext, services); + break; + case DatabaseType.MsSql: + ConfigureMsSql(hostBuilderContext, services); + break; + case DatabaseType.Postgres: + ConfigurePostgreSql(hostBuilderContext, services); + break; + case DatabaseType.Sqlite: + ConfigureSqlite(hostBuilderContext, services); + break; + default: + throw new ArgumentOutOfRangeException(nameof(databaseType), "Database type is not supported"); } } + + private static void ConfigureMySql(HostBuilderContext hostBuilderContext, IServiceCollection services) + { + services + .AddFluentMigratorCore() + .ConfigureRunner(c => c.AddMySql5() + .WithGlobalConnectionString(DbConnectionString(hostBuilderContext)) + .ScanIn(typeof(Salutations_Migrations.Migrations.SqlInitialMigrations).Assembly).For.Migrations() + ); + } + + private static void ConfigureMsSql(HostBuilderContext hostBuilderContext, IServiceCollection services) + { + services + .AddFluentMigratorCore() + .ConfigureRunner(c => c.AddSqlServer() + .WithGlobalConnectionString(DbConnectionString(hostBuilderContext)) + .ScanIn(typeof(Salutations_Migrations.Migrations.SqlInitialMigrations).Assembly).For.Migrations() + ); + } + + private static void ConfigurePostgreSql(HostBuilderContext hostBuilderContext, IServiceCollection services) + { + services + .AddFluentMigratorCore() + .ConfigureRunner(c => c.AddPostgres() + .ConfigureGlobalProcessorOptions(opt => opt.ProviderSwitches = "Force Quote=false") + .WithGlobalConnectionString(DbConnectionString(hostBuilderContext)) + .ScanIn(typeof(Salutations_Migrations.Migrations.SqlInitialMigrations).Assembly).For.Migrations() + ); + } + + private static void ConfigureSqlite(HostBuilderContext hostBuilderContext, IServiceCollection services) + { + services + .AddFluentMigratorCore() + .ConfigureRunner(c => + { + c.AddSQLite() + .WithGlobalConnectionString(DbConnectionString(hostBuilderContext)) + .ScanIn(typeof(Salutations_Migrations.Migrations.SqlInitialMigrations).Assembly).For.Migrations(); + }); + } private static void ConfigureDapper(HostBuilderContext hostBuilderContext, IServiceCollection services) { - services.AddSingleton(new DbConnectionStringProvider(DbConnectionString(hostBuilderContext))); ConfigureDapperByHost(GetDatabaseType(hostBuilderContext), services); - DapperExtensions.DapperExtensions.SetMappingAssemblies(new[] { typeof(SalutationMapper).Assembly }); - DapperAsyncExtensions.SetMappingAssemblies(new[] { typeof(SalutationMapper).Assembly }); } private static void ConfigureDapperByHost(DatabaseType databaseType, IServiceCollection services) @@ -169,6 +221,12 @@ private static void ConfigureDapperByHost(DatabaseType databaseType, IServiceCol case DatabaseType.MySql: ConfigureDapperMySql(services); break; + case DatabaseType.MsSql: + ConfigureDapperMsSql(services); + break; + case DatabaseType.Postgres: + ConfigureDapperPostgreSql(services); + break; default: throw new ArgumentOutOfRangeException(nameof(databaseType), "Database type is not supported"); } @@ -176,41 +234,71 @@ private static void ConfigureDapperByHost(DatabaseType databaseType, IServiceCol private static void ConfigureDapperSqlite(IServiceCollection services) { - DapperExtensions.DapperExtensions.SqlDialect = new SqliteDialect(); - DapperAsyncExtensions.SqlDialect = new SqliteDialect(); - services.AddScoped(); + services.AddScoped(); + services.AddScoped(); } private static void ConfigureDapperMySql(IServiceCollection services) { - DapperExtensions.DapperExtensions.SqlDialect = new MySqlDialect(); - DapperAsyncExtensions.SqlDialect = new MySqlDialect(); - services.AddScoped(); + services.AddScoped(); + services.AddScoped(); } + private static void ConfigureDapperMsSql(IServiceCollection services) + { + services.AddScoped(); + services.AddScoped(); + } - private static IAmAnInbox ConfigureInbox(HostBuilderContext hostContext) + private static void ConfigureDapperPostgreSql(IServiceCollection services) + { + services.AddScoped(); + services.AddScoped(); + } + + private static IAmAnInbox CreateInbox(HostBuilderContext hostContext, IAmARelationalDatabaseConfiguration configuration) { if (hostContext.HostingEnvironment.IsDevelopment()) { - return new SqliteInbox(new SqliteInboxConfiguration(DbConnectionString(hostContext), SchemaCreation.INBOX_TABLE_NAME)); + return new SqliteInbox(configuration); } - return new MySqlInbox(new MySqlInboxConfiguration(DbConnectionString(hostContext), SchemaCreation.INBOX_TABLE_NAME)); + return CreateProductionInbox(GetDatabaseType(hostContext), configuration); + } + + private static IAmAProducerRegistry ConfigureProducerRegistry(MessagingTransport messagingTransport) + { + return messagingTransport switch + { + MessagingTransport.Rmq => GetRmqProducerRegistry(), + MessagingTransport.Kafka => GetKafkaProducerRegistry(), + _ => throw new ArgumentOutOfRangeException(nameof(messagingTransport), "Messaging transport is not supported") + }; + } + + private static IAmAnInbox CreateProductionInbox(DatabaseType databaseType, IAmARelationalDatabaseConfiguration configuration) + { + return databaseType switch + { + DatabaseType.Sqlite => new SqliteInbox(configuration), + DatabaseType.MySql => new MySqlInbox(configuration), + DatabaseType.MsSql => new MsSqlInbox(configuration), + DatabaseType.Postgres => new PostgreSqlInbox(configuration), + _ => throw new ArgumentOutOfRangeException(nameof(databaseType), "Database type is not supported") + }; } private static string DbConnectionString(HostBuilderContext hostContext) { //NOTE: Sqlite needs to use a shared cache to allow Db writes to the Outbox as well as entities return hostContext.HostingEnvironment.IsDevelopment() - ? "Filename=Salutations.db;Cache=Shared" - : hostContext.Configuration.GetConnectionString("Salutations"); + ? GetDevDbConnectionString() + :GetConnectionString(hostContext, GetDatabaseType(hostContext)); } - private static DatabaseType GetDatabaseType(HostBuilderContext hostContext) + private static DatabaseType GetDatabaseType(HostBuilderContext hostContext) { return hostContext.Configuration[DatabaseGlobals.DATABASE_TYPE_ENV] switch - { DatabaseGlobals.MYSQL => DatabaseType.MySql, DatabaseGlobals.MSSQL => DatabaseType.MsSql, @@ -219,12 +307,167 @@ private static DatabaseType GetDatabaseType(HostBuilderContext hostContext) _ => throw new InvalidOperationException("Could not determine the database type") }; } - + + private static IAmAChannelFactory GetChannelFactory(MessagingTransport messagingTransport) + { + return messagingTransport switch + { + MessagingTransport.Rmq => GetRmqChannelFactory(), + MessagingTransport.Kafka => GetKafkaChannelFactory(), + _ => throw new ArgumentOutOfRangeException(nameof(messagingTransport), "Messaging transport is not supported") + }; + } private static string GetEnvironment() { //NOTE: Hosting Context will always return Production outside of ASPNET_CORE at this point, so grab it directly return Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"); } + + private static string GetConnectionString(HostBuilderContext hostContext, DatabaseType databaseType) + { + return databaseType switch + { + DatabaseType.MySql => hostContext.Configuration.GetConnectionString("SalutationsMySql"), + DatabaseType.MsSql => hostContext.Configuration.GetConnectionString("SalutationsMsSql"), + DatabaseType.Postgres => hostContext.Configuration.GetConnectionString("SalutationsPostgreSql"), + DatabaseType.Sqlite => GetDevDbConnectionString(), + _ => throw new InvalidOperationException("Could not determine the database type") + }; + } + private static string GetDevDbConnectionString() + { + return "Filename=Salutations.db;Cache=Shared"; + } + + private static IAmAChannelFactory GetKafkaChannelFactory() + { + return new Paramore.Brighter.MessagingGateway.Kafka.ChannelFactory( + new KafkaMessageConsumerFactory( + new KafkaMessagingGatewayConfiguration + { + Name = "paramore.brighter", + BootStrapServers = new[] { "localhost:9092" } + } + ) + ); + } + + private static IAmAProducerRegistry GetKafkaProducerRegistry() + { + var producerRegistry = new KafkaProducerRegistryFactory( + new KafkaMessagingGatewayConfiguration + { + Name = "paramore.brighter.greetingsender", BootStrapServers = new[] { "localhost:9092" } + }, + new KafkaPublication[] + { + new KafkaPublication + { + Topic = new RoutingKey("SalutationReceived"), + MessageSendMaxRetries = 3, + MessageTimeoutMs = 1000, + MaxInFlightRequestsPerConnection = 1, + MakeChannels = OnMissingChannel.Create + } + }) + .Create(); + + return producerRegistry; + } + + private static IAmAChannelFactory GetRmqChannelFactory() + { + return new ChannelFactory(new RmqMessageConsumerFactory(new RmqMessagingGatewayConnection + { + AmpqUri = new AmqpUriSpecification(new Uri($"amqp://guest:guest@localhost:5672")), + Exchange = new Exchange("paramore.brighter.exchange") + }) + ); + } + + private static IAmAProducerRegistry GetRmqProducerRegistry() + { + var producerRegistry = new RmqProducerRegistryFactory( + new RmqMessagingGatewayConnection + { + AmpqUri = new AmqpUriSpecification(new Uri($"amqp://guest:guest@localhost:5672")), + Exchange = new Exchange("paramore.brighter.exchange") + }, + new RmqPublication[] + { + new RmqPublication + { + Topic = new RoutingKey("SalutationReceived"), + MaxOutStandingMessages = 5, + MaxOutStandingCheckIntervalMilliSeconds = 500, + WaitForConfirmsTimeOutInMilliseconds = 1000, + MakeChannels = OnMissingChannel.Create + } + } + ).Create(); + return producerRegistry; + } + + private static Subscription[] GetRmqSubscriptions() + { + var subscriptions = new Subscription[] + { + new RmqSubscription( + new SubscriptionName("paramore.sample.salutationanalytics"), + new ChannelName("SalutationAnalytics"), + new RoutingKey("GreetingMade"), + runAsync: false, + timeoutInMilliseconds: 200, + isDurable: true, + makeChannels: OnMissingChannel + .Create), //change to OnMissingChannel.Validate if you have infrastructure declared elsewhere + }; + return subscriptions; + } + + private static Subscription[] GetSubscriptions(MessagingTransport messagingTransport) + { + return messagingTransport switch + { + MessagingTransport.Rmq => GetRmqSubscriptions(), + MessagingTransport.Kafka => GetKafkaSubscriptions(), + _ => throw new ArgumentOutOfRangeException(nameof(messagingTransport), "Messaging transport is not supported") + }; + } + + private static Subscription[] GetKafkaSubscriptions() + { + var subscriptions = new KafkaSubscription[] + { + new KafkaSubscription( + new SubscriptionName("paramore.sample.salutationanalytics"), + channelName: new ChannelName("SalutationAnalytics"), + routingKey: new RoutingKey("GreetingMade"), + groupId: "kafka-GreetingsReceiverConsole-Sample", + timeoutInMilliseconds: 100, + offsetDefault: AutoOffsetReset.Earliest, + commitBatchSize: 5, + sweepUncommittedOffsetsIntervalMs: 10000, + makeChannels: OnMissingChannel.Create) + }; + return subscriptions; + } + + private static MessagingTransport GetTransportType(string brighterTransport) + { + return brighterTransport switch + { + MessagingGlobals.RMQ => MessagingTransport.Rmq, + MessagingGlobals.KAFKA => MessagingTransport.Kafka, + _ => throw new ArgumentOutOfRangeException(nameof(MessagingGlobals.BRIGHTER_TRANSPORT), + "Messaging transport is not supported") + }; + } + + private static bool HasBinaryMessagePayload() + { + return GetTransportType(Environment.GetEnvironmentVariable("BRIGHTER_TRANSPORT")) == MessagingTransport.Kafka; + } } } diff --git a/samples/WebAPI_Dapper/SalutationAnalytics/Properties/launchSettings.json b/samples/WebAPI_Dapper/SalutationAnalytics/Properties/launchSettings.json index 4154fbe3ff..639defffb1 100644 --- a/samples/WebAPI_Dapper/SalutationAnalytics/Properties/launchSettings.json +++ b/samples/WebAPI_Dapper/SalutationAnalytics/Properties/launchSettings.json @@ -5,15 +5,45 @@ "commandName": "Project", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", - "BRIGHTER_GREETINGS_DATABASE" : "Sqlite" + "BRIGHTER_GREETINGS_DATABASE": "Sqlite", + "BRIGHTER_TRANSPORT": "RabbitMQ" } }, - "Production": { - "commandName": "Project", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Production", - "BRIGHTER_GREETINGS_DATABASE" : "MySQL" - } + "ProductionMySql": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Production", + "BRIGHTER_GREETINGS_DATABASE": "MySQL", + "BRIGHTER_TRANSPORT": "RabbitMQ" + } + }, + "ProductionPostgres": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Production", + "BRIGHTER_GREETINGS_DATABASE": "PostgresSQL", + "BRIGHTER_TRANSPORT": "RabbitMQ" + } + }, + "ProductionMsSql": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Production", + "BRIGHTER_GREETINGS_DATABASE": "MsSQL", + "BRIGHTER_TRANSPORT": "RabbitMQ" + } } } } diff --git a/samples/WebAPI_Dapper/SalutationAnalytics/SalutationAnalytics.csproj b/samples/WebAPI_Dapper/SalutationAnalytics/SalutationAnalytics.csproj index ee5dff6bb8..03427236da 100644 --- a/samples/WebAPI_Dapper/SalutationAnalytics/SalutationAnalytics.csproj +++ b/samples/WebAPI_Dapper/SalutationAnalytics/SalutationAnalytics.csproj @@ -6,20 +6,22 @@ + + + - + + - - - + @@ -41,4 +43,10 @@ + + + ..\..\..\libs\Npgsql\net6.0\Npgsql.dll + + + diff --git a/samples/WebAPI_Dapper/SalutationAnalytics/appsettings.Production.json b/samples/WebAPI_Dapper/SalutationAnalytics/appsettings.Production.json index dbbfec462a..546be3c89c 100644 --- a/samples/WebAPI_Dapper/SalutationAnalytics/appsettings.Production.json +++ b/samples/WebAPI_Dapper/SalutationAnalytics/appsettings.Production.json @@ -7,7 +7,11 @@ } }, "ConnectionStrings": { - "Salutations": "server=localhost; port=3306; uid=root; pwd=root; database=Salutations", - "SalutationsDb": "server=localhost; port=3306; uid=root; pwd=root" + "SalutationsMySql": "server=localhost; port=3306; uid=root; pwd=root; database=Salutations", + "MySqlDb": "server=localhost; port=3306; uid=root; pwd=root", + "SalutationsPostgreSql": "Server=localhost; Port=5432; Database=salutations; Username=postgres; Password=password", + "PostgreSqlDb": "Server=localhost; Port=5432; Username=postgres; Password=password", + "SalutationsMsSql": "Server=localhost,11433;User Id=sa;Password=Password123!;Database=Salutations;TrustServerCertificate=true;Encrypt=false", + "MsSqlDb": "Server=localhost,11433;User Id=sa;Password=Password123!;TrustServerCertificate=true;Encrypt=false" } } \ No newline at end of file diff --git a/samples/WebAPI_Dapper/SalutationEntities/Salutation.cs b/samples/WebAPI_Dapper/SalutationEntities/Salutation.cs index b3c44a995b..7a4c2c2408 100644 --- a/samples/WebAPI_Dapper/SalutationEntities/Salutation.cs +++ b/samples/WebAPI_Dapper/SalutationEntities/Salutation.cs @@ -1,9 +1,11 @@ -namespace SalutationEntities +using System; + +namespace SalutationEntities { public class Salutation { public long Id { get; set; } - public byte[] TimeStamp { get; set; } + public DateTime TimeStamp { get; set; } public string Greeting { get; set; } public Salutation() { /* ORM needs to create */ } diff --git a/samples/WebAPI_Dapper/SalutationPorts/EntityMappers/SalutationMapper.cs b/samples/WebAPI_Dapper/SalutationPorts/EntityMappers/SalutationMapper.cs deleted file mode 100644 index aacce5f63e..0000000000 --- a/samples/WebAPI_Dapper/SalutationPorts/EntityMappers/SalutationMapper.cs +++ /dev/null @@ -1,15 +0,0 @@ -using DapperExtensions.Mapper; -using SalutationEntities; - -namespace SalutationPorts.EntityMappers; - -public class SalutationMapper : ClassMapper -{ - public SalutationMapper() - { - TableName = nameof(Salutation); - Map(s => s.Id).Column("Id").Key(KeyType.Identity); - Map(s => s.Greeting).Column("Greeting"); - Map(s => s.TimeStamp).Column("TimeStamp").Ignore(); - } -} diff --git a/samples/WebAPI_Dapper/SalutationPorts/Handlers/GreetingMadeHandler.cs b/samples/WebAPI_Dapper/SalutationPorts/Handlers/GreetingMadeHandler.cs index 4d9894b23d..0fca6ec34d 100644 --- a/samples/WebAPI_Dapper/SalutationPorts/Handlers/GreetingMadeHandler.cs +++ b/samples/WebAPI_Dapper/SalutationPorts/Handlers/GreetingMadeHandler.cs @@ -1,11 +1,10 @@ using System; using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using DapperExtensions; +using System.Data.Common; +using Dapper; using Microsoft.Extensions.Logging; using Paramore.Brighter; -using Paramore.Brighter.Dapper; +using Paramore.Brighter.Inbox.Attributes; using Paramore.Brighter.Logging.Attributes; using Paramore.Brighter.Policies.Attributes; using SalutationEntities; @@ -13,50 +12,56 @@ namespace SalutationPorts.Handlers { - public class GreetingMadeHandlerAsync : RequestHandlerAsync + public class GreetingMadeHandler : RequestHandler { - private readonly IUnitOfWork _uow; + private readonly IAmABoxTransactionProvider _transactionConnectionProvider; private readonly IAmACommandProcessor _postBox; - private readonly ILogger _logger; + private readonly ILogger _logger; - public GreetingMadeHandlerAsync(IUnitOfWork uow, IAmACommandProcessor postBox, ILogger logger) + public GreetingMadeHandler(IAmABoxTransactionProvider transactionConnectionProvider, IAmACommandProcessor postBox, ILogger logger) { - _uow = uow; + _transactionConnectionProvider = transactionConnectionProvider; _postBox = postBox; _logger = logger; } - //[UseInboxAsync(step:0, contextKey: typeof(GreetingMadeHandlerAsync), onceOnly: true )] -- we are using a global inbox, so need to be explicit!! - [RequestLoggingAsync(step: 1, timing: HandlerTiming.Before)] - [UsePolicyAsync(step:2, policy: Policies.Retry.EXPONENTIAL_RETRYPOLICYASYNC)] - public override async Task HandleAsync(GreetingMade @event, CancellationToken cancellationToken = default) + [UseInbox(step:0, contextKey: typeof(GreetingMadeHandler), onceOnly: true )] + [RequestLogging(step: 1, timing: HandlerTiming.Before)] + [UsePolicy(step:2, policy: Policies.Retry.EXPONENTIAL_RETRYPOLICY)] + public override GreetingMade Handle(GreetingMade @event) { var posts = new List(); - var tx = await _uow.BeginOrGetTransactionAsync(cancellationToken); + var tx = _transactionConnectionProvider.GetTransaction(); + var conn = tx.Connection; try { var salutation = new Salutation(@event.Greeting); - await _uow.Database.InsertAsync(salutation, tx); + conn.Execute( + "insert into Salutation (greeting) values (@greeting)", + new {greeting = salutation.Greeting}, + tx); - posts.Add(await _postBox.DepositPostAsync(new SalutationReceived(DateTimeOffset.Now), cancellationToken: cancellationToken)); + posts.Add(_postBox.DepositPost( + new SalutationReceived(DateTimeOffset.Now), + _transactionConnectionProvider)); - await tx.CommitAsync(cancellationToken); + _transactionConnectionProvider.Commit(); } catch (Exception e) { _logger.LogError(e, "Could not save salutation"); //if it went wrong rollback entity write and Outbox write - await tx.RollbackAsync(cancellationToken); + _transactionConnectionProvider.Rollback(); - return await base.HandleAsync(@event, cancellationToken); + return base.Handle(@event); } - await _postBox.ClearOutboxAsync(posts, cancellationToken: cancellationToken); + _postBox.ClearOutbox(posts.ToArray()); - return await base.HandleAsync(@event, cancellationToken); + return base.Handle(@event); } } } diff --git a/samples/WebAPI_Dapper/SalutationPorts/Policies/Retry.cs b/samples/WebAPI_Dapper/SalutationPorts/Policies/Retry.cs index 4db47aa42d..58013a5a3f 100644 --- a/samples/WebAPI_Dapper/SalutationPorts/Policies/Retry.cs +++ b/samples/WebAPI_Dapper/SalutationPorts/Policies/Retry.cs @@ -7,21 +7,21 @@ namespace SalutationPorts.Policies { public static class Retry { - public const string RETRYPOLICYASYNC = "SalutationPorts.Policies.RetryPolicyAsync"; - public const string EXPONENTIAL_RETRYPOLICYASYNC = "SalutationPorts.Policies.ExponenttialRetryPolicyAsync"; + public const string RETRYPOLICY = "SalutationPorts.Policies.RetryPolicy"; + public const string EXPONENTIAL_RETRYPOLICY = "SalutationPorts.Policies.ExponenttialRetryPolicy"; - public static AsyncRetryPolicy GetSimpleHandlerRetryPolicy() + public static RetryPolicy GetSimpleHandlerRetryPolicy() { - return Policy.Handle().WaitAndRetryAsync(new[] + return Policy.Handle().WaitAndRetry(new[] { TimeSpan.FromMilliseconds(50), TimeSpan.FromMilliseconds(100), TimeSpan.FromMilliseconds(150) }); } - public static AsyncRetryPolicy GetExponentialHandlerRetryPolicy() + public static RetryPolicy GetExponentialHandlerRetryPolicy() { var delay = Backoff.ExponentialBackoff(TimeSpan.FromMilliseconds(100), retryCount: 5, fastFirst: true); - return Policy.Handle().WaitAndRetryAsync(delay); + return Policy.Handle().WaitAndRetry(delay); } } } diff --git a/samples/WebAPI_Dapper/SalutationPorts/Policies/SalutationPolicy.cs b/samples/WebAPI_Dapper/SalutationPorts/Policies/SalutationPolicy.cs index ddf21c324f..28024f22a7 100644 --- a/samples/WebAPI_Dapper/SalutationPorts/Policies/SalutationPolicy.cs +++ b/samples/WebAPI_Dapper/SalutationPorts/Policies/SalutationPolicy.cs @@ -11,8 +11,8 @@ public SalutationPolicy() private void AddSalutationPolicies() { - Add(Retry.RETRYPOLICYASYNC, Retry.GetSimpleHandlerRetryPolicy()); - Add(Retry.EXPONENTIAL_RETRYPOLICYASYNC, Retry.GetExponentialHandlerRetryPolicy()); + Add(Retry.RETRYPOLICY, Retry.GetSimpleHandlerRetryPolicy()); + Add(Retry.EXPONENTIAL_RETRYPOLICY, Retry.GetExponentialHandlerRetryPolicy()); } } } diff --git a/samples/WebAPI_Dapper/SalutationPorts/SalutationPorts.csproj b/samples/WebAPI_Dapper/SalutationPorts/SalutationPorts.csproj index c513055246..68859628e9 100644 --- a/samples/WebAPI_Dapper/SalutationPorts/SalutationPorts.csproj +++ b/samples/WebAPI_Dapper/SalutationPorts/SalutationPorts.csproj @@ -4,11 +4,11 @@ net6.0 - - - - - + + + + + diff --git a/samples/WebAPI_Dapper/Salutations_SqliteMigrations/Migrations/202205161812_SqliteMigrations.cs b/samples/WebAPI_Dapper/Salutations_Migrations/Migrations/202205161812_SqlInitialMigrations.cs similarity index 62% rename from samples/WebAPI_Dapper/Salutations_SqliteMigrations/Migrations/202205161812_SqliteMigrations.cs rename to samples/WebAPI_Dapper/Salutations_Migrations/Migrations/202205161812_SqlInitialMigrations.cs index 0afb14f529..a0f91cec2e 100644 --- a/samples/WebAPI_Dapper/Salutations_SqliteMigrations/Migrations/202205161812_SqliteMigrations.cs +++ b/samples/WebAPI_Dapper/Salutations_Migrations/Migrations/202205161812_SqlInitialMigrations.cs @@ -1,16 +1,16 @@ using FluentMigrator; -namespace Salutations_SqliteMigrations.Migrations; +namespace Salutations_Migrations.Migrations; [Migration(1)] -public class SqliteInitialCreate : Migration +public class SqlInitialMigrations : Migration { public override void Up() { Create.Table("Salutation") .WithColumn("Id").AsInt32().NotNullable().PrimaryKey().Identity() .WithColumn("Greeting").AsString() - .WithColumn("TimeStamp").AsBinary().WithDefault(SystemMethods.CurrentDateTime); + .WithColumn("TimeStamp").AsDateTime().Nullable().WithDefault(SystemMethods.CurrentDateTime); } public override void Down() diff --git a/samples/WebAPI_Dapper/Salutations_SqliteMigrations/Salutations_SqliteMigrations.csproj b/samples/WebAPI_Dapper/Salutations_Migrations/Salutations_Migrations.csproj similarity index 100% rename from samples/WebAPI_Dapper/Salutations_SqliteMigrations/Salutations_SqliteMigrations.csproj rename to samples/WebAPI_Dapper/Salutations_Migrations/Salutations_Migrations.csproj diff --git a/samples/WebAPI_Dapper/Salutations_mySqlMigrations/Migrations/20220527_MySqlMigrations.cs b/samples/WebAPI_Dapper/Salutations_mySqlMigrations/Migrations/20220527_MySqlMigrations.cs deleted file mode 100644 index d1f4e4aec2..0000000000 --- a/samples/WebAPI_Dapper/Salutations_mySqlMigrations/Migrations/20220527_MySqlMigrations.cs +++ /dev/null @@ -1,20 +0,0 @@ -using FluentMigrator; - -namespace Salutations_mySqlMigrations.Migrations; - -[Migration(1)] -public class MySqlInitialCreate : Migration -{ - public override void Up() - { - Create.Table("Salutation") - .WithColumn("Id").AsInt32().NotNullable().PrimaryKey().Identity() - .WithColumn("Greeting").AsString() - .WithColumn("TimeStamp").AsBinary().WithDefault(SystemMethods.CurrentDateTime); - } - - public override void Down() - { - Delete.Table("Salutation"); - } -} diff --git a/samples/WebAPI_Dapper/Salutations_mySqlMigrations/Salutations_mySqlMigrations.csproj b/samples/WebAPI_Dapper/Salutations_mySqlMigrations/Salutations_mySqlMigrations.csproj deleted file mode 100644 index 9107f11c61..0000000000 --- a/samples/WebAPI_Dapper/Salutations_mySqlMigrations/Salutations_mySqlMigrations.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - - net6.0 - enable - enable - - - - - - - diff --git a/samples/WebAPI_Dapper/build.sh b/samples/WebAPI_Dapper/build.sh deleted file mode 100644 index 052b5d5dc6..0000000000 --- a/samples/WebAPI_Dapper/build.sh +++ /dev/null @@ -1,15 +0,0 @@ -pushd GreetingsWeb || exit -rm -rf out -dotnet restore -dotnet build -dotnet publish -c Release -o out -docker build . -popd || exit -pushd SalutationAnalytics || exit -rm -rf out -dotnet restore -dotnet build -dotnet publish -c Release -o out -docker build . -popd || exit - diff --git a/samples/WebAPI_Dapper/docker-compose.yml b/samples/WebAPI_Dapper/docker-compose.yml deleted file mode 100644 index 703b1a2372..0000000000 --- a/samples/WebAPI_Dapper/docker-compose.yml +++ /dev/null @@ -1,51 +0,0 @@ -version: '3.1' -services: - web: - build: ./GreetingsAdapters - hostname: greetingsapi - ports: - - "5000:5000" - environment: - - BRIGHTER_ConnectionStrings__Greetings=server=greetings_db; port=3306; uid=root; pwd=root; database=Greetings - - BRIGHTER_ConnectionStrings__GreetingsDb=server=greetings_db; port=3306; uid=root; pwd=root - - ASPNETCORE_ENVIRONMENT=Production - links: - - mysql:greetings_db - depends_on: - - mysql - - rabbitmq - worker: - build: ./GreetingsWatcher - hostname: greetingsworker - environment: - - ASPNETCORE_ENVIRONMENT=Production - depends_on: - - rabbitmq - mysql: - hostname: greetings_db - image: mysql - ports: - - "3306:3306" - security_opt: - - seccomp:unconfined - volumes: - - my-db:/var/lib/mysql - environment: - MYSQL_ROOT_PASSWORD: "root" - healthcheck: - test: mysqladmin ping -h localhost -p$$MYSQL_ROOT_PASSWORD && test '0' -eq $$(ps aux | awk '{print $$11}' | grep -c -e '^mysql$$') - rabbitmq: - image: brightercommand/rabbitmq:3.8-management-delay - ports: - - "5672:5672" - - "15672:15672" - volumes: - - rabbitmq-home:/var/lib/rabbitmq - -volumes: - rabbitmq-home: - driver: local - my-db: - driver: local - - diff --git a/samples/WebAPI_Dynamo/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs b/samples/WebAPI_Dynamo/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs index 00b1511547..38ed398c55 100644 --- a/samples/WebAPI_Dynamo/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs +++ b/samples/WebAPI_Dynamo/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs @@ -16,14 +16,14 @@ namespace GreetingsPorts.Handlers { public class AddGreetingHandlerAsync: RequestHandlerAsync { - private readonly DynamoDbUnitOfWork _unitOfWork; + private readonly IAmADynamoDbTransactionProvider _transactionProvider; private readonly IAmACommandProcessor _postBox; private readonly ILogger _logger; - public AddGreetingHandlerAsync(IAmABoxTransactionConnectionProvider uow, IAmACommandProcessor postBox, ILogger logger) + public AddGreetingHandlerAsync(IAmADynamoDbTransactionProvider transactionProvider, IAmACommandProcessor postBox, ILogger logger) { - _unitOfWork = (DynamoDbUnitOfWork)uow; + _transactionProvider = transactionProvider; _postBox = postBox; _logger = logger; } @@ -36,34 +36,44 @@ public override async Task HandleAsync(AddGreeting addGreeting, Can //We use the unit of work to grab connection and transaction, because Outbox needs //to share them 'behind the scenes' - var context = new DynamoDBContext(_unitOfWork.DynamoDb); - var transaction = _unitOfWork.BeginOrGetTransaction(); + var context = new DynamoDBContext(_transactionProvider.DynamoDb); + var transaction = await _transactionProvider.GetTransactionAsync(cancellationToken); try { var person = await context.LoadAsync(addGreeting.Name, cancellationToken); - + person.Greetings.Add(addGreeting.Greeting); var document = context.ToDocument(person); var attributeValues = document.ToAttributeMap(); - - //write the added child entity to the Db - just replace the whole entity as we grabbed the original - //in production code, an update expression would be faster - transaction.TransactItems.Add(new TransactWriteItem{Put = new Put{TableName = "People", Item = attributeValues}}); + + //write the added child entity to the Db - just replace the whole entity as we grabbed the original + //in production code, an update expression would be faster + transaction.TransactItems.Add(new TransactWriteItem + { + Put = new Put { TableName = "People", Item = attributeValues } + }); //Now write the message we want to send to the Db in the same transaction. - posts.Add(await _postBox.DepositPostAsync(new GreetingMade(addGreeting.Greeting), cancellationToken: cancellationToken)); - + posts.Add(await _postBox.DepositPostAsync( + new GreetingMade(addGreeting.Greeting), + _transactionProvider, + cancellationToken: cancellationToken)); + //commit both new greeting and outgoing message - await _unitOfWork.CommitAsync(cancellationToken); + await _transactionProvider.CommitAsync(cancellationToken); } catch (Exception e) - { + { _logger.LogError(e, "Exception thrown handling Add Greeting request"); //it went wrong, rollback the entity change and the downstream message - _unitOfWork.Rollback(); + await _transactionProvider.RollbackAsync(cancellationToken); return await base.HandleAsync(addGreeting, cancellationToken); } + finally + { + _transactionProvider.Close(); + } //Send this message via a transport. We need the ids to send just the messages here, not all outstanding ones. //Alternatively, you can let the Sweeper do this, but at the cost of increased latency diff --git a/samples/WebAPI_Dynamo/GreetingsPorts/Handlers/AddPersonHandlerAsync.cs b/samples/WebAPI_Dynamo/GreetingsPorts/Handlers/AddPersonHandlerAsync.cs index fdced54ece..90df9aa8ad 100644 --- a/samples/WebAPI_Dynamo/GreetingsPorts/Handlers/AddPersonHandlerAsync.cs +++ b/samples/WebAPI_Dynamo/GreetingsPorts/Handlers/AddPersonHandlerAsync.cs @@ -1,6 +1,7 @@ using System.Threading; using System.Threading.Tasks; using Amazon.DynamoDBv2.DataModel; +using Amazon.DynamoDBv2.Model; using GreetingsEntities; using GreetingsPorts.Requests; using Paramore.Brighter; @@ -12,18 +13,18 @@ namespace GreetingsPorts.Handlers { public class AddPersonHandlerAsync : RequestHandlerAsync { - private readonly DynamoDbUnitOfWork _dynamoDbUnitOfWork; + private readonly IAmADynamoDbConnectionProvider _dynamoDbConnectionProvider; - public AddPersonHandlerAsync(IAmABoxTransactionConnectionProvider dynamoDbUnitOfWork) + public AddPersonHandlerAsync(IAmADynamoDbConnectionProvider dynamoDbConnectionProvider) { - _dynamoDbUnitOfWork = (DynamoDbUnitOfWork )dynamoDbUnitOfWork; + _dynamoDbConnectionProvider = dynamoDbConnectionProvider; } [RequestLoggingAsync(0, HandlerTiming.Before)] [UsePolicyAsync(step:1, policy: Policies.Retry.EXPONENTIAL_RETRYPOLICYASYNC)] public override async Task HandleAsync(AddPerson addPerson, CancellationToken cancellationToken = default) { - var context = new DynamoDBContext(_dynamoDbUnitOfWork.DynamoDb); + var context = new DynamoDBContext(_dynamoDbConnectionProvider.DynamoDb); await context.SaveAsync(new Person { Name = addPerson.Name }, cancellationToken); return await base.HandleAsync(addPerson, cancellationToken); diff --git a/samples/WebAPI_Dynamo/GreetingsPorts/Handlers/DeletePersonHandlerAsync.cs b/samples/WebAPI_Dynamo/GreetingsPorts/Handlers/DeletePersonHandlerAsync.cs index bc63f0be3e..af1fdbecc2 100644 --- a/samples/WebAPI_Dynamo/GreetingsPorts/Handlers/DeletePersonHandlerAsync.cs +++ b/samples/WebAPI_Dynamo/GreetingsPorts/Handlers/DeletePersonHandlerAsync.cs @@ -1,6 +1,7 @@ using System.Threading; using System.Threading.Tasks; using Amazon.DynamoDBv2.DataModel; +using Amazon.DynamoDBv2.Model; using GreetingsPorts.Requests; using Paramore.Brighter; using Paramore.Brighter.DynamoDb; @@ -11,18 +12,18 @@ namespace GreetingsPorts.Handlers { public class DeletePersonHandlerAsync : RequestHandlerAsync { - private readonly DynamoDbUnitOfWork _unitOfWork; + private readonly IAmADynamoDbConnectionProvider _dynamoDbConnectionProvider; - public DeletePersonHandlerAsync(IAmABoxTransactionConnectionProvider unitOfWork) + public DeletePersonHandlerAsync(IAmADynamoDbConnectionProvider dynamoDbConnectionProvider) { - _unitOfWork = (DynamoDbUnitOfWork)unitOfWork; + _dynamoDbConnectionProvider = dynamoDbConnectionProvider; } [RequestLoggingAsync(0, HandlerTiming.Before)] [UsePolicyAsync(step:1, policy: Policies.Retry.EXPONENTIAL_RETRYPOLICYASYNC)] public override async Task HandleAsync(DeletePerson deletePerson, CancellationToken cancellationToken = default) { - var context = new DynamoDBContext(_unitOfWork.DynamoDb); + var context = new DynamoDBContext(_dynamoDbConnectionProvider.DynamoDb); await context.DeleteAsync(deletePerson.Name, cancellationToken); return await base.HandleAsync(deletePerson, cancellationToken); diff --git a/samples/WebAPI_Dynamo/GreetingsPorts/Handlers/FIndGreetingsForPersonHandlerAsync.cs b/samples/WebAPI_Dynamo/GreetingsPorts/Handlers/FIndGreetingsForPersonHandlerAsync.cs index aecaae4237..feb9290b80 100644 --- a/samples/WebAPI_Dynamo/GreetingsPorts/Handlers/FIndGreetingsForPersonHandlerAsync.cs +++ b/samples/WebAPI_Dynamo/GreetingsPorts/Handlers/FIndGreetingsForPersonHandlerAsync.cs @@ -2,6 +2,7 @@ using System.Threading; using System.Threading.Tasks; using Amazon.DynamoDBv2.DataModel; +using Amazon.DynamoDBv2.Model; using GreetingsEntities; using GreetingsPorts.Policies; using GreetingsPorts.Requests; @@ -16,18 +17,18 @@ namespace GreetingsPorts.Handlers { public class FIndGreetingsForPersonHandlerAsync : QueryHandlerAsync { - private readonly DynamoDbUnitOfWork _unitOfWork; + private readonly IAmADynamoDbConnectionProvider _dynamoDbConnectionProvider; - public FIndGreetingsForPersonHandlerAsync(IAmABoxTransactionConnectionProvider unitOfWork) + public FIndGreetingsForPersonHandlerAsync(IAmADynamoDbConnectionProvider dynamoDbConnectionProvider) { - _unitOfWork = (DynamoDbUnitOfWork ) unitOfWork; + _dynamoDbConnectionProvider = dynamoDbConnectionProvider; } [QueryLogging(0)] [RetryableQuery(1, Retry.EXPONENTIAL_RETRYPOLICYASYNC)] public override async Task ExecuteAsync(FindGreetingsForPerson query, CancellationToken cancellationToken = new CancellationToken()) { - var context = new DynamoDBContext(_unitOfWork.DynamoDb); + var context = new DynamoDBContext(_dynamoDbConnectionProvider.DynamoDb); var person = await context.LoadAsync(query.Name, cancellationToken); diff --git a/samples/WebAPI_Dynamo/GreetingsPorts/Handlers/FindPersonByNameHandlerAsync.cs b/samples/WebAPI_Dynamo/GreetingsPorts/Handlers/FindPersonByNameHandlerAsync.cs index 0a4f5b0e6b..b12ab3abc6 100644 --- a/samples/WebAPI_Dynamo/GreetingsPorts/Handlers/FindPersonByNameHandlerAsync.cs +++ b/samples/WebAPI_Dynamo/GreetingsPorts/Handlers/FindPersonByNameHandlerAsync.cs @@ -3,6 +3,7 @@ using System.Threading; using System.Threading.Tasks; using Amazon.DynamoDBv2.DataModel; +using Amazon.DynamoDBv2.Model; using GreetingsEntities; using GreetingsPorts.Policies; using GreetingsPorts.Requests; @@ -17,18 +18,18 @@ namespace GreetingsPorts.Handlers { public class FindPersonByNameHandlerAsync : QueryHandlerAsync { - private readonly DynamoDbUnitOfWork _unitOfWork; + private readonly IAmADynamoDbConnectionProvider _dynamoDbConnectionProvider; - public FindPersonByNameHandlerAsync(IAmABoxTransactionConnectionProvider unitOfWork) + public FindPersonByNameHandlerAsync(IAmADynamoDbConnectionProvider dynamoDbConnectionProvider) { - _unitOfWork = (DynamoDbUnitOfWork ) unitOfWork; + _dynamoDbConnectionProvider = dynamoDbConnectionProvider; } [QueryLogging(0)] [RetryableQuery(1, Retry.EXPONENTIAL_RETRYPOLICYASYNC)] public override async Task ExecuteAsync(FindPersonByName query, CancellationToken cancellationToken = new CancellationToken()) { - var context = new DynamoDBContext(_unitOfWork.DynamoDb); + var context = new DynamoDBContext(_dynamoDbConnectionProvider.DynamoDb); var person = await context.LoadAsync(query.Name, cancellationToken); diff --git a/samples/WebAPI_Dynamo/GreetingsWeb/Startup.cs b/samples/WebAPI_Dynamo/GreetingsWeb/Startup.cs index dbd582526a..5784b301fe 100644 --- a/samples/WebAPI_Dynamo/GreetingsWeb/Startup.cs +++ b/samples/WebAPI_Dynamo/GreetingsWeb/Startup.cs @@ -30,7 +30,8 @@ public class Startup { private const string _outBoxTableName = "Outbox"; private IWebHostEnvironment _env; - + private IAmazonDynamoDB _client; + public Startup(IConfiguration configuration, IWebHostEnvironment env) { Configuration = configuration; @@ -79,9 +80,9 @@ public void ConfigureServices(IServiceCollection services) private void ConfigureDynamo(IServiceCollection services) { - IAmazonDynamoDB client = CreateAndRegisterClient(services); - CreateEntityStore(client); - CreateOutbox(client, services); + _client = CreateAndRegisterClient(services); + CreateEntityStore(); + CreateOutbox(services); } private IAmazonDynamoDB CreateAndRegisterClient(IServiceCollection services) @@ -106,9 +107,6 @@ private IAmazonDynamoDB CreateAndRegisterLocalClient(IServiceCollection services var dynamoDb = new AmazonDynamoDBClient(credentials, clientConfig); services.Add(new ServiceDescriptor(typeof(IAmazonDynamoDB), dynamoDb)); - var dynamoDbConfiguration = new DynamoDbConfiguration(); - services.Add(new ServiceDescriptor(typeof(DynamoDbConfiguration), dynamoDbConfiguration)); - return dynamoDb; } @@ -117,10 +115,10 @@ private IAmazonDynamoDB CreateAndRegisterRemoteClient(IServiceCollection service throw new NotImplementedException(); } - private void CreateEntityStore(IAmazonDynamoDB client) + private void CreateEntityStore() { var tableRequestFactory = new DynamoDbTableFactory(); - var dbTableBuilder = new DynamoDbTableBuilder(client); + var dbTableBuilder = new DynamoDbTableBuilder(_client); CreateTableRequest tableRequest = tableRequestFactory.GenerateCreateTableRequest( new DynamoDbCreateProvisionedThroughput @@ -138,10 +136,10 @@ private void CreateEntityStore(IAmazonDynamoDB client) } } - private void CreateOutbox(IAmazonDynamoDB client, IServiceCollection services) + private void CreateOutbox(IServiceCollection services) { var tableRequestFactory = new DynamoDbTableFactory(); - var dbTableBuilder = new DynamoDbTableBuilder(client); + var dbTableBuilder = new DynamoDbTableBuilder(_client); var createTableRequest = new DynamoDbTableFactory().GenerateCreateTableRequest( new DynamoDbCreateProvisionedThroughput( @@ -163,6 +161,24 @@ private void CreateOutbox(IAmazonDynamoDB client, IServiceCollection services) private void ConfigureBrighter(IServiceCollection services) { + var producerRegistry = new RmqProducerRegistryFactory( + new RmqMessagingGatewayConnection + { + AmpqUri = new AmqpUriSpecification(new Uri("amqp://guest:guest@localhost:5672")), + Exchange = new Exchange("paramore.brighter.exchange"), + }, + new RmqPublication[]{ + new RmqPublication + { + Topic = new RoutingKey("GreetingMade"), + MaxOutStandingMessages = 5, + MaxOutStandingCheckIntervalMilliSeconds = 500, + WaitForConfirmsTimeOutInMilliseconds = 1000, + OutBoxBag = new Dictionary {{"Topic", "GreetingMade"}}, + MakeChannels = OnMissingChannel.Create + }} + ).Create(); + services.AddBrighter(options => { //we want to use scoped, so make sure everything understands that which needs to @@ -171,26 +187,13 @@ private void ConfigureBrighter(IServiceCollection services) options.MapperLifetime = ServiceLifetime.Singleton; options.PolicyRegistry = new GreetingsPolicy(); }) - .UseExternalBus(new RmqProducerRegistryFactory( - new RmqMessagingGatewayConnection - { - AmpqUri = new AmqpUriSpecification(new Uri("amqp://guest:guest@localhost:5672")), - Exchange = new Exchange("paramore.brighter.exchange"), - }, - new RmqPublication[]{ - new RmqPublication - { - Topic = new RoutingKey("GreetingMade"), - MaxOutStandingMessages = 5, - MaxOutStandingCheckIntervalMilliSeconds = 500, - WaitForConfirmsTimeOutInMilliseconds = 1000, - OutBoxBag = new Dictionary {{"Topic", "GreetingMade"}}, - MakeChannels = OnMissingChannel.Create - }} - ).Create() - ) - .UseDynamoDbOutbox(ServiceLifetime.Singleton) - .UseDynamoDbTransactionConnectionProvider(typeof(DynamoDbUnitOfWork), ServiceLifetime.Scoped) + .UseExternalBus((configure) => + { + configure.ProducerRegistry = producerRegistry; + configure.Outbox = new DynamoDbOutbox(_client, new DynamoDbConfiguration()); + configure.ConnectionProvider = typeof(DynamoDbUnitOfWork); + configure.TransactionProvider = typeof(DynamoDbUnitOfWork); + }) .UseOutboxSweeper(options => { options.Args.Add("Topic", "GreetingMade"); }) .AutoFromAssemblies(typeof(AddPersonHandlerAsync).Assembly); } diff --git a/samples/WebAPI_Dynamo/SalutationAnalytics/Program.cs b/samples/WebAPI_Dynamo/SalutationAnalytics/Program.cs index 7c7ef23f26..22dec0b091 100644 --- a/samples/WebAPI_Dynamo/SalutationAnalytics/Program.cs +++ b/samples/WebAPI_Dynamo/SalutationAnalytics/Program.cs @@ -69,7 +69,7 @@ private static void ConfigureBrighter( new SubscriptionName("paramore.sample.salutationanalytics"), new ChannelName("SalutationAnalytics"), new RoutingKey("GreetingMade"), - runAsync: true, + runAsync: false, timeoutInMilliseconds: 200, isDurable: true, makeChannels: OnMissingChannel.Create), //change to OnMissingChannel.Validate if you have infrastructure declared elsewhere @@ -84,6 +84,21 @@ private static void ConfigureBrighter( var rmqMessageConsumerFactory = new RmqMessageConsumerFactory(rmqConnection); + var producerRegistry = new RmqProducerRegistryFactory( + rmqConnection, + new RmqPublication[] + { + new RmqPublication + { + Topic = new RoutingKey("SalutationReceived"), + MaxOutStandingMessages = 5, + MaxOutStandingCheckIntervalMilliSeconds = 500, + WaitForConfirmsTimeOutInMilliseconds = 1000, + MakeChannels = OnMissingChannel.Create + } + } + ).Create(); + services.AddServiceActivator(options => { options.Subscriptions = subscriptions; @@ -93,38 +108,27 @@ private static void ConfigureBrighter( options.MapperLifetime = ServiceLifetime.Singleton; options.CommandProcessorLifetime = ServiceLifetime.Scoped; options.PolicyRegistry = new SalutationPolicy(); + options.InboxConfiguration = new InboxConfiguration( + ConfigureInbox(awsCredentials, dynamoDb), + scope: InboxScope.Commands, + onceOnly: true, + actionOnExists: OnceOnlyAction.Throw + ); }) .ConfigureJsonSerialisation((options) => { //We don't strictly need this, but added as an example options.PropertyNameCaseInsensitive = true; }) - .UseExternalBus(new RmqProducerRegistryFactory( - rmqConnection, - new RmqPublication[] - { - new RmqPublication - { - Topic = new RoutingKey("SalutationReceived"), - MaxOutStandingMessages = 5, - MaxOutStandingCheckIntervalMilliSeconds = 500, - WaitForConfirmsTimeOutInMilliseconds = 1000, - MakeChannels = OnMissingChannel.Create - } - } - ).Create() - ) - .AutoFromAssemblies() - .UseExternalInbox( - ConfigureInbox(awsCredentials, dynamoDb), - new InboxConfiguration( - scope: InboxScope.Commands, - onceOnly: true, - actionOnExists: OnceOnlyAction.Throw - ) + .UseExternalBus((configure) => + { + configure.ProducerRegistry = producerRegistry; + configure.Outbox = ConfigureOutbox(awsCredentials, dynamoDb); + configure.ConnectionProvider = typeof(DynamoDbUnitOfWork); + configure.TransactionProvider = typeof(DynamoDbUnitOfWork); + } ) - .UseExternalOutbox(ConfigureOutbox(awsCredentials, dynamoDb)) - .UseDynamoDbTransactionConnectionProvider(typeof(DynamoDbUnitOfWork), ServiceLifetime.Scoped); + .AutoFromAssemblies(); services.AddHostedService(); } @@ -240,7 +244,7 @@ private static IAmAnInbox ConfigureInbox(AWSCredentials credentials, IAmazonDyna return new DynamoDbInbox(dynamoDb, new DynamoDbInboxConfiguration(credentials, RegionEndpoint.EUWest1)); } - private static IAmAnOutbox ConfigureOutbox(AWSCredentials credentials, IAmazonDynamoDB dynamoDb) + private static IAmAnOutbox ConfigureOutbox(AWSCredentials credentials, IAmazonDynamoDB dynamoDb) { return new DynamoDbOutbox(dynamoDb, new DynamoDbConfiguration(credentials, RegionEndpoint.EUWest1)); } diff --git a/samples/WebAPI_Dynamo/SalutationPorts/Handlers/GreetingMadeHandler.cs b/samples/WebAPI_Dynamo/SalutationPorts/Handlers/GreetingMadeHandler.cs index 4fd22a6af0..89a91a2802 100644 --- a/samples/WebAPI_Dynamo/SalutationPorts/Handlers/GreetingMadeHandler.cs +++ b/samples/WebAPI_Dynamo/SalutationPorts/Handlers/GreetingMadeHandler.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.Logging; using Paramore.Brighter; using Paramore.Brighter.DynamoDb; +using Paramore.Brighter.Inbox.Attributes; using Paramore.Brighter.Logging.Attributes; using Paramore.Brighter.Policies.Attributes; using SalutationEntities; @@ -14,49 +15,56 @@ namespace SalutationPorts.Handlers { - public class GreetingMadeHandlerAsync : RequestHandlerAsync + public class GreetingMadeHandler : RequestHandler { - private readonly DynamoDbUnitOfWork _uow; + private readonly IAmADynamoDbTransactionProvider _transactionProvider; private readonly IAmACommandProcessor _postBox; - private readonly ILogger _logger; + private readonly ILogger _logger; - public GreetingMadeHandlerAsync(IAmABoxTransactionConnectionProvider uow, IAmACommandProcessor postBox, ILogger logger) + public GreetingMadeHandler(IAmADynamoDbTransactionProvider transactionProvider, IAmACommandProcessor postBox, ILogger logger) { - _uow = (DynamoDbUnitOfWork)uow; + _transactionProvider = transactionProvider; _postBox = postBox; _logger = logger; } - //[UseInboxAsync(step:0, contextKey: typeof(GreetingMadeHandlerAsync), onceOnly: true )] -- we are using a global inbox, so need to be explicit!! - [RequestLoggingAsync(step: 1, timing: HandlerTiming.Before)] - [UsePolicyAsync(step:2, policy: Policies.Retry.EXPONENTIAL_RETRYPOLICYASYNC)] - public override async Task HandleAsync(GreetingMade @event, CancellationToken cancellationToken = default) + [UseInbox(step:0, contextKey: typeof(GreetingMadeHandler), onceOnly: true )] + [RequestLogging(step: 1, timing: HandlerTiming.Before)] + [UsePolicy(step:2, policy: Policies.Retry.EXPONENTIAL_RETRYPOLICY)] + public override GreetingMade Handle(GreetingMade @event) { var posts = new List(); - var context = new DynamoDBContext(_uow.DynamoDb); - var tx = _uow.BeginOrGetTransaction(); + var context = new DynamoDBContext(_transactionProvider.DynamoDb); + var tx = _transactionProvider.GetTransaction(); try { - var salutation = new Salutation{ Greeting = @event.Greeting}; + var salutation = new Salutation { Greeting = @event.Greeting }; var attributes = context.ToDocument(salutation).ToAttributeMap(); - - tx.TransactItems.Add(new TransactWriteItem{Put = new Put{ TableName = "Salutations", Item = attributes}}); - - posts.Add(await _postBox.DepositPostAsync(new SalutationReceived(DateTimeOffset.Now), cancellationToken: cancellationToken)); - - await _uow.CommitAsync(cancellationToken); + + tx.TransactItems.Add(new TransactWriteItem + { + Put = new Put { TableName = "Salutations", Item = attributes } + }); + + posts.Add(_postBox.DepositPost(new SalutationReceived(DateTimeOffset.Now), _transactionProvider)); + + _transactionProvider.Commit(); } catch (Exception e) { _logger.LogError(e, "Could not save salutation"); - _uow.Rollback(); - - return await base.HandleAsync(@event, cancellationToken); + _transactionProvider.Rollback(); + + return base.Handle(@event); + } + finally + { + _transactionProvider.Close(); } - await _postBox.ClearOutboxAsync(posts, cancellationToken: cancellationToken); + _postBox.ClearOutboxAsync(posts); - return await base.HandleAsync(@event, cancellationToken); + return base.Handle(@event); } } } diff --git a/samples/WebAPI_Dynamo/SalutationPorts/Policies/Retry.cs b/samples/WebAPI_Dynamo/SalutationPorts/Policies/Retry.cs index 4db47aa42d..58013a5a3f 100644 --- a/samples/WebAPI_Dynamo/SalutationPorts/Policies/Retry.cs +++ b/samples/WebAPI_Dynamo/SalutationPorts/Policies/Retry.cs @@ -7,21 +7,21 @@ namespace SalutationPorts.Policies { public static class Retry { - public const string RETRYPOLICYASYNC = "SalutationPorts.Policies.RetryPolicyAsync"; - public const string EXPONENTIAL_RETRYPOLICYASYNC = "SalutationPorts.Policies.ExponenttialRetryPolicyAsync"; + public const string RETRYPOLICY = "SalutationPorts.Policies.RetryPolicy"; + public const string EXPONENTIAL_RETRYPOLICY = "SalutationPorts.Policies.ExponenttialRetryPolicy"; - public static AsyncRetryPolicy GetSimpleHandlerRetryPolicy() + public static RetryPolicy GetSimpleHandlerRetryPolicy() { - return Policy.Handle().WaitAndRetryAsync(new[] + return Policy.Handle().WaitAndRetry(new[] { TimeSpan.FromMilliseconds(50), TimeSpan.FromMilliseconds(100), TimeSpan.FromMilliseconds(150) }); } - public static AsyncRetryPolicy GetExponentialHandlerRetryPolicy() + public static RetryPolicy GetExponentialHandlerRetryPolicy() { var delay = Backoff.ExponentialBackoff(TimeSpan.FromMilliseconds(100), retryCount: 5, fastFirst: true); - return Policy.Handle().WaitAndRetryAsync(delay); + return Policy.Handle().WaitAndRetry(delay); } } } diff --git a/samples/WebAPI_Dynamo/SalutationPorts/Policies/SalutationPolicy.cs b/samples/WebAPI_Dynamo/SalutationPorts/Policies/SalutationPolicy.cs index ddf21c324f..28024f22a7 100644 --- a/samples/WebAPI_Dynamo/SalutationPorts/Policies/SalutationPolicy.cs +++ b/samples/WebAPI_Dynamo/SalutationPorts/Policies/SalutationPolicy.cs @@ -11,8 +11,8 @@ public SalutationPolicy() private void AddSalutationPolicies() { - Add(Retry.RETRYPOLICYASYNC, Retry.GetSimpleHandlerRetryPolicy()); - Add(Retry.EXPONENTIAL_RETRYPOLICYASYNC, Retry.GetExponentialHandlerRetryPolicy()); + Add(Retry.RETRYPOLICY, Retry.GetSimpleHandlerRetryPolicy()); + Add(Retry.EXPONENTIAL_RETRYPOLICY, Retry.GetExponentialHandlerRetryPolicy()); } } } diff --git a/samples/WebAPI_Dynamo/tests.http b/samples/WebAPI_Dynamo/tests.http index 26adac670e..ac986c633d 100644 --- a/samples/WebAPI_Dynamo/tests.http +++ b/samples/WebAPI_Dynamo/tests.http @@ -23,7 +23,7 @@ POST http://localhost:5000/Greetings/Tyrion/new HTTP/1.1 Content-Type: application/json { - "Greeting" : "I drink, and I know things" + "Greeting" : "I drink, and I know things #1" } ### And now look up Tyrion's greetings diff --git a/samples/WebAPI_EFCore/GreetingsEntities/GreetingsEntities.csproj b/samples/WebAPI_EFCore/GreetingsEntities/GreetingsEntities.csproj index 4f444d8c8b..7d9ee003f9 100644 --- a/samples/WebAPI_EFCore/GreetingsEntities/GreetingsEntities.csproj +++ b/samples/WebAPI_EFCore/GreetingsEntities/GreetingsEntities.csproj @@ -1,7 +1,7 @@ - net6.0 + net7.0 diff --git a/samples/WebAPI_EFCore/GreetingsPorts/GreetingsPorts.csproj b/samples/WebAPI_EFCore/GreetingsPorts/GreetingsPorts.csproj index 6ad024cb6a..99b918dfa9 100644 --- a/samples/WebAPI_EFCore/GreetingsPorts/GreetingsPorts.csproj +++ b/samples/WebAPI_EFCore/GreetingsPorts/GreetingsPorts.csproj @@ -1,7 +1,7 @@ - net6.0 + net7.0 diff --git a/samples/WebAPI_EFCore/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs b/samples/WebAPI_EFCore/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs index f7d2599ab5..7115622fd5 100644 --- a/samples/WebAPI_EFCore/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs +++ b/samples/WebAPI_EFCore/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs @@ -17,12 +17,13 @@ public class AddGreetingHandlerAsync: RequestHandlerAsync { private readonly GreetingsEntityGateway _uow; private readonly IAmACommandProcessor _postBox; - - public AddGreetingHandlerAsync(GreetingsEntityGateway uow, IAmACommandProcessor postBox) + private readonly IAmATransactionConnectionProvider _transactionProvider; + + public AddGreetingHandlerAsync(GreetingsEntityGateway uow, IAmATransactionConnectionProvider provider, IAmACommandProcessor postBox) { _uow = uow; _postBox = postBox; - + _transactionProvider = provider; } [RequestLoggingAsync(0, HandlerTiming.Before)] [UsePolicyAsync(step:1, policy: Policies.Retry.EXPONENTIAL_RETRYPOLICYASYNC)] @@ -30,33 +31,39 @@ public override async Task HandleAsync(AddGreeting addGreeting, Can { var posts = new List(); - //We span a Db outside of EF's control, so start an explicit transactional scope - var tx = await _uow.Database.BeginTransactionAsync(cancellationToken); + await _transactionProvider.GetTransactionAsync(cancellationToken); try { var person = await _uow.People .Where(p => p.Name == addGreeting.Name) .SingleAsync(cancellationToken); - + var greeting = new Greeting(addGreeting.Greeting); - + person.AddGreeting(greeting); - + //Now write the message we want to send to the Db in the same transaction. - posts.Add(await _postBox.DepositPostAsync(new GreetingMade(greeting.Greet()), cancellationToken: cancellationToken)); - + posts.Add(await _postBox.DepositPostAsync( + new GreetingMade(greeting.Greet()), + _transactionProvider, + cancellationToken: cancellationToken)); + //write the changed entity to the Db await _uow.SaveChangesAsync(cancellationToken); //write new person and the associated message to the Db - await tx.CommitAsync(cancellationToken); + await _transactionProvider.CommitAsync(cancellationToken); } catch (Exception) { //it went wrong, rollback the entity change and the downstream message - await tx.RollbackAsync(cancellationToken); + await _transactionProvider.RollbackAsync(cancellationToken); return await base.HandleAsync(addGreeting, cancellationToken); } + finally + { + _transactionProvider.Close(); + } //Send this message via a transport. We need the ids to send just the messages here, not all outstanding ones. //Alternatively, you can let the Sweeper do this, but at the cost of increased latency diff --git a/samples/WebAPI_EFCore/GreetingsWeb/Database/SchemaCreation.cs b/samples/WebAPI_EFCore/GreetingsWeb/Database/SchemaCreation.cs index d1a493e9f8..fe4bccbfa0 100644 --- a/samples/WebAPI_EFCore/GreetingsWeb/Database/SchemaCreation.cs +++ b/samples/WebAPI_EFCore/GreetingsWeb/Database/SchemaCreation.cs @@ -140,7 +140,8 @@ private static void CreateOutboxProduction(string connectionString) using var existsQuery = sqlConnection.CreateCommand(); existsQuery.CommandText = MySqlOutboxBuilder.GetExistsQuery(OUTBOX_TABLE_NAME); - bool exists = existsQuery.ExecuteScalar() != null; + var findOutbox = existsQuery.ExecuteScalar(); + bool exists = findOutbox is long and > 0; if (exists) return; diff --git a/samples/WebAPI_EFCore/GreetingsWeb/GreetingsWeb.csproj b/samples/WebAPI_EFCore/GreetingsWeb/GreetingsWeb.csproj index 68d27f0623..b4722b5527 100644 --- a/samples/WebAPI_EFCore/GreetingsWeb/GreetingsWeb.csproj +++ b/samples/WebAPI_EFCore/GreetingsWeb/GreetingsWeb.csproj @@ -1,11 +1,11 @@ - net6.0 + net7.0 - + diff --git a/samples/WebAPI_EFCore/GreetingsWeb/Startup.cs b/samples/WebAPI_EFCore/GreetingsWeb/Startup.cs index 3028c0341e..60376552e2 100644 --- a/samples/WebAPI_EFCore/GreetingsWeb/Startup.cs +++ b/samples/WebAPI_EFCore/GreetingsWeb/Startup.cs @@ -1,13 +1,9 @@ using System; -using System.Data; -using System.Data.Common; using GreetingsPorts.EntityGateway; using GreetingsPorts.Handlers; using GreetingsPorts.Policies; -using Hellang.Middleware.ProblemDetails; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; -using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -28,7 +24,6 @@ using Paramore.Darker.Policies; using Paramore.Darker.QueryLogging; using Polly; -using Polly.Registry; namespace GreetingsWeb { @@ -36,7 +31,7 @@ public class Startup { private const string _outBoxTableName = "Outbox"; private IWebHostEnvironment _env; - + public Startup(IConfiguration configuration, IWebHostEnvironment env) { Configuration = configuration; @@ -48,8 +43,6 @@ public Startup(IConfiguration configuration, IWebHostEnvironment env) // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { - app.UseProblemDetails(); - if (env.IsDevelopment()) { app.UseSwagger(); @@ -82,16 +75,17 @@ public void ConfigureServices(IServiceCollection services) ConfigureBrighter(services); ConfigureDarker(services); } - + private void CheckDbIsUp() { string connectionString = DbConnectionString(); - + var policy = Policy.Handle().WaitAndRetryForever( retryAttempt => TimeSpan.FromSeconds(2), (exception, timespan) => { - Console.WriteLine($"Healthcheck: Waiting for the database {connectionString} to come online - {exception.Message}"); + Console.WriteLine( + $"Healthcheck: Waiting for the database {connectionString} to come online - {exception.Message}"); }); policy.Execute(() => @@ -109,73 +103,35 @@ private void CheckDbIsUp() private void ConfigureBrighter(IServiceCollection services) { - if (_env.IsDevelopment()) - { - services.AddBrighter(options => - { - //we want to use scoped, so make sure everything understands that which needs to - options.HandlerLifetime = ServiceLifetime.Scoped; - options.CommandProcessorLifetime = ServiceLifetime.Scoped; - options.MapperLifetime = ServiceLifetime.Singleton; - options.PolicyRegistry = new GreetingsPolicy(); - }) - .UseExternalBus(new RmqProducerRegistryFactory( - new RmqMessagingGatewayConnection - { - AmpqUri = new AmqpUriSpecification(new Uri("amqp://guest:guest@localhost:5672")), - Exchange = new Exchange("paramore.brighter.exchange"), - }, - new RmqPublication[]{ - new RmqPublication - { - Topic = new RoutingKey("GreetingMade"), - MaxOutStandingMessages = 5, - MaxOutStandingCheckIntervalMilliSeconds = 500, - WaitForConfirmsTimeOutInMilliseconds = 1000, - MakeChannels = OnMissingChannel.Create - }} - ).Create() - ) - .UseSqliteOutbox(new SqliteConfiguration(DbConnectionString(), _outBoxTableName), typeof(SqliteConnectionProvider), ServiceLifetime.Singleton) - .UseSqliteTransactionConnectionProvider(typeof(SqliteEntityFrameworkConnectionProvider), ServiceLifetime.Scoped) - .UseOutboxSweeper(options => - { - options.TimerInterval = 5; - options.MinimumMessageAge = 5000; - }) - .AutoFromAssemblies(); - } - else - { - services.AddBrighter(options => - { - options.HandlerLifetime = ServiceLifetime.Scoped; - options.MapperLifetime = ServiceLifetime.Singleton; - options.PolicyRegistry = new GreetingsPolicy(); - }) - .UseExternalBus(new RmqProducerRegistryFactory( - new RmqMessagingGatewayConnection - { - AmpqUri = new AmqpUriSpecification(new Uri("amqp://guest:guest@rabbitmq:5672")), - Exchange = new Exchange("paramore.brighter.exchange"), - }, - new RmqPublication[] { - new RmqPublication - { - Topic = new RoutingKey("GreetingMade"), - MaxOutStandingMessages = 5, - MaxOutStandingCheckIntervalMilliSeconds = 500, - WaitForConfirmsTimeOutInMilliseconds = 1000, - MakeChannels = OnMissingChannel.Create - }} - ).Create() - ) - .UseMySqlOutbox(new MySqlConfiguration(DbConnectionString(), _outBoxTableName), typeof(MySqlConnectionProvider), ServiceLifetime.Singleton) - .UseMySqTransactionConnectionProvider(typeof(MySqlEntityFrameworkConnectionProvider), ServiceLifetime.Scoped) - .UseOutboxSweeper() - .AutoFromAssemblies(); - } + (IAmAnOutbox outbox, Type transactionProvider, Type connectionProvider) = MakeOutbox(); + var outboxConfiguration = new RelationalDatabaseConfiguration(DbConnectionString()); + services.AddSingleton(outboxConfiguration); + + IAmAProducerRegistry producerRegistry = ConfigureProducerRegistry(); + services.AddBrighter(options => + { + //we want to use scoped, so make sure everything understands that which needs to + options.HandlerLifetime = ServiceLifetime.Scoped; + options.CommandProcessorLifetime = ServiceLifetime.Scoped; + options.MapperLifetime = ServiceLifetime.Singleton; + options.PolicyRegistry = new GreetingsPolicy(); + }) + .UseExternalBus((configure) => + { + configure.ProducerRegistry = producerRegistry; + configure.Outbox = outbox; + configure.TransactionProvider = transactionProvider; + configure.ConnectionProvider = connectionProvider; + } + ) + .UseOutboxSweeper(options => + { + options.TimerInterval = 5; + options.MinimumMessageAge = 5000; + }) + .UseOutboxSweeper() + .AutoFromAssemblies(); } private void ConfigureDarker(IServiceCollection services) @@ -188,7 +144,6 @@ private void ConfigureDarker(IServiceCollection services) .AddHandlersFromAssemblies(typeof(FindPersonByNameHandlerAsync).Assembly) .AddJsonQueryLogging() .AddPolicies(new GreetingsPolicy()); - } private void ConfigureEFCore(IServiceCollection services) @@ -200,33 +155,79 @@ private void ConfigureEFCore(IServiceCollection services) services.AddDbContext( builder => { - builder.UseSqlite(connectionString, + builder.UseSqlite(connectionString, optionsBuilder => { optionsBuilder.MigrationsAssembly("Greetings_SqliteMigrations"); - }); + }) + .EnableDetailedErrors() + .EnableSensitiveDataLogging(); }); } else { - services.AddDbContextPool(builder => - { - builder - .UseMySql(connectionString, ServerVersion.AutoDetect(connectionString), optionsBuilder => - { - optionsBuilder.MigrationsAssembly("Greetings_MySqlMigrations"); - }) - .EnableDetailedErrors() - .EnableSensitiveDataLogging(); - }); + services.AddDbContextPool(builder => + { + builder + .UseMySql(connectionString, ServerVersion.AutoDetect(connectionString), optionsBuilder => + { + optionsBuilder.MigrationsAssembly("Greetings_MySqlMigrations"); + }) + .EnableDetailedErrors() + .EnableSensitiveDataLogging(); + }); } } + private static IAmAProducerRegistry ConfigureProducerRegistry() + { + var producerRegistry = new RmqProducerRegistryFactory( + new RmqMessagingGatewayConnection + { + AmpqUri = new AmqpUriSpecification(new Uri("amqp://guest:guest@localhost:5672")), + Exchange = new Exchange("paramore.brighter.exchange"), + }, + new RmqPublication[] + { + new RmqPublication + { + Topic = new RoutingKey("GreetingMade"), + MaxOutStandingMessages = 5, + MaxOutStandingCheckIntervalMilliSeconds = 500, + WaitForConfirmsTimeOutInMilliseconds = 1000, + MakeChannels = OnMissingChannel.Create + } + } + ).Create(); + return producerRegistry; + } private string DbConnectionString() { //NOTE: Sqlite needs to use a shared cache to allow Db writes to the Outbox as well as entities - return _env.IsDevelopment() ? "Filename=Greetings.db;Cache=Shared" : Configuration.GetConnectionString("Greetings"); + return _env.IsDevelopment() + ? "Filename=Greetings.db;Cache=Shared" + : Configuration.GetConnectionString("Greetings"); + } + + private (IAmAnOutbox outbox, Type transactionProvider, Type connectionProvider) MakeOutbox() + { + if (_env.IsDevelopment()) + { + var outbox = new SqliteOutbox( + new RelationalDatabaseConfiguration(DbConnectionString(), _outBoxTableName)); + var transactionProvider = typeof(SqliteEntityFrameworkConnectionProvider); + var connectionProvider = typeof(SqliteConnectionProvider); + return (outbox, transactionProvider, connectionProvider); + } + else + { + var outbox = new MySqlOutbox( + new RelationalDatabaseConfiguration(DbConnectionString(), _outBoxTableName)); + var transactionProvider = typeof(MySqlEntityFrameworkConnectionProvider); + var connectionProvider = typeof(MySqlConnectionProvider); + return (outbox, transactionProvider, connectionProvider); + } } } } diff --git a/samples/WebAPI_EFCore/Greetings_MySqlMigrations/Greetings_MySqlMigrations.csproj b/samples/WebAPI_EFCore/Greetings_MySqlMigrations/Greetings_MySqlMigrations.csproj index fab1b18a4a..cf93cb986b 100644 --- a/samples/WebAPI_EFCore/Greetings_MySqlMigrations/Greetings_MySqlMigrations.csproj +++ b/samples/WebAPI_EFCore/Greetings_MySqlMigrations/Greetings_MySqlMigrations.csproj @@ -1,7 +1,7 @@ - net6.0 + net7.0 diff --git a/samples/WebAPI_EFCore/Greetings_MySqlMigrations/Migrations/20210920201732_InitialCreate.Designer.cs b/samples/WebAPI_EFCore/Greetings_MySqlMigrations/Migrations/20210920201732_InitialCreate.Designer.cs index 5a1e895969..2652d4af8a 100644 --- a/samples/WebAPI_EFCore/Greetings_MySqlMigrations/Migrations/20210920201732_InitialCreate.Designer.cs +++ b/samples/WebAPI_EFCore/Greetings_MySqlMigrations/Migrations/20210920201732_InitialCreate.Designer.cs @@ -10,7 +10,7 @@ namespace Greetings_MySqlMigrations.Migrations { [DbContext(typeof(GreetingsEntityGateway))] [Migration("20210920201732_InitialCreate")] - partial class InitialCreate + partial class MySqlInitialCreate { protected override void BuildTargetModel(ModelBuilder modelBuilder) { diff --git a/samples/WebAPI_EFCore/Greetings_MySqlMigrations/Migrations/20210920201732_InitialCreate.cs b/samples/WebAPI_EFCore/Greetings_MySqlMigrations/Migrations/20210920201732_InitialCreate.cs index 60611e06ce..ddcd96f016 100644 --- a/samples/WebAPI_EFCore/Greetings_MySqlMigrations/Migrations/20210920201732_InitialCreate.cs +++ b/samples/WebAPI_EFCore/Greetings_MySqlMigrations/Migrations/20210920201732_InitialCreate.cs @@ -4,7 +4,7 @@ namespace Greetings_MySqlMigrations.Migrations { - public partial class InitialCreate : Migration + public partial class MySqlInitialCreate : Migration { protected override void Up(MigrationBuilder migrationBuilder) { diff --git a/samples/WebAPI_EFCore/Greetings_SqliteMigrations/Greetings_SqliteMigrations.csproj b/samples/WebAPI_EFCore/Greetings_SqliteMigrations/Greetings_SqliteMigrations.csproj index e347ce3528..14afa7dfbb 100644 --- a/samples/WebAPI_EFCore/Greetings_SqliteMigrations/Greetings_SqliteMigrations.csproj +++ b/samples/WebAPI_EFCore/Greetings_SqliteMigrations/Greetings_SqliteMigrations.csproj @@ -1,7 +1,7 @@ - net6.0 + net7.0 diff --git a/samples/WebAPI_EFCore/Greetings_SqliteMigrations/Migrations/20210902180249_Initial.Designer.cs b/samples/WebAPI_EFCore/Greetings_SqliteMigrations/Migrations/20210902180249_Initial.Designer.cs index d7e6605b1f..797cdcf0d5 100644 --- a/samples/WebAPI_EFCore/Greetings_SqliteMigrations/Migrations/20210902180249_Initial.Designer.cs +++ b/samples/WebAPI_EFCore/Greetings_SqliteMigrations/Migrations/20210902180249_Initial.Designer.cs @@ -9,7 +9,7 @@ namespace Greetings_SqliteMigrations.Migrations { [DbContext(typeof(GreetingsEntityGateway))] [Migration("20210902180249_Initial")] - partial class Initial + partial class SqliteInitialCreate { protected override void BuildTargetModel(ModelBuilder modelBuilder) { diff --git a/samples/WebAPI_EFCore/Greetings_SqliteMigrations/Migrations/20210902180249_Initial.cs b/samples/WebAPI_EFCore/Greetings_SqliteMigrations/Migrations/20210902180249_Initial.cs index 2fa6db671e..5e882b87fe 100644 --- a/samples/WebAPI_EFCore/Greetings_SqliteMigrations/Migrations/20210902180249_Initial.cs +++ b/samples/WebAPI_EFCore/Greetings_SqliteMigrations/Migrations/20210902180249_Initial.cs @@ -2,7 +2,7 @@ namespace Greetings_SqliteMigrations.Migrations { - public partial class Initial : Migration + public partial class SqliteInitialCreate : Migration { protected override void Up(MigrationBuilder migrationBuilder) { diff --git a/samples/WebAPI_EFCore/README.md b/samples/WebAPI_EFCore/README.md index ec61ff1b6d..ca6aee0bce 100644 --- a/samples/WebAPI_EFCore/README.md +++ b/samples/WebAPI_EFCore/README.md @@ -2,7 +2,6 @@ - [Web API and EF Core Example](#web-api-and-ef-core-example) * [Environments](#environments) * [Architecture](#architecture) - + [Outbox](#outbox) + [GreetingsAPI](#greetingsapi) + [SalutationAnalytics](#salutationanalytics) * [Build and Deploy](#build-and-deploy) @@ -14,32 +13,43 @@ - [Connection issue with the RabbitMQ](#connection-issue-with-the-rabbitmq) - [Helpful documentation links](#helpful-documentation-links) * [Tests](#tests) + # Web API and EF Core Example + This sample shows a typical scenario when using WebAPI and Brighter/Darker. It demonstrates both using Brighter and Darker to implement the API endpoints, and using a work queue to handle asynchronous work that results from handling the API call. ## Environments -*Development* - runs locally on your machine, uses Sqlite as a data store; uses RabbitMQ for messaging, can be launched individually from the docker compose file; it represents a typical setup for development. +*Development* + +- Uses a local Sqlite instance for the data store. +- We support Docker hosted messaging brokers, either RabbitMQ or Kafka. -*Production* - runs in Docker, uses MySql as a data store; uses RabbitMQ for messaging; it emulates a possible production environment. +*Production* +- We offer support for MySQL using Docker. +- We support Docker hosted messaging brokers, using RabbitMQ. -We provide launchSetting.json files for both, which allows you to run Production; you should launch MySQl and RabbitMQ from the docker compose file; useful for debugging MySQL operations. +### Configuration + +We provide launchSetting.json files for all of these, which allows you to run Production with the appropriate db; you should launch your SQL data store and broker from the docker compose file. In case you are using Command Line Interface for running the project, consider adding --launch-profile: ```sh -dotnet run --launch-profile Production -d +dotnet run --launch-profile XXXXXX -d ``` + ## Architecture -### Outbox - Brighter does have an [Outbox pattern support](https://paramore.readthedocs.io/en/latest/OutboxPattern.html). In case you are new to it, consider reading it before diving deeper. + ### GreetingsAPI We follow a _ports and adapters_ architectural style, dividing the app into the following modules: * **GreetingsAdapters**: The adapters' module, handles the primary adapter of HTTP requests and responses to the app - * **GreetingsPorts**: the ports' module, handles requests from the primary adapter (HTTP) to the domain, and requests to secondary adapters. In a fuller app, the handlers for the primary adapter would correspond to our use case boundaries. The secondary port of the EntityGateway handles access to the DB via EF Core. We choose to treat EF Core as a port, not an adapter itself, here, as it wraps our underlying adapters for Sqlite or MySql. + * **GreetingsPorts**: the ports' module, handles requests from the primary adapter (HTTP) to the domain, and requests to secondary adapters. +In a fuller app, the handlers for the primary adapter would correspond to our use case boundaries. The secondary port of the EntityGateway handles access to the DB via EF Core. +We choose to treat EF Core as a port, not an adapter itself, here, as it wraps our underlying adapters for Sqlite or MySql. * **GreetingsEntities**: the domain model (or application in ports & adapters). In a fuller app, this would contain the logic that has a dependency on entity state. @@ -49,76 +59,68 @@ The assemblies migrations: **Greetings_MySqlMigrations** and **Greetings_SqliteM ### SalutationAnalytics -This listens for a GreetingMade message and stores it. It demonstrates listening to a queue. It also demonstrates the use of scopes provided by Brighter's ServiceActivator, which work with EFCore. These support writing to an Outbox when this component raises a message in turn. +* **SalutationAnalytics** The adapter subscribes to GreetingMade messages. It demonstrates listening to a queue. It also demonstrates the use of scopes provided by Brighter's ServiceActivator, which work with Dapper. These support writing to an Outbox when this component raises a message in turn. -We don't listen to that message, and without any listeners the RabbitMQ will drop the message we send, as it has no queues to give it to. We don't listen because we would just be repeating what we have shown here. If you want to see the messages produced, use the RMQ Management Console (localhost:15672) to create a queue and then bind it to the paramore.binding.exchange with the routingkey of SalutationReceived. +* **SalutationPorts** The ports' module, handles requests from the primary adapter to the domain, and requests to secondary adapters. It writes to the entity store and sends another message. We don't listen to that message. Note that without any listeners RabbitMQ will drop the message we send, as it has no queues to give it to. + If you want to see the messages produced, use the RMQ Management Console (localhost:15672) or Kafka Console (localhost:9021). (You will need to create a subscribing queue in RabbitMQ) -We also add an Inbox here. The Inbox can be used to de-duplicate messages. In messaging, the guarantee is 'at least once' if you use a technique such as an Outbox to ensure sending. This means we may receive a message twice. We either need, as in this case, to use an Inbox to de-duplicate, or we need to be idempotent such that receiving the message multiple times would result in the same outcome. +* **SalutationEntities** The domain model (or application in ports & adapters). In a fuller app, this would contain the logic that has a dependency on entity state. +We add an Inbox as well as the Outbox here. The Inbox can be used to de-duplicate messages. In messaging, the guarantee is 'at least once' if you use a technique such as an Outbox to ensure sending. This means we may receive a message twice. We either need, as in this case, to use an Inbox to de-duplicate, or we need to be idempotent such that receiving the message multiple times would result in the same outcome. -## Build and Deploy +The assemblies migrations: **Salutations_MySqlMigrations** and **Salutations_SqliteMigrations** hold code to configure the Db. -### Building +We don't listen to that message, and without any listeners the RabbitMQ will drop the message we send, as it has no queues to give it to. We don't listen because we would just be repeating what we have shown here. If you want to see the messages produced, use the RMQ Management Console (localhost:15672) to create a queue and then bind it to the paramore.binding.exchange with the routingkey of SalutationReceived. -Use the build.sh file to: +We also add an Inbox here. The Inbox can be used to de-duplicate messages. In messaging, the guarantee is 'at least once' if you use a technique such as an Outbox to ensure sending. This means we may receive a message twice. We either need, as in this case, to use an Inbox to de-duplicate, or we need to be idempotent such that receiving the message multiple times would result in the same outcome. -- Build both GreetingsAdapters and SalutationAnalytics and publish it to the /out directory. The Dockerfile assumes the app will be published here. -- Build the Docker image from the Dockerfile for each. +### Possible issues +#### Sqlite Database Read-Only Errors -(Why not use a multi-stage Docker build? We can't do this as the projects here reference projects not NuGet packages for Brighter libraries and there are not in the Docker build context.) +A Sqlite database will only have permissions for the process that created it. This can result in you receiving read-only errors between invocations of the sample. You either need to alter the permissions on your Db, or delete it between runs. -A common error is to change something, forget to run build.sh and use an old Docker image. +Maintainers, please don't check the Sqlite files into source control. -### Deploy +#### Queue Creation and Dropped Messages -We provide a docker compose file to allow you to run a 'Production' environment or to startup RabbitMQ for production: -```sh -docker compose up -d rabbitmq # will just start rabbitmq -``` +Queues are created by consumers. This is because publishers don't know who consumes them, and thus don't create their queues. This means that if you run a producer, such as GreetingsWeb, and use tests.http to push in greetings, although a message will be published to RabbitMQ, it won't have a queue to be delivered to and will be dropped, unless you have first run the SalutationAnalytics worker to create the queue. + +Generally, the rule of thumb is: start the consumer and *then* start the producer. + +You can spot this by looking in the [RabbitMQ Management console](http://localhost:1567) and noting that no queue is bound to the routing key in the exchange. +You can use default credentials for the RabbitMQ Management console: ```sh -docker compose up -d mysql # will just start mysql +user :guest +passowrd: guest ``` +## Acceptance Tests +We provide a tests.http file (supported by both JetBrains Rider and VS Code with the REST Client plugin) to allow you to test operations on the API. -and so on. +## Possible issues -### Possible issues #### Sqlite Database Read-Only Errors A Sqlite database will only have permissions for the process that created it. This can result in you receiving read-only errors between invocations of the sample. You either need to alter the permissions on your Db, or delete it between runs. Maintainers, please don't check the Sqlite files into source control. -#### Queue Creation and Dropped Messages +#### RabbitMQ Queue Creation and Dropped Messages -Queues are created by consumers. This is because publishers don't know who consumes them, and thus don't create their queues. This means that if you run a producer, such as GreetingsWeb, and use tests.http to push in greetings, although a message will be published to RabbitMQ, it won't have a queue to be delivered to and will be dropped, unless you have first run the SalutationAnalytics worker to create the queue. +For Rabbit MQ, queues are created by consumers. This is because publishers don't know who consumes them, and thus don't create their queues. This means that if you run a producer, such as GreetingsWeb, and use tests.http to push in greetings, although a message will be published to RabbitMQ, it won't have a queue to be delivered to and will be dropped, unless you have first run the SalutationAnalytics worker to create the queue. Generally, the rule of thumb is: start the consumer and *then* start the producer. -You can spot this by looking in the [RabbitMQ Management console](http://localhost:1567) and noting that no queue is bound to the routing key in the exchange. +You can spot this by looking in the [RabbitMQ Management console](http://localhost:15672) and noting that no queue is bound to the routing key in the exchange. You can use default credentials for the RabbitMQ Management console: + ```sh user :guest passowrd: guest ``` -#### Connection issue with the RabbitMQ -When running RabbitMQ from the docker compose file (without any additional network setup, etc.) your RabbitMQ instance in docker will still be accessible by **localhost** as a host name. Consider this when running your application in the Production environment. -In Production, the application by default will have: -```sh -amqp://guest:guest@rabbitmq:5672 -``` - -as an Advanced Message Queuing Protocol (AMQP) connection string. -So one of the options will be replacing AMQP connection string with: -```sh -amqp://guest:guest@localhost:5672 -``` -In case you still struggle, consider following these steps: [RabbitMQ Troubleshooting Networking](https://www.rabbitmq.com/troubleshooting-networking.html) #### Helpful documentation links * [Brighter technical documentation](https://paramore.readthedocs.io/en/latest/index.html) * [Rabbit Message Queue (RMQ) documentation](https://www.rabbitmq.com/documentation.html) +* [Kafka documentation](https://kafka.apache.org/documentation/) -## Tests - -We provide a tests.http file (supported by both JetBrains Rider and VS Code with the REST Client plugin) to allow you to test operations. \ No newline at end of file diff --git a/samples/WebAPI_EFCore/SalutationAnalytics/Program.cs b/samples/WebAPI_EFCore/SalutationAnalytics/Program.cs index a1afcb3d0b..c68f8e6ce2 100644 --- a/samples/WebAPI_EFCore/SalutationAnalytics/Program.cs +++ b/samples/WebAPI_EFCore/SalutationAnalytics/Program.cs @@ -12,9 +12,14 @@ using Paramore.Brighter.Inbox.MySql; using Paramore.Brighter.Inbox.Sqlite; using Paramore.Brighter.MessagingGateway.RMQ; +using Paramore.Brighter.MySql; +using Paramore.Brighter.MySql.EntityFrameworkCore; +using Paramore.Brighter.Outbox.MySql; +using Paramore.Brighter.Outbox.Sqlite; using Paramore.Brighter.ServiceActivator.Extensions.DependencyInjection; using Paramore.Brighter.ServiceActivator.Extensions.Hosting; -using Polly.Registry; +using Paramore.Brighter.Sqlite; +using Paramore.Brighter.Sqlite.EntityFrameworkCore; using SalutationAnalytics.Database; using SalutationPorts.EntityGateway; using SalutationPorts.Policies; @@ -41,7 +46,9 @@ private static IHostBuilder CreateHostBuilder(string[] args) => configurationBuilder.SetBasePath(Directory.GetCurrentDirectory()); configurationBuilder.AddJsonFile("appsettings.json", optional: true); configurationBuilder.AddJsonFile($"appsettings.{GetEnvironment()}.json", optional: true); - configurationBuilder.AddEnvironmentVariables(prefix: "ASPNETCORE_"); //NOTE: Although not web, we use this to grab the environment + configurationBuilder + .AddEnvironmentVariables( + prefix: "ASPNETCORE_"); //NOTE: Although not web, we use this to grab the environment configurationBuilder.AddEnvironmentVariables(prefix: "BRIGHTER_"); configurationBuilder.AddCommandLine(args); }) @@ -59,27 +66,30 @@ private static IHostBuilder CreateHostBuilder(string[] args) => private static void ConfigureBrighter(HostBuilderContext hostContext, IServiceCollection services) { + (IAmAnOutbox outbox, Type transactionProvider, Type connectionProvider) = MakeOutbox(hostContext); + var outboxConfiguration = new RelationalDatabaseConfiguration(DbConnectionString(hostContext)); + services.AddSingleton(outboxConfiguration); + + IAmAProducerRegistry producerRegistry = ConfigureProducerRegistry(); + var subscriptions = new Subscription[] { new RmqSubscription( new SubscriptionName("paramore.sample.salutationanalytics"), new ChannelName("SalutationAnalytics"), new RoutingKey("GreetingMade"), - runAsync: true, + runAsync: false, timeoutInMilliseconds: 200, isDurable: true, - makeChannels: OnMissingChannel.Create), //change to OnMissingChannel.Validate if you have infrastructure declared elsewhere + makeChannels: OnMissingChannel + .Create), //change to OnMissingChannel.Validate if you have infrastructure declared elsewhere }; - var host = hostContext.HostingEnvironment.IsDevelopment() ? "localhost" : "rabbitmq"; - - var rmqConnection = new RmqMessagingGatewayConnection + var rmqMessageConsumerFactory = new RmqMessageConsumerFactory(new RmqMessagingGatewayConnection { - AmpqUri = new AmqpUriSpecification(new Uri($"amqp://guest:guest@{host}:5672")), - Exchange = new Exchange("paramore.brighter.exchange") - }; - - var rmqMessageConsumerFactory = new RmqMessageConsumerFactory(rmqConnection); + AmpqUri = new AmqpUriSpecification(new Uri("amqp://guest:guest@localhost:5672")), + Exchange = new Exchange("paramore.brighter.exchange"), + }); services.AddServiceActivator(options => { @@ -90,41 +100,32 @@ private static void ConfigureBrighter(HostBuilderContext hostContext, IServiceCo options.MapperLifetime = ServiceLifetime.Singleton; options.CommandProcessorLifetime = ServiceLifetime.Scoped; options.PolicyRegistry = new SalutationPolicy(); - }) - .UseExternalBus(new RmqProducerRegistryFactory( - rmqConnection, - new RmqPublication[] - { - new RmqPublication - { - Topic = new RoutingKey("SalutationReceived"), - MaxOutStandingMessages = 5, - MaxOutStandingCheckIntervalMilliSeconds = 500, - WaitForConfirmsTimeOutInMilliseconds = 1000, - MakeChannels = OnMissingChannel.Create - } - } - ).Create() - ) - .AutoFromAssemblies() - .UseExternalInbox( - ConfigureInbox(hostContext), - new InboxConfiguration( + options.InboxConfiguration = new InboxConfiguration( + ConfigureInbox(hostContext), scope: InboxScope.Commands, onceOnly: true, actionOnExists: OnceOnlyAction.Throw - ) - ); + ); + }) + .UseExternalBus((configure) => + { + configure.ProducerRegistry = producerRegistry; + configure.Outbox = outbox; + configure.TransactionProvider = transactionProvider; + configure.ConnectionProvider = connectionProvider; + }) + .AutoFromAssemblies(); services.AddHostedService(); } + private static string GetEnvironment() { //NOTE: Hosting Context will always return Production outside of ASPNET_CORE at this point, so grab it directly return Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"); } - + private static void ConfigureEFCore(HostBuilderContext hostContext, IServiceCollection services) { string connectionString = DbConnectionString(hostContext); @@ -134,25 +135,25 @@ private static void ConfigureEFCore(HostBuilderContext hostContext, IServiceColl services.AddDbContext( builder => { - builder.UseSqlite(connectionString, + builder.UseSqlite(connectionString, optionsBuilder => { - optionsBuilder.MigrationsAssembly("Salutations_SqliteMigrations"); + optionsBuilder.MigrationsAssembly("Salutations_Migrations"); }); }); } else { - services.AddDbContextPool(builder => - { - builder - .UseMySql(connectionString, ServerVersion.AutoDetect(connectionString), optionsBuilder => - { - optionsBuilder.MigrationsAssembly("Salutations_MySqlMigrations"); - }) - .EnableDetailedErrors() - .EnableSensitiveDataLogging(); - }); + services.AddDbContextPool(builder => + { + builder + .UseMySql(connectionString, ServerVersion.AutoDetect(connectionString), optionsBuilder => + { + optionsBuilder.MigrationsAssembly("Salutations_MySqlMigrations"); + }) + .EnableDetailedErrors() + .EnableSensitiveDataLogging(); + }); } } @@ -160,17 +161,63 @@ private static IAmAnInbox ConfigureInbox(HostBuilderContext hostContext) { if (hostContext.HostingEnvironment.IsDevelopment()) { - return new SqliteInbox(new SqliteInboxConfiguration(DbConnectionString(hostContext), SchemaCreation.INBOX_TABLE_NAME)); + return new SqliteInbox(new RelationalDatabaseConfiguration(DbConnectionString(hostContext), + SchemaCreation.INBOX_TABLE_NAME)); } - return new MySqlInbox(new MySqlInboxConfiguration(DbConnectionString(hostContext), SchemaCreation.INBOX_TABLE_NAME)); + return new MySqlInbox(new RelationalDatabaseConfiguration(DbConnectionString(hostContext), + SchemaCreation.INBOX_TABLE_NAME)); } + private static IAmAProducerRegistry ConfigureProducerRegistry() + { + var producerRegistry = new RmqProducerRegistryFactory( + new RmqMessagingGatewayConnection + { + AmpqUri = new AmqpUriSpecification(new Uri("amqp://guest:guest@localhost:5672")), + Exchange = new Exchange("paramore.brighter.exchange"), + }, + new RmqPublication[] + { + new RmqPublication + { + Topic = new RoutingKey("SalutationReceived"), + MaxOutStandingMessages = 5, + MaxOutStandingCheckIntervalMilliSeconds = 500, + WaitForConfirmsTimeOutInMilliseconds = 1000, + MakeChannels = OnMissingChannel.Create + } + } + ).Create(); + + return producerRegistry; + } + + private static string DbConnectionString(HostBuilderContext hostContext) { //NOTE: Sqlite needs to use a shared cache to allow Db writes to the Outbox as well as entities - return hostContext.HostingEnvironment.IsDevelopment() ? "Filename=Salutations.db;Cache=Shared" : hostContext.Configuration.GetConnectionString("Salutations"); + return hostContext.HostingEnvironment.IsDevelopment() + ? "Filename=Salutations.db;Cache=Shared" + : hostContext.Configuration.GetConnectionString("Salutations"); + } + + private static (IAmAnOutbox outbox, Type transactionProvider, Type connectionProvider) MakeOutbox(HostBuilderContext hostContext) + { + if (hostContext.HostingEnvironment.IsDevelopment()) + { + var outbox = new SqliteOutbox(new RelationalDatabaseConfiguration(DbConnectionString(hostContext))); + var transactionProvider = typeof(SqliteEntityFrameworkConnectionProvider); + var connectionProvider = typeof(SqliteConnectionProvider); + return (outbox, transactionProvider, connectionProvider); + } + else + { + var outbox = new MySqlOutbox(new RelationalDatabaseConfiguration(DbConnectionString(hostContext))); + var transactionProvider = typeof(MySqlEntityFrameworkConnectionProvider); + var connectionProvider = typeof(MySqlConnectionProvider); + return (outbox, transactionProvider, connectionProvider); + } } - } } diff --git a/samples/WebAPI_EFCore/SalutationAnalytics/SalutationAnalytics.csproj b/samples/WebAPI_EFCore/SalutationAnalytics/SalutationAnalytics.csproj index 8a911e922d..c2af94ab47 100644 --- a/samples/WebAPI_EFCore/SalutationAnalytics/SalutationAnalytics.csproj +++ b/samples/WebAPI_EFCore/SalutationAnalytics/SalutationAnalytics.csproj @@ -2,18 +2,20 @@ Exe - net6.0 + net7.0 + + diff --git a/samples/WebAPI_EFCore/SalutationEntities/SalutationEntities.csproj b/samples/WebAPI_EFCore/SalutationEntities/SalutationEntities.csproj index dbc151713b..8268829b64 100644 --- a/samples/WebAPI_EFCore/SalutationEntities/SalutationEntities.csproj +++ b/samples/WebAPI_EFCore/SalutationEntities/SalutationEntities.csproj @@ -1,7 +1,7 @@ - net6.0 + net7.0 diff --git a/samples/WebAPI_EFCore/SalutationPorts/Handlers/GreetingMadeHandler.cs b/samples/WebAPI_EFCore/SalutationPorts/Handlers/GreetingMadeHandler.cs index 7f50726fda..ecb171a58d 100644 --- a/samples/WebAPI_EFCore/SalutationPorts/Handlers/GreetingMadeHandler.cs +++ b/samples/WebAPI_EFCore/SalutationPorts/Handlers/GreetingMadeHandler.cs @@ -12,51 +12,60 @@ namespace SalutationPorts.Handlers { - public class GreetingMadeHandlerAsync : RequestHandlerAsync + public class GreetingMadeHandler : RequestHandler { private readonly SalutationsEntityGateway _uow; private readonly IAmACommandProcessor _postBox; + private readonly IAmATransactionConnectionProvider _transactionProvider; - public GreetingMadeHandlerAsync(SalutationsEntityGateway uow, IAmACommandProcessor postBox) + public GreetingMadeHandler(SalutationsEntityGateway uow, IAmATransactionConnectionProvider provider, IAmACommandProcessor postBox) { _uow = uow; _postBox = postBox; + _transactionProvider = provider; } - //[UseInboxAsync(step:0, contextKey: typeof(GreetingMadeHandlerAsync), onceOnly: true )] -- we are using a global inbox, so need to be explicit!! - [RequestLoggingAsync(step: 1, timing: HandlerTiming.Before)] - [UsePolicyAsync(step:2, policy: Policies.Retry.EXPONENTIAL_RETRYPOLICYASYNC)] - public override async Task HandleAsync(GreetingMade @event, CancellationToken cancellationToken = default) + [UseInbox(step:0, contextKey: typeof(GreetingMadeHandler), onceOnly: true )] + [RequestLogging(step: 1, timing: HandlerTiming.Before)] + [UsePolicy(step:2, policy: Policies.Retry.EXPONENTIAL_RETRYPOLICY)] + public override GreetingMade Handle(GreetingMade @event) { var posts = new List(); - var tx = await _uow.Database.BeginTransactionAsync(cancellationToken); + var tx =_transactionProvider.GetTransaction(); try { var salutation = new Salutation(@event.Greeting); _uow.Salutations.Add(salutation); - - posts.Add(await _postBox.DepositPostAsync(new SalutationReceived(DateTimeOffset.Now), cancellationToken: cancellationToken)); - - await _uow.SaveChangesAsync(cancellationToken); - await tx.CommitAsync(cancellationToken); + posts.Add(_postBox.DepositPost( + new SalutationReceived(DateTimeOffset.Now), + _transactionProvider) + ); + + _uow.SaveChanges(); + + _transactionProvider.Commit(); } catch (Exception e) { Console.WriteLine(e); - - await tx.RollbackAsync(cancellationToken); - + + _transactionProvider.Rollback(); + Console.WriteLine("Salutation analytical record not saved"); throw; } + finally + { + _transactionProvider.Close(); + } - await _postBox.ClearOutboxAsync(posts, cancellationToken: cancellationToken); + _postBox.ClearOutbox(posts.ToArray()); - return await base.HandleAsync(@event, cancellationToken); + return base.Handle(@event); } } } diff --git a/samples/WebAPI_EFCore/SalutationPorts/Policies/Retry.cs b/samples/WebAPI_EFCore/SalutationPorts/Policies/Retry.cs index 4db47aa42d..40ec53c32b 100644 --- a/samples/WebAPI_EFCore/SalutationPorts/Policies/Retry.cs +++ b/samples/WebAPI_EFCore/SalutationPorts/Policies/Retry.cs @@ -7,21 +7,21 @@ namespace SalutationPorts.Policies { public static class Retry { - public const string RETRYPOLICYASYNC = "SalutationPorts.Policies.RetryPolicyAsync"; - public const string EXPONENTIAL_RETRYPOLICYASYNC = "SalutationPorts.Policies.ExponenttialRetryPolicyAsync"; + public const string RETRYPOLICY = "SalutationPorts.Policies.RetryPolicyAsync"; + public const string EXPONENTIAL_RETRYPOLICY = "SalutationPorts.Policies.ExponenttialRetryPolicyAsync"; - public static AsyncRetryPolicy GetSimpleHandlerRetryPolicy() + public static RetryPolicy GetSimpleHandlerRetryPolicy() { - return Policy.Handle().WaitAndRetryAsync(new[] + return Policy.Handle().WaitAndRetry(new[] { TimeSpan.FromMilliseconds(50), TimeSpan.FromMilliseconds(100), TimeSpan.FromMilliseconds(150) }); } - public static AsyncRetryPolicy GetExponentialHandlerRetryPolicy() + public static RetryPolicy GetExponentialHandlerRetryPolicy() { var delay = Backoff.ExponentialBackoff(TimeSpan.FromMilliseconds(100), retryCount: 5, fastFirst: true); - return Policy.Handle().WaitAndRetryAsync(delay); + return Policy.Handle().WaitAndRetry(delay); } } } diff --git a/samples/WebAPI_EFCore/SalutationPorts/Policies/SalutationPolicy.cs b/samples/WebAPI_EFCore/SalutationPorts/Policies/SalutationPolicy.cs index ddf21c324f..28024f22a7 100644 --- a/samples/WebAPI_EFCore/SalutationPorts/Policies/SalutationPolicy.cs +++ b/samples/WebAPI_EFCore/SalutationPorts/Policies/SalutationPolicy.cs @@ -11,8 +11,8 @@ public SalutationPolicy() private void AddSalutationPolicies() { - Add(Retry.RETRYPOLICYASYNC, Retry.GetSimpleHandlerRetryPolicy()); - Add(Retry.EXPONENTIAL_RETRYPOLICYASYNC, Retry.GetExponentialHandlerRetryPolicy()); + Add(Retry.RETRYPOLICY, Retry.GetSimpleHandlerRetryPolicy()); + Add(Retry.EXPONENTIAL_RETRYPOLICY, Retry.GetExponentialHandlerRetryPolicy()); } } } diff --git a/samples/WebAPI_EFCore/SalutationPorts/SalutationPorts.csproj b/samples/WebAPI_EFCore/SalutationPorts/SalutationPorts.csproj index 8b83a99c91..5b6e770d6d 100644 --- a/samples/WebAPI_EFCore/SalutationPorts/SalutationPorts.csproj +++ b/samples/WebAPI_EFCore/SalutationPorts/SalutationPorts.csproj @@ -1,7 +1,7 @@ - net6.0 + net7.0 diff --git a/samples/WebAPI_EFCore/Salutations_MySqlMigrations/Salutations_MySqlMigrations.csproj b/samples/WebAPI_EFCore/Salutations_MySqlMigrations/Salutations_MySqlMigrations.csproj index 9c08384194..dc6d4eb4c4 100644 --- a/samples/WebAPI_EFCore/Salutations_MySqlMigrations/Salutations_MySqlMigrations.csproj +++ b/samples/WebAPI_EFCore/Salutations_MySqlMigrations/Salutations_MySqlMigrations.csproj @@ -1,7 +1,7 @@ - net6.0 + net7.0 diff --git a/samples/WebAPI_EFCore/Salutations_SqliteMigrations/Salutations_SqliteMigrations.csproj b/samples/WebAPI_EFCore/Salutations_SqliteMigrations/Salutations_SqliteMigrations.csproj index 23e2d9de53..7a766e756a 100644 --- a/samples/WebAPI_EFCore/Salutations_SqliteMigrations/Salutations_SqliteMigrations.csproj +++ b/samples/WebAPI_EFCore/Salutations_SqliteMigrations/Salutations_SqliteMigrations.csproj @@ -1,7 +1,7 @@ - net6.0 + net7.0 diff --git a/samples/WebAPI_EFCore/build.sh b/samples/WebAPI_EFCore/build.sh deleted file mode 100644 index a9522415f7..0000000000 --- a/samples/WebAPI_EFCore/build.sh +++ /dev/null @@ -1,15 +0,0 @@ -pushd GreetingsAdapters || exit -rm -rf out -dotnet restore -dotnet build -dotnet publish -c Release -o out -docker build . -popd || exit -pushd SalutationAnalytics || exit -rm -rf out -dotnet restore -dotnet build -dotnet publish -c Release -o out -docker build . -popd || exit - diff --git a/samples/WebAPI_EFCore/docker-compose.yml b/samples/WebAPI_EFCore/docker-compose.yml deleted file mode 100644 index 703b1a2372..0000000000 --- a/samples/WebAPI_EFCore/docker-compose.yml +++ /dev/null @@ -1,51 +0,0 @@ -version: '3.1' -services: - web: - build: ./GreetingsAdapters - hostname: greetingsapi - ports: - - "5000:5000" - environment: - - BRIGHTER_ConnectionStrings__Greetings=server=greetings_db; port=3306; uid=root; pwd=root; database=Greetings - - BRIGHTER_ConnectionStrings__GreetingsDb=server=greetings_db; port=3306; uid=root; pwd=root - - ASPNETCORE_ENVIRONMENT=Production - links: - - mysql:greetings_db - depends_on: - - mysql - - rabbitmq - worker: - build: ./GreetingsWatcher - hostname: greetingsworker - environment: - - ASPNETCORE_ENVIRONMENT=Production - depends_on: - - rabbitmq - mysql: - hostname: greetings_db - image: mysql - ports: - - "3306:3306" - security_opt: - - seccomp:unconfined - volumes: - - my-db:/var/lib/mysql - environment: - MYSQL_ROOT_PASSWORD: "root" - healthcheck: - test: mysqladmin ping -h localhost -p$$MYSQL_ROOT_PASSWORD && test '0' -eq $$(ps aux | awk '{print $$11}' | grep -c -e '^mysql$$') - rabbitmq: - image: brightercommand/rabbitmq:3.8-management-delay - ports: - - "5672:5672" - - "15672:15672" - volumes: - - rabbitmq-home:/var/lib/rabbitmq - -volumes: - rabbitmq-home: - driver: local - my-db: - driver: local - - diff --git a/samples/WebApiWithWorkerAndSweeper/Orders.API/Program.cs b/samples/WebApiWithWorkerAndSweeper/Orders.API/Program.cs index a264fb3554..549609d28a 100644 --- a/samples/WebApiWithWorkerAndSweeper/Orders.API/Program.cs +++ b/samples/WebApiWithWorkerAndSweeper/Orders.API/Program.cs @@ -1,7 +1,9 @@ using System.ComponentModel; +using System.Data.Common; using System.Reflection; using System.Text.Json; using System.Text.Json.Serialization; +using System.Transactions; using Orders.Data; using Paramore.Brighter.MessagingGateway.AzureServiceBus; using Paramore.Brighter.MsSql; @@ -31,7 +33,13 @@ var asbConnection = new ServiceBusVisualStudioCredentialClientProvider(asbEndpoint); -var outboxConfig = new MsSqlConfiguration(dbConnString, "BrighterOutbox"); +var outboxConfig = new RelationalDatabaseConfiguration(dbConnString, outBoxTableName: "BrighterOutbox"); + +var producerRegistry = new AzureServiceBusProducerRegistryFactory( + asbConnection, + new AzureServiceBusPublication[] { new() { Topic = new RoutingKey(NewOrderVersionEvent.Topic) }, } + ) + .Create(); builder.Services .AddBrighter(opt => @@ -39,18 +47,15 @@ opt.PolicyRegistry = new DefaultPolicy(); opt.CommandProcessorLifetime = ServiceLifetime.Scoped; }) - .UseExternalBus( - new AzureServiceBusProducerRegistryFactory( - asbConnection, - new AzureServiceBusPublication[] { new() { Topic = new RoutingKey(NewOrderVersionEvent.Topic) }, } - ) - .Create() + .UseExternalBus((configure) => + { + configure.ProducerRegistry = producerRegistry; + configure.Outbox = new MsSqlOutbox(outboxConfig); + configure.TransactionProvider = typeof(MsSqlUnitOfWork); + } ) - .UseMsSqlOutbox(outboxConfig, typeof(MsSqlSqlAuthConnectionProvider)) - .UseMsSqlTransactionConnectionProvider(typeof(SqlConnectionProvider)) .AutoFromAssemblies(Assembly.GetAssembly(typeof(NewOrderVersionEvent))); - builder.Services.AddControllersWithViews().AddJsonOptions(options => { options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter(allowIntegerValues: false)); diff --git a/samples/WebApiWithWorkerAndSweeper/Orders.Data/SqlConnectionProvider.cs b/samples/WebApiWithWorkerAndSweeper/Orders.Data/SqlConnectionProvider.cs index eaead8234a..fcaf04521d 100644 --- a/samples/WebApiWithWorkerAndSweeper/Orders.Data/SqlConnectionProvider.cs +++ b/samples/WebApiWithWorkerAndSweeper/Orders.Data/SqlConnectionProvider.cs @@ -1,10 +1,10 @@ +using System.Data.Common; using Microsoft.Data.SqlClient; -using Orders.Domain; -using Paramore.Brighter.MsSql; +using Paramore.Brighter; namespace Orders.Data; -public class SqlConnectionProvider : IMsSqlTransactionConnectionProvider +public class SqlConnectionProvider : RelationalDbConnectionProvider { private readonly SqlUnitOfWork _sqlConnection; @@ -13,21 +13,16 @@ public SqlConnectionProvider(SqlUnitOfWork sqlConnection) _sqlConnection = sqlConnection; } - public SqlConnection GetConnection() + public override DbConnection GetConnection() { return _sqlConnection.Connection; } - public Task GetConnectionAsync(CancellationToken cancellationToken = default) - { - return Task.FromResult(_sqlConnection.Connection); - } - - public SqlTransaction? GetTransaction() + public override DbTransaction GetTransaction() { return _sqlConnection.Transaction; } - public bool HasOpenTransaction { get => _sqlConnection.Transaction != null; } - public bool IsSharedConnection { get => true; } + public override bool HasOpenTransaction { get => _sqlConnection.Transaction != null; } + public override bool IsSharedConnection { get => true; } } diff --git a/samples/WebApiWithWorkerAndSweeper/Orders.Sweeper/Extensions/BrighterExtensions.cs b/samples/WebApiWithWorkerAndSweeper/Orders.Sweeper/Extensions/BrighterExtensions.cs index 98397646d1..1c2669dd60 100644 --- a/samples/WebApiWithWorkerAndSweeper/Orders.Sweeper/Extensions/BrighterExtensions.cs +++ b/samples/WebApiWithWorkerAndSweeper/Orders.Sweeper/Extensions/BrighterExtensions.cs @@ -1,3 +1,5 @@ +using System.Data.Common; +using System.Transactions; using Azure.Identity; using Orders.Sweeper.Settings; using Paramore.Brighter; @@ -32,28 +34,32 @@ public static WebApplicationBuilder AddBrighter(this WebApplicationBuilder build new() {MakeChannels = OnMissingChannel.Validate, Topic = new RoutingKey("default")} }, boxSettings.BatchChunkSize).Create(); - var outboxSettings = new MsSqlConfiguration(boxSettings.ConnectionString, boxSettings.OutboxTableName); - Type outboxType; + var outboxSettings = new RelationalDatabaseConfiguration(boxSettings.ConnectionString, outBoxTableName: boxSettings.OutboxTableName); + Type transactionProviderType; if (boxSettings.UseMsi) { if (environmentName != null && environmentName.Equals(_developmentEnvironemntName, StringComparison.InvariantCultureIgnoreCase)) { - outboxType = typeof(MsSqlVisualStudioConnectionProvider); + transactionProviderType = typeof(MsSqlVisualStudioConnectionProvider); } else { - outboxType = typeof(MsSqlDefaultAzureConnectionProvider); + transactionProviderType = typeof(MsSqlDefaultAzureConnectionProvider); } } else { - outboxType = typeof(MsSqlSqlAuthConnectionProvider); + transactionProviderType = typeof(MsSqlConnectionProvider); } builder.Services.AddBrighter() - .UseExternalBus(producerRegistry) - .UseMsSqlOutbox(outboxSettings, outboxType) + .UseExternalBus((configure) => + { + configure.ProducerRegistry = producerRegistry; + configure.Outbox = new MsSqlOutbox(outboxSettings); + configure.TransactionProvider = transactionProviderType; + }) .UseOutboxSweeper(options => { options.TimerInterval = boxSettings.OutboxSweeperInterval; diff --git a/samples/WebApiWithWorkerAndSweeper/Orders.Sweeper/Extensions/HealthCheckExtensions.cs b/samples/WebApiWithWorkerAndSweeper/Orders.Sweeper/Extensions/HealthCheckExtensions.cs index 0ebde0eee8..0f9599f27b 100644 --- a/samples/WebApiWithWorkerAndSweeper/Orders.Sweeper/Extensions/HealthCheckExtensions.cs +++ b/samples/WebApiWithWorkerAndSweeper/Orders.Sweeper/Extensions/HealthCheckExtensions.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.Diagnostics.HealthChecks; using Orders.Sweeper.HealthChecks; +using Paramore.Brighter; using Paramore.Brighter.MsSql; namespace Orders.Sweeper.Extensions; @@ -9,7 +10,7 @@ public static class HealthCheckExtensions public static IHealthChecksBuilder AddBrighterOutbox(this IHealthChecksBuilder builder) { return builder.Add(new HealthCheckRegistration("Brighter Outbox", - sp => new BrighterOutboxConnectionHealthCheck(sp.GetService()), + sp => new BrighterOutboxConnectionHealthCheck(sp.GetService()), HealthStatus.Unhealthy, null, TimeSpan.FromSeconds(15))); diff --git a/samples/WebApiWithWorkerAndSweeper/Orders.Sweeper/HealthChecks/BrighterOutboxConnectionHealthCheck.cs b/samples/WebApiWithWorkerAndSweeper/Orders.Sweeper/HealthChecks/BrighterOutboxConnectionHealthCheck.cs index afe490604b..813d1fe8dc 100644 --- a/samples/WebApiWithWorkerAndSweeper/Orders.Sweeper/HealthChecks/BrighterOutboxConnectionHealthCheck.cs +++ b/samples/WebApiWithWorkerAndSweeper/Orders.Sweeper/HealthChecks/BrighterOutboxConnectionHealthCheck.cs @@ -1,36 +1,30 @@ using System.Data; using Microsoft.Extensions.Diagnostics.HealthChecks; +using Paramore.Brighter; using Paramore.Brighter.MsSql; namespace Orders.Sweeper.HealthChecks; public class BrighterOutboxConnectionHealthCheck : IHealthCheck { - private readonly IMsSqlConnectionProvider _connectionProvider; + private readonly IAmARelationalDbConnectionProvider _connectionProvider; - public BrighterOutboxConnectionHealthCheck(IMsSqlConnectionProvider connectionProvider) + public BrighterOutboxConnectionHealthCheck(IAmARelationalDbConnectionProvider connectionProvider) { _connectionProvider = connectionProvider; } - public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = new CancellationToken()) + public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) { try { - var connection = await _connectionProvider.GetConnectionAsync(cancellationToken); + await using var connection = await _connectionProvider.GetConnectionAsync(cancellationToken); - await connection.OpenAsync(cancellationToken); - - if (connection.State != ConnectionState.Open) await connection.OpenAsync(cancellationToken); var command = connection.CreateCommand(); - if (_connectionProvider.HasOpenTransaction) command.Transaction = _connectionProvider.GetTransaction(); command.CommandText = "SELECT 1;"; await command.ExecuteScalarAsync(cancellationToken); - if (!_connectionProvider.IsSharedConnection) connection.Dispose(); - else if (!_connectionProvider.HasOpenTransaction) connection.Close(); - return HealthCheckResult.Healthy(); } catch (Exception ex) diff --git a/samples/WebApiWithWorkerAndSweeper/Orders.Sweeper/Settings/BrighterBoxSettings.cs b/samples/WebApiWithWorkerAndSweeper/Orders.Sweeper/Settings/BrighterBoxSettings.cs index ce5408effd..bc94eed512 100644 --- a/samples/WebApiWithWorkerAndSweeper/Orders.Sweeper/Settings/BrighterBoxSettings.cs +++ b/samples/WebApiWithWorkerAndSweeper/Orders.Sweeper/Settings/BrighterBoxSettings.cs @@ -9,6 +9,7 @@ public class BrighterBoxSettings public int OutboxSweeperInterval { get; set; } = 5; public bool UseMsi { get; set; } = true; + public string ConnectionString { get; set; } public int MinimumMessageAge { get; set; } = 5000; diff --git a/samples/WebApiWithWorkerAndSweeper/Orders.Worker/Program.cs b/samples/WebApiWithWorkerAndSweeper/Orders.Worker/Program.cs index d546ebc93b..ed42d85cd5 100644 --- a/samples/WebApiWithWorkerAndSweeper/Orders.Worker/Program.cs +++ b/samples/WebApiWithWorkerAndSweeper/Orders.Worker/Program.cs @@ -50,7 +50,7 @@ -var outboxConfig = new MsSqlConfiguration(dbConnString, "BrighterOutbox"); +var outboxConfig = new RelationalDatabaseConfiguration(dbConnString, outBoxTableName: "BrighterOutbox"); //TODO: add your ASB qualified name here var clientProvider = new ServiceBusVisualStudioCredentialClientProvider(".servicebus.windows.net"); @@ -62,8 +62,7 @@ options.ChannelFactory = new AzureServiceBusChannelFactory(asbConsumerFactory); options.UseScoped = true; - }).UseMsSqlOutbox(outboxConfig, typeof(MsSqlSqlAuthConnectionProvider)) - .UseMsSqlTransactionConnectionProvider(typeof(SqlConnectionProvider)) + }) .AutoFromAssemblies(Assembly.GetAssembly(typeof(CreateOrderCommand))); builder.Services.AddHostedService(); diff --git a/src/Paramore.Brighter.Dapper/IUnitOfWork.cs b/src/Paramore.Brighter.Dapper/IUnitOfWork.cs deleted file mode 100644 index 642c18fc1f..0000000000 --- a/src/Paramore.Brighter.Dapper/IUnitOfWork.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using System.Data.Common; -using System.Threading; -using System.Threading.Tasks; - -namespace Paramore.Brighter.Dapper -{ - /// - /// Creates a unit of work, so that Brighter can access the active transaction for the Outbox - /// - public interface IUnitOfWork : IAmABoxTransactionConnectionProvider, IDisposable - { - /// - /// Begins a new transaction against the database. Will open the connection if it is not already open, - /// - /// A transaction - DbTransaction BeginOrGetTransaction(); - - /// - /// Begins a new transaction asynchronously against the database. Will open the connection if it is not already open, - /// - /// - /// A transaction - Task BeginOrGetTransactionAsync(CancellationToken cancellationToken); - - /// - /// Commits any pending transactions - /// - void Commit(); - - /// - /// The .NET DbConnection to the Database - /// - DbConnection Database { get; } - - /// - /// Is there an extant transaction - /// - /// True if a transaction is already open on this unit of work, false otherwise - bool HasTransaction(); - } -} diff --git a/src/Paramore.Brighter.DynamoDb/DynamoDbTableBuilder.cs b/src/Paramore.Brighter.DynamoDb/DynamoDbTableBuilder.cs index 8010b27aa3..b225b45fc5 100644 --- a/src/Paramore.Brighter.DynamoDb/DynamoDbTableBuilder.cs +++ b/src/Paramore.Brighter.DynamoDb/DynamoDbTableBuilder.cs @@ -49,7 +49,7 @@ public async Task EnsureTablesDeleted(IEnumerable tableNames, Cancellati Dictionary tableResults = null; do { - var tableQuery = new DynampDbTableQuery(); + var tableQuery = new DynamoDbTableQuery(); tableResults = await tableQuery.HasTables(_client, tableNames, ct: ct); } while (tableResults.Any(tr => tr.Value)); } diff --git a/src/Paramore.Brighter.DynamoDb/DynampDbTableQuery.cs b/src/Paramore.Brighter.DynamoDb/DynamoDbTableQuery.cs similarity index 97% rename from src/Paramore.Brighter.DynamoDb/DynampDbTableQuery.cs rename to src/Paramore.Brighter.DynamoDb/DynamoDbTableQuery.cs index 700043d013..f17ebc4b07 100644 --- a/src/Paramore.Brighter.DynamoDb/DynampDbTableQuery.cs +++ b/src/Paramore.Brighter.DynamoDb/DynamoDbTableQuery.cs @@ -7,7 +7,7 @@ namespace Paramore.Brighter.DynamoDb { - public class DynampDbTableQuery + public class DynamoDbTableQuery { public async Task> HasTables( IAmazonDynamoDB client, diff --git a/src/Paramore.Brighter.DynamoDb/DynamoDbUnitOfWork.cs b/src/Paramore.Brighter.DynamoDb/DynamoDbUnitOfWork.cs index 17cd772623..eb56f7cc90 100644 --- a/src/Paramore.Brighter.DynamoDb/DynamoDbUnitOfWork.cs +++ b/src/Paramore.Brighter.DynamoDb/DynamoDbUnitOfWork.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Net; using System.Threading; using System.Threading.Tasks; using Amazon.DynamoDBv2; @@ -7,7 +8,7 @@ namespace Paramore.Brighter.DynamoDb { - public class DynamoDbUnitOfWork : IDynamoDbClientTransactionProvider, IDisposable + public class DynamoDbUnitOfWork : IAmADynamoDbTransactionProvider, IDisposable { private TransactWriteItemsRequest _tx; @@ -15,12 +16,56 @@ public class DynamoDbUnitOfWork : IDynamoDbClientTransactionProvider, IDisposabl /// The AWS client for dynamoDb /// public IAmazonDynamoDB DynamoDb { get; } + + /// + /// The response for the last transaction commit + /// + public TransactWriteItemsResponse LastResponse { get; set; } public DynamoDbUnitOfWork(IAmazonDynamoDB dynamoDb) { DynamoDb = dynamoDb; } + public void Close() + { + _tx = null; + } + + /// + /// Commit a transaction, performing all associated write actions + /// + public void Commit() + { + if (!HasOpenTransaction) + throw new InvalidOperationException("No transaction to commit"); + + LastResponse = DynamoDb.TransactWriteItemsAsync(_tx).GetAwaiter().GetResult(); + + } + + /// + /// Commit a transaction, performing all associated write actions + /// + /// A cancellation token + /// + public async Task CommitAsync(CancellationToken ct = default) + { + if (!HasOpenTransaction) + throw new InvalidOperationException("No transaction to commit"); + try + { + LastResponse = await DynamoDb.TransactWriteItemsAsync(_tx, ct); + if (LastResponse.HttpStatusCode != HttpStatusCode.OK) + throw new InvalidOperationException($"HTTP error writing to DynamoDb {LastResponse.HttpStatusCode}"); + } + catch (AmazonDynamoDBException e) + { + throw new InvalidOperationException($"HTTP error writing to DynamoDb {e.Message}"); + } + } + + /// /// Begin a transaction if one has not been started, otherwise return the extant transaction /// We populate the TransactItems member with an empty list, so you do not need to create your own list @@ -28,52 +73,35 @@ public DynamoDbUnitOfWork(IAmazonDynamoDB dynamoDb) /// i.e. tx.TransactItems.Add(new TransactWriteItem(... /// /// - public TransactWriteItemsRequest BeginOrGetTransaction() + public TransactWriteItemsRequest GetTransaction() { - if (HasTransaction()) - { - return _tx; - } - else + if (HasOpenTransaction) { - _tx = new TransactWriteItemsRequest(); - _tx.TransactItems = new List(); return _tx; } + + _tx = new TransactWriteItemsRequest(); + _tx.TransactItems = new List(); + return _tx; } - /// - /// Commit a transaction, performing all associated write actions - /// - /// A response indicating the status of the transaction - public TransactWriteItemsResponse Commit() + public Task GetTransactionAsync(CancellationToken cancellationToken = default) { - if (!HasTransaction()) - throw new InvalidOperationException("No transaction to commit"); - - return DynamoDb.TransactWriteItemsAsync(_tx).GetAwaiter().GetResult(); + var tcs = new TaskCompletionSource(); + tcs.SetResult(GetTransaction()); + return tcs.Task; } - /// - /// Commit a transaction, performing all associated write actions + /// + /// Is there an existing transaction? /// - /// A response indicating the status of the transaction - public async Task CommitAsync(CancellationToken ct = default) - { - if (!HasTransaction()) - throw new InvalidOperationException("No transaction to commit"); - - return await DynamoDb.TransactWriteItemsAsync(_tx, ct); - } + /// + public bool HasOpenTransaction => _tx != null; /// - /// Is there an existing transaction + /// Is there a shared connection, not true, but we do not manage the DynamoDb client /// - /// - public bool HasTransaction() - { - return _tx != null; - } + public bool IsSharedConnection => false; /// /// Clear any transaction @@ -83,12 +111,18 @@ public void Rollback() _tx = null; } + public Task RollbackAsync(CancellationToken cancellationToken = default) + { + Rollback(); + return Task.CompletedTask; + } + /// /// Clear any transaction. Does not kill any client to DynamoDb as we assume that we don't own it. /// public void Dispose() { - if (HasTransaction()) + if (HasOpenTransaction) _tx = null; } } diff --git a/src/Paramore.Brighter.DynamoDb/IAmADynamoDbConnectionProvider.cs b/src/Paramore.Brighter.DynamoDb/IAmADynamoDbConnectionProvider.cs new file mode 100644 index 0000000000..29117af24b --- /dev/null +++ b/src/Paramore.Brighter.DynamoDb/IAmADynamoDbConnectionProvider.cs @@ -0,0 +1,15 @@ +using Amazon.DynamoDBv2; + +namespace Paramore.Brighter.DynamoDb +{ + /// + /// Provides the dynamo db connection we are using, base of any unit of work + /// + public interface IAmADynamoDbConnectionProvider + { + /// + /// The AWS client for dynamoDb + /// + IAmazonDynamoDB DynamoDb { get; } + } +} diff --git a/src/Paramore.Brighter.DynamoDb/IAmADynamoDbTransactionProvider.cs b/src/Paramore.Brighter.DynamoDb/IAmADynamoDbTransactionProvider.cs new file mode 100644 index 0000000000..37757444be --- /dev/null +++ b/src/Paramore.Brighter.DynamoDb/IAmADynamoDbTransactionProvider.cs @@ -0,0 +1,10 @@ +using System.Threading; +using System.Threading.Tasks; +using Amazon.DynamoDBv2.Model; + +namespace Paramore.Brighter.DynamoDb +{ + public interface IAmADynamoDbTransactionProvider : IAmADynamoDbConnectionProvider, IAmABoxTransactionProvider + { + } +} diff --git a/src/Paramore.Brighter.DynamoDb/IDynamoDbClientProvider.cs b/src/Paramore.Brighter.DynamoDb/IDynamoDbClientProvider.cs deleted file mode 100644 index 86b0a887d6..0000000000 --- a/src/Paramore.Brighter.DynamoDb/IDynamoDbClientProvider.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; -using Amazon.DynamoDBv2; -using Amazon.DynamoDBv2.Model; - -namespace Paramore.Brighter.DynamoDb -{ - public interface IDynamoDbClientProvider - { - /// - /// The AWS client for dynamoDb - /// - IAmazonDynamoDB DynamoDb { get; } - - /// - /// Begin a transaction if one has not been started, otherwise return the extant transaction - /// - /// - TransactWriteItemsRequest BeginOrGetTransaction(); - - /// - /// Commit a transaction, performing all associated write actions - /// - /// A response indicating the status of the transaction - TransactWriteItemsResponse Commit(); - - /// - /// Commit a transaction, performing all associated write actions - /// - /// The cancellation token for the task - /// A response indicating the status of the transaction - Task CommitAsync(CancellationToken ct); - - /// - /// Is there an existing transaction - /// - /// - bool HasTransaction(); - - /// - /// Clear any transaction - /// - void Rollback(); - } -} - diff --git a/src/Paramore.Brighter.DynamoDb/IDynamoDbTransactionProvider.cs b/src/Paramore.Brighter.DynamoDb/IDynamoDbTransactionProvider.cs deleted file mode 100644 index 90dad7735e..0000000000 --- a/src/Paramore.Brighter.DynamoDb/IDynamoDbTransactionProvider.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Paramore.Brighter.DynamoDb -{ - public interface IDynamoDbClientTransactionProvider : IDynamoDbClientProvider, IAmABoxTransactionConnectionProvider - { - - } -} diff --git a/src/Paramore.Brighter.Extensions.DependencyInjection/BrighterOptions.cs b/src/Paramore.Brighter.Extensions.DependencyInjection/BrighterOptions.cs index 77d5ec5b02..6b92ee7a2c 100644 --- a/src/Paramore.Brighter.Extensions.DependencyInjection/BrighterOptions.cs +++ b/src/Paramore.Brighter.Extensions.DependencyInjection/BrighterOptions.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.DependencyInjection; +using Paramore.Brighter.FeatureSwitch; using Polly.Registry; namespace Paramore.Brighter.Extensions.DependencyInjection @@ -6,14 +7,15 @@ namespace Paramore.Brighter.Extensions.DependencyInjection public class BrighterOptions : IBrighterOptions { /// - /// Used to create a channel, an abstraction over a message processing pipeline + /// Configures the life time of the Command Processor. Defaults to Transient. /// - public IAmAChannelFactory ChannelFactory { get; set; } + public ServiceLifetime CommandProcessorLifetime { get; set; } = ServiceLifetime.Transient; /// - /// Configures the life time of the Command Processor. Defaults to Transient. + /// Do we support feature switching? In which case please supply an initialized feature switch registry /// - public ServiceLifetime CommandProcessorLifetime { get; set; } = ServiceLifetime.Transient; + /// + public IAmAFeatureSwitchRegistry FeatureSwitchRegistry { get; set; } = null; /// /// Configures the lifetime of the Handlers. Defaults to Scoped. @@ -26,7 +28,7 @@ public class BrighterOptions : IBrighterOptions public ServiceLifetime MapperLifetime { get; set; } = ServiceLifetime.Singleton; /// - /// Configures the polly policy registry. + /// Configures the polly policy registry. /// public IPolicyRegistry PolicyRegistry { get; set; } = new DefaultPolicy(); @@ -39,21 +41,24 @@ public class BrighterOptions : IBrighterOptions /// Configures the lifetime of any transformers. Defaults to Singleton /// public ServiceLifetime TransformerLifetime { get; set; } = ServiceLifetime.Singleton; + + } public interface IBrighterOptions { - /// - /// Used to create a channel, an abstraction over a message processing pipeline - /// - IAmAChannelFactory ChannelFactory { get; set; } - /// /// Configures the life time of the Command Processor. /// ServiceLifetime CommandProcessorLifetime { get; set; } /// + /// Do we support feature switching? In which case please supply an initialized feature switch registry + /// + /// + IAmAFeatureSwitchRegistry FeatureSwitchRegistry { get; set; } + + /// /// Configures the lifetime of the Handlers. /// ServiceLifetime HandlerLifetime { get; set; } @@ -77,5 +82,6 @@ public interface IBrighterOptions /// Configures the lifetime of any transformers. /// ServiceLifetime TransformerLifetime { get; set; } - } + + } } diff --git a/src/Paramore.Brighter.Extensions.DependencyInjection/IBrighterBuilder.cs b/src/Paramore.Brighter.Extensions.DependencyInjection/IBrighterBuilder.cs index ad016f94fb..c4e2fd64e9 100644 --- a/src/Paramore.Brighter.Extensions.DependencyInjection/IBrighterBuilder.cs +++ b/src/Paramore.Brighter.Extensions.DependencyInjection/IBrighterBuilder.cs @@ -22,12 +22,10 @@ THE SOFTWARE. */ #endregion - using System; -using System.Collections; -using System.Collections.Generic; using System.Reflection; using Microsoft.Extensions.DependencyInjection; +using Polly.Registry; namespace Paramore.Brighter.Extensions.DependencyInjection { @@ -85,7 +83,6 @@ public interface IBrighterBuilder /// This builder, allows chaining calls IBrighterBuilder MapperRegistryFromAssemblies(params Assembly[] assemblies); - /// /// Scan the assemblies for implementations of IAmAMessageTransformAsync and register them with ServiceCollection /// @@ -93,9 +90,17 @@ public interface IBrighterBuilder /// This builder, allows chaining calls IBrighterBuilder TransformsFromAssemblies(params Assembly[] assemblies); + /// + /// The policy registry to use for the command processor and the event bus + /// It needs to be here as we need to pass it between AddBrighter and UseExternalBus + /// + IPolicyRegistry PolicyRegistry { get; set; } + + /// /// The IoC container to populate /// IServiceCollection Services { get; } + } } diff --git a/src/Paramore.Brighter.Extensions.DependencyInjection/Paramore.Brighter.Extensions.DependencyInjection.csproj b/src/Paramore.Brighter.Extensions.DependencyInjection/Paramore.Brighter.Extensions.DependencyInjection.csproj index 6649daf21c..5cde23b269 100644 --- a/src/Paramore.Brighter.Extensions.DependencyInjection/Paramore.Brighter.Extensions.DependencyInjection.csproj +++ b/src/Paramore.Brighter.Extensions.DependencyInjection/Paramore.Brighter.Extensions.DependencyInjection.csproj @@ -14,6 +14,7 @@ + diff --git a/src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionBrighterBuilder.cs b/src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionBrighterBuilder.cs index aa4fd8e706..6e0c371cc6 100644 --- a/src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionBrighterBuilder.cs +++ b/src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionBrighterBuilder.cs @@ -23,15 +23,13 @@ THE SOFTWARE. */ #endregion - using System; -using System.Collections; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Reflection; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; +using Polly.Registry; namespace Paramore.Brighter.Extensions.DependencyInjection { @@ -40,6 +38,8 @@ public class ServiceCollectionBrighterBuilder : IBrighterBuilder private readonly ServiceCollectionSubscriberRegistry _serviceCollectionSubscriberRegistry; private readonly ServiceCollectionMessageMapperRegistry _mapperRegistry; private readonly ServiceCollectionTransformerRegistry _transformerRegistry; + + public IPolicyRegistry PolicyRegistry { get; set; } /// /// Registers the components of Brighter pipelines @@ -48,16 +48,20 @@ public class ServiceCollectionBrighterBuilder : IBrighterBuilder /// The register for looking up message handlers /// The register for looking up message mappers /// The register for transforms + /// The list of policies that we require public ServiceCollectionBrighterBuilder( - IServiceCollection services, + IServiceCollection services, ServiceCollectionSubscriberRegistry serviceCollectionSubscriberRegistry, - ServiceCollectionMessageMapperRegistry mapperRegistry, - ServiceCollectionTransformerRegistry transformerRegistry = null) + ServiceCollectionMessageMapperRegistry mapperRegistry, + ServiceCollectionTransformerRegistry transformerRegistry = null, + IPolicyRegistry policyRegistry = null + ) { Services = services; _serviceCollectionSubscriberRegistry = serviceCollectionSubscriberRegistry; _mapperRegistry = mapperRegistry; _transformerRegistry = transformerRegistry ?? new ServiceCollectionTransformerRegistry(services); + PolicyRegistry = policyRegistry; } /// @@ -66,9 +70,35 @@ public ServiceCollectionBrighterBuilder( public IServiceCollection Services { get; } /// - /// Scan the assemblies provided for implementations of IHandleRequests, IHandleRequestsAsync, IAmAMessageMapper and register them with ServiceCollection + /// Scan the assemblies provided for implementations of IHandleRequestsAsync and register them with ServiceCollection + /// + /// A callback to register handlers + /// This builder, allows chaining calls + public IBrighterBuilder AsyncHandlers(Action registerHandlers) + { + if (registerHandlers == null) + throw new ArgumentNullException(nameof(registerHandlers)); + + registerHandlers(_serviceCollectionSubscriberRegistry); + + return this; + } + + /// + /// Scan the assemblies provided for implementations of IHandleRequests and register them with ServiceCollection /// /// The assemblies to scan + /// This builder, allows chaining calls + public IBrighterBuilder AsyncHandlersFromAssemblies(params Assembly[] assemblies) + { + RegisterHandlersFromAssembly(typeof(IHandleRequestsAsync<>), assemblies, typeof(IHandleRequestsAsync<>).Assembly); + return this; + } + + /// + /// Scan the assemblies provided for implementations of IHandleRequests, IHandleRequestsAsync, IAmAMessageMapper and register them with ServiceCollection + /// + /// The assemblies to scan /// public IBrighterBuilder AutoFromAssemblies(params Assembly[] extraAssemblies) { @@ -151,34 +181,6 @@ public IBrighterBuilder HandlersFromAssemblies(params Assembly[] assemblies) return this; } - - /// - /// Scan the assemblies provided for implementations of IHandleRequestsAsync and register them with ServiceCollection - /// - /// A callback to register handlers - /// This builder, allows chaining calls - public IBrighterBuilder AsyncHandlers(Action registerHandlers) - { - if (registerHandlers == null) - throw new ArgumentNullException(nameof(registerHandlers)); - - registerHandlers(_serviceCollectionSubscriberRegistry); - - return this; - } - - /// - /// Scan the assemblies provided for implementations of IHandleRequests and register them with ServiceCollection - /// - /// The assemblies to scan - /// This builder, allows chaining calls - public IBrighterBuilder AsyncHandlersFromAssemblies(params Assembly[] assemblies) - { - RegisterHandlersFromAssembly(typeof(IHandleRequestsAsync<>), assemblies, typeof(IHandleRequestsAsync<>).Assembly); - return this; - } - - /// /// Scan the assemblies for implementations of IAmAMessageTransformAsync and register them with the ServiceCollection /// @@ -204,7 +206,7 @@ from i in ti.ImplementedInterfaces return this; } - + private void RegisterHandlersFromAssembly(Type interfaceType, IEnumerable assemblies, Assembly assembly) { assemblies = assemblies.Concat(new[] { assembly }); diff --git a/src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionExtensions.cs b/src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionExtensions.cs index f9bd4a0a57..bcf76d72b8 100644 --- a/src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionExtensions.cs @@ -1,4 +1,5 @@ #region Licence + /* The MIT License (MIT) Copyright © 2022 Ian Cooper @@ -19,38 +20,41 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ - -#endregion +#endregion using System; -using System.Collections.Generic; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; using Paramore.Brighter.FeatureSwitch; using Paramore.Brighter.Logging; using System.Text.Json; +using System.Transactions; +using Paramore.Brighter.DynamoDb; +using Polly.Registry; namespace Paramore.Brighter.Extensions.DependencyInjection { public static class ServiceCollectionExtensions { - private static int _outboxBulkChunkSize = 100; - /// /// Will add Brighter into the .NET IoC Container - ServiceCollection - /// Registers singletons with the service collection :- + /// Registers the following with the service collection :- /// - BrighterOptions - how should we configure Brighter + /// - Feature Switch Registry - optional if features switch support is desired + /// - Inbox - defaults to InMemoryInbox if none supplied /// - SubscriberRegistry - what handlers subscribe to what requests /// - MapperRegistry - what mappers translate what messages - /// - InMemoryOutbox - Optional - if an in memory outbox is selected /// - /// The IoC container to update + /// The collection of services that we want to add registrations to /// A callback that defines what options to set when Brighter is built - /// A builder that can be used to populate the IoC container with handlers and mappers by inspection - used by built in factory from CommandProcessor + /// A builder that can be used to populate the IoC container with handlers and mappers by inspection + /// - used by built in factory from CommandProcessor /// Thrown if we have no IoC provided ServiceCollection - public static IBrighterBuilder AddBrighter(this IServiceCollection services, Action configure = null) + public static IBrighterBuilder AddBrighter( + this IServiceCollection services, + Action configure = null) { if (services == null) throw new ArgumentNullException(nameof(services)); @@ -61,189 +65,285 @@ public static IBrighterBuilder AddBrighter(this IServiceCollection services, Act return BrighterHandlerBuilder(services, options); } - + /// - /// Normally you want to call AddBrighter from client code, and not this method. Public only to support Service Activator extensions - /// Registers singletons with the service collection :- + /// This is public so that we can call it from + /// which allows that extension method to be called with a configuration + /// that derives from . + /// DON'T CALL THIS DIRECTLY + /// Registers the following with the service collection :- + /// - BrighterOptions - how should we configure Brighter + /// - Feature Switch Registry - optional if features switch support is desired + /// - Inbox - defaults to InMemoryInbox if none supplied /// - SubscriberRegistry - what handlers subscribe to what requests /// - MapperRegistry - what mappers translate what messages /// - /// The IoC container to update - /// A callback that defines what options to set when Brighter is built - /// A builder that can be used to populate the IoC container with handlers and mappers by inspection - used by built in factory from CommandProcessor + /// The collection of services that we want to add registrations to + /// + /// public static IBrighterBuilder BrighterHandlerBuilder(IServiceCollection services, BrighterOptions options) { var subscriberRegistry = new ServiceCollectionSubscriberRegistry(services, options.HandlerLifetime); - services.TryAddSingleton(subscriberRegistry); + services.TryAddSingleton(subscriberRegistry); var transformRegistry = new ServiceCollectionTransformerRegistry(services, options.TransformerLifetime); - services.TryAddSingleton(transformRegistry); - - services.TryAdd(new ServiceDescriptor(typeof(IAmACommandProcessor), BuildCommandProcessor, options.CommandProcessorLifetime)); + services.TryAddSingleton(transformRegistry); var mapperRegistry = new ServiceCollectionMessageMapperRegistry(services, options.MapperLifetime); - services.TryAddSingleton(mapperRegistry); + services.TryAddSingleton(mapperRegistry); + + if (options.FeatureSwitchRegistry != null) + services.TryAddSingleton(options.FeatureSwitchRegistry); - return new ServiceCollectionBrighterBuilder(services, subscriberRegistry, mapperRegistry, transformRegistry); + //Add the policy registry + IPolicyRegistry policyRegistry; + if (options.PolicyRegistry == null) policyRegistry = new DefaultPolicy(); + else policyRegistry = AddDefaults(options.PolicyRegistry); + + services.TryAdd(new ServiceDescriptor(typeof(IAmACommandProcessor), + (serviceProvider) => (IAmACommandProcessor)BuildCommandProcessor(serviceProvider), + options.CommandProcessorLifetime)); + + return new ServiceCollectionBrighterBuilder( + services, + subscriberRegistry, + mapperRegistry, + transformRegistry, + policyRegistry + ); } /// - /// Use an external Brighter Outbox to store messages Posted to another process (evicts based on age and size). - /// Advantages: By using the same Db to store both any state changes for your app, and outgoing messages you can create a transaction that spans both - /// your state change and writing to an outbox [use DepositPost to store]. Then a sweeper process can look for message not flagged as sent and send them. - /// For low latency just send after the transaction with ClearOutbox, for higher latency just let the sweeper run in the background. - /// The outstanding messages dispatched this way can be sent from any producer that runs a sweeper process and so it not tied to the lifetime of the - /// producer, offering guaranteed, at least once, delivery. - /// NOTE: there may be a database specific Use*OutBox available. If so, use that in preference to this generic method - /// If not null, registers singletons with the service collection :- - /// - IAmAnOutboxSync - what messages have we posted - /// - ImAnOutboxAsync - what messages have we posted (async pipeline compatible) + /// An external bus is the use of Message Oriented Middleware (MoM) to dispatch a message between a producer + /// and a consumer. The assumption is that this is being used for inter-process communication, for example the + /// work queue pattern for distributing work, or between microservicves + /// Registers singletons with the service collection :- + /// - An Event Bus - used to send message externally and contains: + /// -- Producer Registry - A list of producers we can send middleware messages with + /// -- Outbox - stores messages so that they can be written in the same transaction as entity writes + /// -- Outbox Transaction Provider - used to provide a transaction that spans the Outbox write and + /// your updates to your entities + /// -- RelationalDb Connection Provider - if your transaction provider is for a relational db we register this + /// interface to access your Db and make it available to your own classes + /// -- Transaction Connection Provider - if your transaction provider is also a relational db connection + /// provider it will implement this interface which inherits from both + /// -- External Bus Configuration - the configuration parameters for an external bus, mainly used internally + /// -- UseRpc - do we want to use RPC i.e. a command blocks waiting for a response, over middleware. /// /// The Brighter builder to add this option to - /// The outbox provider - if your outbox supports both sync and async options, just provide this and we will register both - /// - /// - public static IBrighterBuilder UseExternalOutbox(this IBrighterBuilder brighterBuilder, IAmAnOutbox outbox = null, int outboxBulkChunkSize = 100) + /// A callback that allows you to configure options + /// The transaction provider for the outbox, can be null for in-memory default + /// of which you must set the generic type to for + /// + /// The lifetime of the transaction provider + /// The Brighter builder to allow chaining of requests + public static IBrighterBuilder UseExternalBus( + this IBrighterBuilder brighterBuilder, + Action configure, + ServiceLifetime serviceLifetime = ServiceLifetime.Scoped) { - if (outbox is IAmAnOutboxSync) + if (brighterBuilder is null) + throw new ArgumentNullException($"{nameof(brighterBuilder)} cannot be null.", nameof(brighterBuilder)); + + var busConfiguration = new ExternalBusConfiguration(); + configure?.Invoke(busConfiguration); + brighterBuilder.Services.TryAddSingleton(busConfiguration); + + //default to using System Transactions if nothing provided, so we always technically can share the outbox transaction + Type transactionProvider = busConfiguration.TransactionProvider ?? typeof(CommittableTransactionProvider); + + //Find the transaction type from the provider + Type transactionProviderInterface = typeof(IAmABoxTransactionProvider<>); + Type transactionType = null; + foreach (Type i in transactionProvider.GetInterfaces()) + if (i.IsGenericType && i.GetGenericTypeDefinition() == transactionProviderInterface) + transactionType = i.GetGenericArguments()[0]; + + if (transactionType == null) + throw new ConfigurationException( + $"Unable to register provider of type {transactionProvider.Name}. It does not implement {typeof(IAmABoxTransactionProvider<>).Name}."); + + //register the generic interface with the transaction type + var boxProviderType = transactionProviderInterface.MakeGenericType(transactionType); + + brighterBuilder.Services.Add(new ServiceDescriptor(boxProviderType, transactionProvider, serviceLifetime)); + + //NOTE: It is a little unsatisfactory to hard code our types in here + RegisterRelationalProviderServicesMaybe(brighterBuilder, busConfiguration.ConnectionProvider, transactionProvider, serviceLifetime); + RegisterDynamoProviderServicesMaybe(brighterBuilder, busConfiguration.ConnectionProvider, transactionProvider, serviceLifetime); + + return ExternalBusBuilder(brighterBuilder, busConfiguration, transactionType); + } + + private static INeedARequestContext AddEventBus( + IServiceProvider provider, + INeedMessaging messagingBuilder, + IUseRpc useRequestResponse) + { + var eventBus = provider.GetService(); + var eventBusConfiguration = provider.GetService(); + var messageMapperRegistry = MessageMapperRegistry(provider); + var messageTransformFactory = TransformFactory(provider); + + INeedARequestContext ret = null; + var hasEventBus = eventBus != null; + bool useRpc = useRequestResponse != null && useRequestResponse.RPC; + + if (!hasEventBus) ret = messagingBuilder.NoExternalBus(); + + if (hasEventBus && !useRpc) { - brighterBuilder.Services.TryAdd(new ServiceDescriptor(typeof(IAmAnOutboxSync), _ => outbox, ServiceLifetime.Singleton)); + ret = messagingBuilder.ExternalBus( + ExternalBusType.FireAndForget, + eventBus, + messageMapperRegistry, + messageTransformFactory, + eventBusConfiguration.ResponseChannelFactory, + eventBusConfiguration.ReplyQueueSubscriptions); } - - if (outbox is IAmAnOutboxAsync) + + if (hasEventBus && useRpc) { - brighterBuilder.Services.TryAdd(new ServiceDescriptor(typeof(IAmAnOutboxAsync), _ => outbox, ServiceLifetime.Singleton)); + ret = messagingBuilder.ExternalBus( + ExternalBusType.RPC, + eventBus, + messageMapperRegistry, + messageTransformFactory, + eventBusConfiguration.ResponseChannelFactory, + eventBusConfiguration.ReplyQueueSubscriptions + ); } - _outboxBulkChunkSize = outboxBulkChunkSize; - - return brighterBuilder; - + return ret; } - /// - /// Uses an external Brighter Inbox to record messages received to allow "once only" or diagnostics (how did we get here?) - /// Advantages: by using an external inbox then you can share "once only" across multiple threads/processes and support a competing consumer - /// model; an internal inbox is useful for testing but outside of single consumer scenarios won't work as intended - /// If not null, registers singletons with the service collection :- - /// - IAmAnInboxSync - what messages have we received - /// - IAmAnInboxAsync - what messages have we received (async pipeline compatible) - /// - /// Extension method to support a fluent interface - /// The external inbox to use - /// If this is null, configure by hand, if not, will auto-add inbox to handlers - /// - public static IBrighterBuilder UseExternalInbox( - this IBrighterBuilder brighterBuilder, - IAmAnInbox inbox, InboxConfiguration inboxConfiguration = null, - ServiceLifetime serviceLifetime = ServiceLifetime.Scoped) - { - if (inbox is IAmAnInboxSync) - { - brighterBuilder.Services.TryAdd(new ServiceDescriptor(typeof(IAmAnInboxSync), _ => inbox, serviceLifetime)); - } - - if (inbox is IAmAnInboxAsync) - { - brighterBuilder.Services.TryAdd(new ServiceDescriptor(typeof(IAmAnInboxAsync), _ => inbox, serviceLifetime)); - } - - if (inboxConfiguration != null) - { - brighterBuilder.Services.TryAddSingleton(inboxConfiguration); - } - - return brighterBuilder; - } - - /// - /// Use the Brighter In-Memory Outbox to store messages Posted to another process (evicts based on age and size). - /// Advantages: fast and no additional infrastructure required - /// Disadvantages: The Outbox will not survive restarts, so messages not published by shutdown will not be flagged as not posted - /// Registers singletons with the service collection :- - /// - InMemoryOutboxSync - what messages have we posted - /// - InMemoryOutboxAsync - what messages have we posted (async pipeline compatible) - /// - /// The builder we are adding this facility to - /// The Brighter builder to allow chaining of requests - public static IBrighterBuilder UseInMemoryOutbox(this IBrighterBuilder brighterBuilder) + private static IPolicyRegistry AddDefaults(IPolicyRegistry policyRegistry) { - brighterBuilder.Services.TryAdd(new ServiceDescriptor(typeof(IAmAnOutboxSync), _ => new InMemoryOutbox(), ServiceLifetime.Singleton)); - brighterBuilder.Services.TryAdd(new ServiceDescriptor(typeof(IAmAnOutboxAsync), _ => new InMemoryOutbox(), ServiceLifetime.Singleton)); + if (!policyRegistry.ContainsKey(CommandProcessor.RETRYPOLICY)) + throw new ConfigurationException( + "The policy registry is missing the CommandProcessor.RETRYPOLICY policy which is required"); - return brighterBuilder; + if (!policyRegistry.ContainsKey(CommandProcessor.CIRCUITBREAKER)) + throw new ConfigurationException( + "The policy registry is missing the CommandProcessor.CIRCUITBREAKER policy which is required"); + + return policyRegistry; } - /// - /// Uses the Brighter In-Memory Inbox to store messages received to support once-only messaging and diagnostics - /// Advantages: Fast and no additional infrastructure required - /// Disadvantages: The inbox will not survive restarts, so messages will not be de-duped if received after a restart. - /// The inbox will not work across threads/processes so only works with a single performer/consumer. - /// Registers singletons with the service collection: - /// - InMemoryInboxSync - what messages have we received - /// - InMemoryInboxAsync - what messages have we received (async pipeline compatible) - /// - /// - /// - public static IBrighterBuilder UseInMemoryInbox(this IBrighterBuilder brighterBuilder) + private static object BuildCommandProcessor(IServiceProvider provider) { - brighterBuilder.Services.TryAdd(new ServiceDescriptor(typeof(IAmAnInboxSync), _ => new InMemoryInbox(), ServiceLifetime.Singleton)); - brighterBuilder.Services.TryAdd(new ServiceDescriptor(typeof(IAmAnInboxAsync), _ => new InMemoryInbox(), ServiceLifetime.Singleton)); + var loggerFactory = provider.GetService(); + ApplicationLogging.LoggerFactory = loggerFactory; - return brighterBuilder; + var options = provider.GetService(); + var subscriberRegistry = provider.GetService(); + var useRequestResponse = provider.GetService(); + + var handlerFactory = new ServiceProviderHandlerFactory(provider); + var handlerConfiguration = new HandlerConfiguration(subscriberRegistry, handlerFactory); + + var needHandlers = CommandProcessorBuilder.With(); + + var featureSwitchRegistry = provider.GetService(); + + if (featureSwitchRegistry != null) + needHandlers = needHandlers.ConfigureFeatureSwitches(featureSwitchRegistry); + + var policyBuilder = needHandlers.Handlers(handlerConfiguration); + + var messagingBuilder = options.PolicyRegistry == null + ? policyBuilder.DefaultPolicy() + : policyBuilder.Policies(options.PolicyRegistry); + + INeedARequestContext ret = AddEventBus(provider, messagingBuilder, useRequestResponse); + + var commandProcessor = ret + .RequestContextFactory(options.RequestContextFactory) + .Build(); + + return commandProcessor; } - /// - /// An external bus is the use of Message Oriented Middleware (MoM) to dispatch a message between a producer and a consumer. The assumption is that this - /// is being used for inter-process communication, for example the work queue pattern for distributing work, or between microservicves - /// Registers singletons with the service collection :- - /// - Producer - the Gateway wrapping access to Middleware - /// - UseRpc - do we want to use Rpc i.e. a command blocks waiting for a response, over middleware - /// - /// The Brighter builder to add this option to - /// The collection of producers - clients that connect to a specific transport - /// Add support for RPC over MoM by using a reply queue - /// Reply queue subscription - /// The Brighter builder to allow chaining of requests - public static IBrighterBuilder UseExternalBus(this IBrighterBuilder brighterBuilder, IAmAProducerRegistry producerRegistry, bool useRequestResponseQueues = false, IEnumerable replyQueueSubscriptions = null) + private static IBrighterBuilder ExternalBusBuilder( + IBrighterBuilder brighterBuilder, + IAmExternalBusConfiguration externalBusConfiguration, + Type transactionType) { + if (externalBusConfiguration.ProducerRegistry == null) + throw new ConfigurationException("An external bus must have an IAmAProducerRegistry"); + + var serviceCollection = brighterBuilder.Services; + + serviceCollection.TryAddSingleton(externalBusConfiguration); + serviceCollection.TryAddSingleton(externalBusConfiguration.ProducerRegistry); + + //we always need an outbox in case of producer callbacks + var outbox = externalBusConfiguration.Outbox; + if (outbox == null) + { + outbox = new InMemoryOutbox(); + } + + //we create the outbox from interfaces from the determined transaction type to prevent the need + //to pass generic types as we know the transaction provider type + var syncOutboxType = typeof(IAmAnOutboxSync<,>).MakeGenericType(typeof(Message), transactionType); + var asyncOutboxType = typeof(IAmAnOutboxAsync<,>).MakeGenericType(typeof(Message), transactionType); + + foreach (Type i in outbox.GetType().GetInterfaces()) + { + if (i.IsGenericType && i.GetGenericTypeDefinition() == syncOutboxType) + { + var outboxDescriptor = new ServiceDescriptor(syncOutboxType, _ => outbox, ServiceLifetime.Singleton); + serviceCollection.Add(outboxDescriptor); + } + + if (i.IsGenericType && i.GetGenericTypeDefinition() == asyncOutboxType) + { + var asyncOutboxdescriptor = new ServiceDescriptor(asyncOutboxType, _ => outbox, ServiceLifetime.Singleton); + serviceCollection.Add(asyncOutboxdescriptor); + } + } + + if (externalBusConfiguration.UseRpc) + { + serviceCollection.TryAddSingleton(new UseRpc(externalBusConfiguration.UseRpc, + externalBusConfiguration.ReplyQueueSubscriptions)); + } - brighterBuilder.Services.TryAddSingleton(producerRegistry); - - brighterBuilder.Services.TryAddSingleton(new UseRpc(useRequestResponseQueues, replyQueueSubscriptions)); + //Because the bus has specialized types as members, we need to create the bus type dynamically + //again to prevent someone configuring Brighter from having to pass generic types + var busType = typeof(ExternalBusServices<,>).MakeGenericType(typeof(Message), transactionType); - return brighterBuilder; - } + IAmAnExternalBusService bus = (IAmAnExternalBusService)Activator.CreateInstance(busType, + externalBusConfiguration.ProducerRegistry, + brighterBuilder.PolicyRegistry, + outbox, + externalBusConfiguration.OutboxBulkChunkSize, + externalBusConfiguration.OutboxTimeout); + + serviceCollection.TryAddSingleton(bus); - /// - /// Configure a Feature Switch registry to control handlers to be feature switched at runtime - /// - /// The Brighter builder to add this option to - /// The registry for handler Feature Switches - /// The Brighter builder to allow chaining of requests - public static IBrighterBuilder UseFeatureSwitches(this IBrighterBuilder brighterBuilder, IAmAFeatureSwitchRegistry featureSwitchRegistry) - { - brighterBuilder.Services.TryAddSingleton(featureSwitchRegistry); return brighterBuilder; } - + /// - /// Config the Json Serialiser that is used inside of Brighter + /// Config the Json Serializer that is used inside of Brighter /// /// The Brighter Builder /// Action to configure the options /// Brighter Builder - public static IBrighterBuilder ConfigureJsonSerialisation(this IBrighterBuilder brighterBuilder, Action configure) + public static IBrighterBuilder ConfigureJsonSerialisation(this IBrighterBuilder brighterBuilder, + Action configure) { var options = new JsonSerializerOptions(); - + configure.Invoke(options); JsonSerialisationOptions.Options = options; - + return brighterBuilder; } - + /// /// Registers message mappers with the registry. Normally you don't need to call this, it is called by the builder for Brighter or the Service Activator /// Visibility is required for use from both @@ -263,129 +363,62 @@ public static MessageMapperRegistry MessageMapperRegistry(IServiceProvider provi return messageMapperRegistry; } - - /// - /// Creates transforms. Normally you don't need to call this, it is called by the builder for Brighter or the Service Activator - /// Visibility is required for use from both - /// - /// The IoC container to build the transform factory over - /// - public static ServiceProviderTransformerFactory TransformFactory(IServiceProvider provider) - { - return new ServiceProviderTransformerFactory(provider); - } - private static CommandProcessor BuildCommandProcessor(IServiceProvider provider) + private static void RegisterDynamoProviderServicesMaybe( + IBrighterBuilder brighterBuilder, + Type connectionProvider, + Type transactionProvider, + ServiceLifetime serviceLifetime) { - var loggerFactory = provider.GetService(); - ApplicationLogging.LoggerFactory = loggerFactory; - - var options = provider.GetService(); - var subscriberRegistry = provider.GetService(); - var useRequestResponse = provider.GetService(); - - var handlerFactory = new ServiceProviderHandlerFactory(provider); - var handlerConfiguration = new HandlerConfiguration(subscriberRegistry, handlerFactory); - - var messageMapperRegistry = MessageMapperRegistry(provider); - - var transformFactory = TransformFactory(provider); - - var outbox = provider.GetService>(); - var asyncOutbox = provider.GetService>(); - var overridingConnectionProvider = provider.GetService(); - - if (outbox == null) outbox = new InMemoryOutbox(); - if (asyncOutbox == null) asyncOutbox = new InMemoryOutbox(); - - var inboxConfiguration = provider.GetService(); - - var producerRegistry = provider.GetService(); - - var needHandlers = CommandProcessorBuilder.With(); - - var featureSwitchRegistry = provider.GetService(); - - if (featureSwitchRegistry != null) - needHandlers = needHandlers.ConfigureFeatureSwitches(featureSwitchRegistry); + //not all box transaction providers are also relational connection providers + if (typeof(IAmADynamoDbConnectionProvider).IsAssignableFrom(connectionProvider)) + { + brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmADynamoDbConnectionProvider), + connectionProvider, serviceLifetime)); + } - var policyBuilder = needHandlers.Handlers(handlerConfiguration); - - var messagingBuilder = options.PolicyRegistry == null - ? policyBuilder.DefaultPolicy() - : policyBuilder.Policies(options.PolicyRegistry); - - var commandProcessor = AddExternalBusMaybe( - options, - producerRegistry, - messagingBuilder, - messageMapperRegistry, - inboxConfiguration, - outbox, - overridingConnectionProvider, - useRequestResponse, - _outboxBulkChunkSize, - transformFactory) - .RequestContextFactory(options.RequestContextFactory) - .Build(); - - return commandProcessor; + //not all box transaction providers are also relational connection providers + if (typeof(IAmADynamoDbTransactionProvider).IsAssignableFrom(transactionProvider)) + { + //register the combined interface just in case + brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmADynamoDbTransactionProvider), + transactionProvider, serviceLifetime)); + } } - - private enum ExternalBusType + private static void RegisterRelationalProviderServicesMaybe( + IBrighterBuilder brighterBuilder, + Type connectionProvider, + Type transactionProvider, + ServiceLifetime serviceLifetime + ) { - None = 0, - FireAndForget = 1, - RPC = 2 - } - - private static INeedARequestContext AddExternalBusMaybe( - IBrighterOptions options, - IAmAProducerRegistry producerRegistry, - INeedMessaging messagingBuilder, - MessageMapperRegistry messageMapperRegistry, - InboxConfiguration inboxConfiguration, - IAmAnOutboxSync outbox, - IAmABoxTransactionConnectionProvider overridingConnectionProvider, - IUseRpc useRequestResponse, - int outboxBulkChunkSize, - IAmAMessageTransformerFactory transformerFactory) - { - ExternalBusType externalBusType = GetExternalBusType(producerRegistry, useRequestResponse); - - if (externalBusType == ExternalBusType.None) - return messagingBuilder.NoExternalBus(); - else if (externalBusType == ExternalBusType.FireAndForget) - return messagingBuilder.ExternalBus( - new ExternalBusConfiguration( - producerRegistry, - messageMapperRegistry, - outboxBulkChunkSize: outboxBulkChunkSize, - useInbox: inboxConfiguration, - transformerFactory: transformerFactory), - outbox, - overridingConnectionProvider); - else if (externalBusType == ExternalBusType.RPC) + //not all box transaction providers are also relational connection providers + if (typeof(IAmARelationalDbConnectionProvider).IsAssignableFrom(connectionProvider)) { - return messagingBuilder.ExternalRPC( - new ExternalBusConfiguration( - producerRegistry, - messageMapperRegistry, - responseChannelFactory: options.ChannelFactory, - useInbox: inboxConfiguration), - outbox, - useRequestResponse.ReplyQueueSubscriptions); + brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmARelationalDbConnectionProvider), + connectionProvider, serviceLifetime)); + } + + //not all box transaction providers are also relational connection providers + if (typeof(IAmATransactionConnectionProvider).IsAssignableFrom(transactionProvider)) + { + //register the combined interface just in case + brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmATransactionConnectionProvider), + transactionProvider, serviceLifetime)); } - - throw new ArgumentOutOfRangeException("The external bus type requested was not understood"); } - private static ExternalBusType GetExternalBusType(IAmAProducerRegistry producerRegistry, IUseRpc useRequestResponse) + /// + /// Creates transforms. Normally you don't need to call this, it is called by the builder for Brighter or + /// the Service Activator + /// Visibility is required for use from both + /// + /// The IoC container to build the transform factory over + /// + public static ServiceProviderTransformerFactory TransformFactory(IServiceProvider provider) { - var externalBusType = producerRegistry == null ? ExternalBusType.None : ExternalBusType.FireAndForget; - if (externalBusType == ExternalBusType.FireAndForget && useRequestResponse.RPC) externalBusType = ExternalBusType.RPC; - return externalBusType; + return new ServiceProviderTransformerFactory(provider); } } } diff --git a/src/Paramore.Brighter.Extensions.Hosting/HostedServiceCollectionExtensions.cs b/src/Paramore.Brighter.Extensions.Hosting/HostedServiceCollectionExtensions.cs index a26da51c7d..4e38bce4be 100644 --- a/src/Paramore.Brighter.Extensions.Hosting/HostedServiceCollectionExtensions.cs +++ b/src/Paramore.Brighter.Extensions.Hosting/HostedServiceCollectionExtensions.cs @@ -24,7 +24,7 @@ public static IBrighterBuilder UseOutboxSweeper(this IBrighterBuilder brighterBu return brighterBuilder; } - public static IBrighterBuilder UseOutboxArchiver(this IBrighterBuilder brighterBuilder, + public static IBrighterBuilder UseOutboxArchiver(this IBrighterBuilder brighterBuilder, IAmAnArchiveProvider archiveProvider, Action timedOutboxArchiverOptionsAction = null) { @@ -33,7 +33,7 @@ public static IBrighterBuilder UseOutboxArchiver(this IBrighterBuilder brighterB brighterBuilder.Services.TryAddSingleton(options); brighterBuilder.Services.AddSingleton(archiveProvider); - brighterBuilder.Services.AddHostedService(); + brighterBuilder.Services.AddHostedService>(); return brighterBuilder; } diff --git a/src/Paramore.Brighter.Extensions.Hosting/TimedOutboxArchiver.cs b/src/Paramore.Brighter.Extensions.Hosting/TimedOutboxArchiver.cs index 446a59e165..f35c6abac8 100644 --- a/src/Paramore.Brighter.Extensions.Hosting/TimedOutboxArchiver.cs +++ b/src/Paramore.Brighter.Extensions.Hosting/TimedOutboxArchiver.cs @@ -1,6 +1,7 @@ using System; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Paramore.Brighter.Logging; @@ -8,17 +9,19 @@ namespace Paramore.Brighter.Extensions.Hosting { - public class TimedOutboxArchiver : IHostedService, IDisposable + public class TimedOutboxArchiver : IHostedService, IDisposable where TMessage : Message { private readonly TimedOutboxArchiverOptions _options; private static readonly ILogger s_logger = ApplicationLogging.CreateLogger(); - private IAmAnOutbox _outbox; - private IAmAnArchiveProvider _archiveProvider; + private readonly IAmAnOutbox _outbox; + private readonly IAmAnArchiveProvider _archiveProvider; private Timer _timer; private static readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); - public TimedOutboxArchiver(IAmAnOutbox outbox, IAmAnArchiveProvider archiveProvider, + public TimedOutboxArchiver( + IAmAnOutbox outbox, + IAmAnArchiveProvider archiveProvider, TimedOutboxArchiverOptions options) { _outbox = outbox; @@ -56,7 +59,7 @@ private async Task Archive(object state, CancellationToken cancellationToken) s_logger.LogInformation("Outbox Archiver looking for messages to Archive"); try { - var outBoxArchiver = new OutboxArchiver( + var outBoxArchiver = new OutboxArchiver( _outbox, _archiveProvider, _options.BatchSize); diff --git a/src/Paramore.Brighter.Extensions.Hosting/TimedOutboxSweeper.cs b/src/Paramore.Brighter.Extensions.Hosting/TimedOutboxSweeper.cs index 36005de97a..4fd3e7e213 100644 --- a/src/Paramore.Brighter.Extensions.Hosting/TimedOutboxSweeper.cs +++ b/src/Paramore.Brighter.Extensions.Hosting/TimedOutboxSweeper.cs @@ -1,11 +1,13 @@ using System; using System.Threading; using System.Threading.Tasks; +using System.Timers; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Paramore.Brighter.Logging; using ILogger = Microsoft.Extensions.Logging.ILogger; +using Timer = System.Timers.Timer; namespace Paramore.Brighter.Extensions.Hosting { @@ -14,7 +16,9 @@ public class TimedOutboxSweeper : IHostedService, IDisposable private readonly IServiceScopeFactory _serviceScopeFactory; private readonly TimedOutboxSweeperOptions _options; private static readonly ILogger s_logger= ApplicationLogging.CreateLogger(); + private Timer _timer; + //private Timer _timer; public TimedOutboxSweeper (IServiceScopeFactory serviceScopeFactory, TimedOutboxSweeperOptions options) { @@ -26,12 +30,17 @@ public Task StartAsync(CancellationToken cancellationToken) { s_logger.LogInformation("Outbox Sweeper Service is starting."); - _timer = new Timer(DoWork, null, TimeSpan.Zero, TimeSpan.FromSeconds(_options.TimerInterval)); + //_timer = new Timer(DoWork, null, TimeSpan.Zero, TimeSpan.FromSeconds(_options.TimerInterval)); + var milliseconds = _options.TimerInterval * 1000; + _timer = new Timer(milliseconds); + _timer.Elapsed += new ElapsedEventHandler(OnElapsed); + _timer.Enabled = true; + _timer.AutoReset = false; return Task.CompletedTask; } - private void DoWork(object state) + private void OnElapsed(object sender, ElapsedEventArgs elapsedEventArgs) { s_logger.LogInformation("Outbox Sweeper looking for unsent messages"); @@ -60,6 +69,7 @@ private void DoWork(object state) finally { scope.Dispose(); + ((Timer)sender).Enabled = true; } s_logger.LogInformation("Outbox Sweeper sleeping"); @@ -69,7 +79,8 @@ public Task StopAsync(CancellationToken cancellationToken) { s_logger.LogInformation("Outbox Sweeper Service is stopping."); - _timer?.Change(Timeout.Infinite, 0); + //_timer?.Change(Timeout.Infinite, 0); + _timer.Stop(); return Task.CompletedTask; } diff --git a/src/Paramore.Brighter.Inbox.MsSql/DDL Scripts/MSSQL/Inbox.sql b/src/Paramore.Brighter.Inbox.MsSql/DDL Scripts/MSSQL/Inbox.sql deleted file mode 100644 index 6aae88b479..0000000000 --- a/src/Paramore.Brighter.Inbox.MsSql/DDL Scripts/MSSQL/Inbox.sql +++ /dev/null @@ -1,28 +0,0 @@ --- User Table information: --- Number of tables: 1 --- Commands: 0 row(s) - -PRINT 'Creating Inbox table' -CREATE TABLE [Commands] ( - [Id] [BIGINT] NOT NULL IDENTITY -, [CommandId] uniqueidentifier NOT NULL -, [CommandType] nvarchar(256) NULL -, [CommandBody] ntext NULL -, [Timestamp] datetime NULL -, [ContextKey] nvarchar(256) NULL -, PRIMARY KEY ( [Id] ) -); -GO -IF (NOT EXISTS ( SELECT * - FROM sys.indexes - WHERE name = 'UQ_Commands__CommandId' - AND object_id = OBJECT_ID('Commands') ) - ) -BEGIN - PRINT 'Creating a unique index on the CommandId column of the Command table...' - - CREATE UNIQUE NONCLUSTERED INDEX UQ_Commands__CommandId - ON Commands(CommandId) -END -GO -Print 'Done' \ No newline at end of file diff --git a/src/Paramore.Brighter.Inbox.MsSql/MsSqlInbox.cs b/src/Paramore.Brighter.Inbox.MsSql/MsSqlInbox.cs index abda3e134f..527fd9ba55 100644 --- a/src/Paramore.Brighter.Inbox.MsSql/MsSqlInbox.cs +++ b/src/Paramore.Brighter.Inbox.MsSql/MsSqlInbox.cs @@ -24,6 +24,8 @@ THE SOFTWARE. */ #endregion using System; +using System.Data; +using System.Data.Common; using Microsoft.Data.SqlClient; using System.Text.Json; using System.Threading; @@ -44,15 +46,15 @@ public class MsSqlInbox : IAmAnInboxSync, IAmAnInboxAsync private const int MsSqlDuplicateKeyError_UniqueIndexViolation = 2601; private const int MsSqlDuplicateKeyError_UniqueConstraintViolation = 2627; - private readonly MsSqlConfiguration _configuration; - private readonly IMsSqlConnectionProvider _connectionProvider; + private readonly IAmARelationalDatabaseConfiguration _configuration; + private readonly IAmARelationalDbConnectionProvider _connectionProvider; /// /// Initializes a new instance of the class. /// /// The configuration. /// The Connection Provider. - public MsSqlInbox(MsSqlConfiguration configuration, IMsSqlConnectionProvider connectionProvider) + public MsSqlInbox(IAmARelationalDatabaseConfiguration configuration, IAmARelationalDbConnectionProvider connectionProvider) { _configuration = configuration; ContinueOnCapturedContext = false; @@ -63,8 +65,8 @@ public MsSqlInbox(MsSqlConfiguration configuration, IMsSqlConnectionProvider con /// Initializes a new instance of the class. /// /// The configuration. - public MsSqlInbox(MsSqlConfiguration configuration) : this(configuration, - new MsSqlSqlAuthConnectionProvider(configuration)) + public MsSqlInbox(IAmARelationalDatabaseConfiguration configuration) : this(configuration, + new MsSqlConnectionProvider(configuration)) { } @@ -82,7 +84,6 @@ public void Add(T command, string contextKey, int timeoutInMilliseconds = -1) using (var connection = _connectionProvider.GetConnection()) { - connection.Open(); var sqlcmd = InitAddDbCommand(connection, parameters, timeoutInMilliseconds); try { @@ -159,7 +160,6 @@ public async Task AddAsync(T command, string contextKey, int timeoutInMillise using (var connection = await _connectionProvider.GetConnectionAsync(cancellationToken)) { - await connection.OpenAsync(cancellationToken).ConfigureAwait(ContinueOnCapturedContext); var sqlcmd = InitAddDbCommand(connection, parameters, timeoutInMilliseconds); try { @@ -255,8 +255,12 @@ private SqlParameter CreateSqlParameter(string parameterName, object value) return new SqlParameter(parameterName, value ?? DBNull.Value); } - private T ExecuteCommand(Func execute, string sql, int timeoutInMilliseconds, - params SqlParameter[] parameters) + private T ExecuteCommand( + Func execute, + string sql, + int timeoutInMilliseconds, + params IDbDataParameter[] parameters + ) { using (var connection = _connectionProvider.GetConnection()) using (var command = connection.CreateCommand()) @@ -265,18 +269,17 @@ private T ExecuteCommand(Func execute, string sql, int timeout command.CommandText = sql; command.Parameters.AddRange(parameters); - connection.Open(); var item = execute(command); return item; } } private async Task ExecuteCommandAsync( - Func> execute, + Func> execute, string sql, int timeoutInMilliseconds, CancellationToken cancellationToken = default, - params SqlParameter[] parameters) + params IDbDataParameter[] parameters) { using (var connection = await _connectionProvider.GetConnectionAsync(cancellationToken)) using (var command = connection.CreateCommand()) @@ -285,13 +288,12 @@ private async Task ExecuteCommandAsync( command.CommandText = sql; command.Parameters.AddRange(parameters); - await connection.OpenAsync(cancellationToken).ConfigureAwait(ContinueOnCapturedContext); var item = await execute(command).ConfigureAwait(ContinueOnCapturedContext); return item; } } - private SqlCommand InitAddDbCommand(SqlConnection connection, SqlParameter[] parameters, int timeoutInMilliseconds) + private DbCommand InitAddDbCommand(DbConnection connection, IDbDataParameter[] parameters, int timeoutInMilliseconds) { var sqlAdd = $"insert into {_configuration.InBoxTableName} (CommandID, CommandType, CommandBody, Timestamp, ContextKey) values (@CommandID, @CommandType, @CommandBody, @Timestamp, @ContextKey)"; @@ -304,7 +306,7 @@ private SqlCommand InitAddDbCommand(SqlConnection connection, SqlParameter[] par return sqlcmd; } - private SqlParameter[] InitAddDbParameters(T command, string contextKey) where T : class, IRequest + private IDbDataParameter[] InitAddDbParameters(T command, string contextKey) where T : class, IRequest { var commandJson = JsonSerializer.Serialize(command, JsonSerialisationOptions.Options); var parameters = new[] @@ -318,7 +320,7 @@ private SqlParameter[] InitAddDbParameters(T command, string contextKey) wher return parameters; } - private TResult ReadCommand(SqlDataReader dr, Guid commandId) where TResult : class, IRequest + private TResult ReadCommand(IDataReader dr, Guid commandId) where TResult : class, IRequest { if (dr.Read()) { diff --git a/src/Paramore.Brighter.Inbox.MsSql/Paramore.Brighter.Inbox.MsSql.csproj b/src/Paramore.Brighter.Inbox.MsSql/Paramore.Brighter.Inbox.MsSql.csproj index 6f3f14994d..be0785ee34 100644 --- a/src/Paramore.Brighter.Inbox.MsSql/Paramore.Brighter.Inbox.MsSql.csproj +++ b/src/Paramore.Brighter.Inbox.MsSql/Paramore.Brighter.Inbox.MsSql.csproj @@ -8,9 +8,6 @@ - - - diff --git a/src/Paramore.Brighter.Inbox.MsSql/SqlInboxBuilder.cs b/src/Paramore.Brighter.Inbox.MsSql/SqlInboxBuilder.cs index e1d7fea8e8..f34b1272e4 100644 --- a/src/Paramore.Brighter.Inbox.MsSql/SqlInboxBuilder.cs +++ b/src/Paramore.Brighter.Inbox.MsSql/SqlInboxBuilder.cs @@ -29,19 +29,19 @@ namespace Paramore.Brighter.Inbox.MsSql /// public class SqlInboxBuilder { - private const string OutboxDDL = @" + private const string InboxDDL = @" CREATE TABLE {0} ( [Id] [BIGINT] IDENTITY(1, 1) NOT NULL , [CommandId] [UNIQUEIDENTIFIER] NOT NULL , [CommandType] [NVARCHAR](256) NULL , - [CommandBody] [NTEXT] NULL , + [CommandBody] [NVARCHAR](MAX) NULL , [Timestamp] [DATETIME] NULL , [ContextKey] [NVARCHAR](256) NULL, PRIMARY KEY ( [Id] ) );"; - private const string InboxExistsSQL = @"SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = N'{0}'"; + private const string InboxExistsSQL = @"IF EXISTS (SELECT 1 FROM sys.tables WHERE name = '{0}') SELECT 1 AS TableExists; ELSE SELECT 0 AS TableExists;"; /// /// Get the DDL statements to create an Inbox in MSSQL @@ -50,7 +50,7 @@ PRIMARY KEY ( [Id] ) /// The required DDL public static string GetDDL(string inboxTableName) { - return string.Format(OutboxDDL, inboxTableName); + return string.Format(InboxDDL, inboxTableName); } /// diff --git a/src/Paramore.Brighter.Inbox.MySql/MySqlInbox.cs b/src/Paramore.Brighter.Inbox.MySql/MySqlInbox.cs index 0e6a492a56..589c043afc 100644 --- a/src/Paramore.Brighter.Inbox.MySql/MySqlInbox.cs +++ b/src/Paramore.Brighter.Inbox.MySql/MySqlInbox.cs @@ -44,13 +44,13 @@ public class MySqlInbox : IAmAnInboxSync, IAmAnInboxAsync private static readonly ILogger s_logger = ApplicationLogging.CreateLogger(); private const int MySqlDuplicateKeyError = 1062; - private readonly MySqlInboxConfiguration _configuration; + private readonly IAmARelationalDatabaseConfiguration _configuration; /// /// Initializes a new instance of the class. /// /// The configuration. - public MySqlInbox(MySqlInboxConfiguration configuration) + public MySqlInbox(IAmARelationalDatabaseConfiguration configuration) { _configuration = configuration; ContinueOnCapturedContext = false; diff --git a/src/Paramore.Brighter.Inbox.MySql/MySqlInboxBuilder.cs b/src/Paramore.Brighter.Inbox.MySql/MySqlInboxBuilder.cs index 6315158312..1de432190d 100644 --- a/src/Paramore.Brighter.Inbox.MySql/MySqlInboxBuilder.cs +++ b/src/Paramore.Brighter.Inbox.MySql/MySqlInboxBuilder.cs @@ -41,8 +41,7 @@ public class MySqlInboxBuilder PRIMARY KEY (`CommandId`) ) ENGINE = InnoDB;"; - const string InboxExistsQuery = @"SHOW TABLES LIKE '{0}'; "; - + const string InboxExistsQuery = @"SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = '{0}') AS TableExists;"; /// /// Gets the DDL statements to create an Inbox in MySQL diff --git a/src/Paramore.Brighter.Inbox.MySql/MySqlInboxConfiguration.cs b/src/Paramore.Brighter.Inbox.MySql/MySqlInboxConfiguration.cs deleted file mode 100644 index 0e8b29b8cc..0000000000 --- a/src/Paramore.Brighter.Inbox.MySql/MySqlInboxConfiguration.cs +++ /dev/null @@ -1,54 +0,0 @@ -#region Licence -/* The MIT License (MIT) -Copyright © 2014 Francesco Pighi - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the “Software”), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. */ - -#endregion - -namespace Paramore.Brighter.Inbox.MySql -{ - /// - /// Class MySqlInboxConfiguration. - /// - public class MySqlInboxConfiguration - { - /// - /// Initializes a new instance of the class. - /// - /// The subscription string. - /// Name of the outbox table. - public MySqlInboxConfiguration(string connectionString, string inBoxTableName) - { - InBoxTableName = inBoxTableName; - ConnectionString = connectionString; - } - - /// - /// Gets the subscription string. - /// - /// The subscription string. - public string ConnectionString { get; private set; } - /// - /// Gets the name of the outbox table. - /// - /// The name of the outbox table. - public string InBoxTableName { get; private set; } - } -} diff --git a/src/Paramore.Brighter.Inbox.Postgres/Paramore.Brighter.Inbox.Postgres.csproj b/src/Paramore.Brighter.Inbox.Postgres/Paramore.Brighter.Inbox.Postgres.csproj index 2d4b9c63df..10c0e90f22 100644 --- a/src/Paramore.Brighter.Inbox.Postgres/Paramore.Brighter.Inbox.Postgres.csproj +++ b/src/Paramore.Brighter.Inbox.Postgres/Paramore.Brighter.Inbox.Postgres.csproj @@ -12,13 +12,10 @@ - - all runtime; build; native; contentfiles; analyzers; buildtransitive - diff --git a/src/Paramore.Brighter.Inbox.Postgres/PostgresSqlInbox.cs b/src/Paramore.Brighter.Inbox.Postgres/PostgreSqlInbox.cs similarity index 76% rename from src/Paramore.Brighter.Inbox.Postgres/PostgresSqlInbox.cs rename to src/Paramore.Brighter.Inbox.Postgres/PostgreSqlInbox.cs index cef7880f17..73b4545573 100644 --- a/src/Paramore.Brighter.Inbox.Postgres/PostgresSqlInbox.cs +++ b/src/Paramore.Brighter.Inbox.Postgres/PostgreSqlInbox.cs @@ -38,11 +38,11 @@ THE SOFTWARE. */ namespace Paramore.Brighter.Inbox.Postgres { - public class PostgresSqlInbox : IAmAnInboxSync, IAmAnInboxAsync + public class PostgreSqlInbox : IAmAnInboxSync, IAmAnInboxAsync { - private readonly PostgresSqlInboxConfiguration _configuration; - private readonly IPostgreSqlConnectionProvider _connectionProvider; - private static readonly ILogger s_logger = ApplicationLogging.CreateLogger(); + private readonly IAmARelationalDatabaseConfiguration _configuration; + private readonly IAmARelationalDbConnectionProvider _connectionProvider; + private static readonly ILogger s_logger = ApplicationLogging.CreateLogger(); /// /// If false we the default thread synchronization context to run any continuation, if true we re-use the original /// synchronization context. @@ -53,7 +53,7 @@ public class PostgresSqlInbox : IAmAnInboxSync, IAmAnInboxAsync /// public bool ContinueOnCapturedContext { get; set; } - public PostgresSqlInbox(PostgresSqlInboxConfiguration configuration, IPostgreSqlConnectionProvider connectionProvider = null) + public PostgreSqlInbox(IAmARelationalDatabaseConfiguration configuration, IAmARelationalDbConnectionProvider connectionProvider = null) { _configuration = configuration; _connectionProvider = connectionProvider; @@ -62,10 +62,8 @@ public PostgresSqlInbox(PostgresSqlInboxConfiguration configuration, IPostgreSql public void Add(T command, string contextKey, int timeoutInMilliseconds = -1) where T : class, IRequest { - var connectionProvider = GetConnectionProvider(); var parameters = InitAddDbParameters(command, contextKey); - var connection = GetOpenConnection(connectionProvider); - + var connection = GetConnection(); try { using (var sqlcmd = InitAddDbCommand(connection, parameters, timeoutInMilliseconds)) @@ -86,10 +84,7 @@ public void Add(T command, string contextKey, int timeoutInMilliseconds = -1) } finally { - if (!connectionProvider.IsSharedConnection) connection.Dispose(); - else if (!connectionProvider.HasOpenTransaction) - connection.Close(); } } @@ -120,9 +115,8 @@ public bool Exists(Guid id, string contextKey, int timeoutInMilliseconds = -1 public async Task AddAsync(T command, string contextKey, int timeoutInMilliseconds = -1, CancellationToken cancellationToken = default) where T : class, IRequest { - var connectionProvider = GetConnectionProvider(); var parameters = InitAddDbParameters(command, contextKey); - var connection = await GetOpenConnectionAsync(connectionProvider, cancellationToken).ConfigureAwait(ContinueOnCapturedContext); + var connection = GetConnection(); try { @@ -145,10 +139,7 @@ public async Task AddAsync(T command, string contextKey, int timeoutInMillise } finally { - if (!connectionProvider.IsSharedConnection) - await connection.DisposeAsync().ConfigureAwait(ContinueOnCapturedContext); - else if (!connectionProvider.HasOpenTransaction) - await connection.CloseAsync().ConfigureAwait(ContinueOnCapturedContext); + connection.Dispose(); } } @@ -192,35 +183,17 @@ public async Task ExistsAsync(Guid id, string contextKey, int timeoutIn parameters) .ConfigureAwait(ContinueOnCapturedContext); } - - private IPostgreSqlConnectionProvider GetConnectionProvider(IAmABoxTransactionConnectionProvider transactionConnectionProvider = null) + + private DbConnection GetConnection() { - var connectionProvider = _connectionProvider ?? new PostgreSqlNpgsqlConnectionProvider(_configuration); - - if (transactionConnectionProvider != null) - { - if (transactionConnectionProvider is IPostgreSqlTransactionConnectionProvider provider) - connectionProvider = provider; - else - throw new Exception($"{nameof(transactionConnectionProvider)} does not implement interface {nameof(IPostgreSqlTransactionConnectionProvider)}."); - } - - return connectionProvider; - } - - private NpgsqlConnection GetOpenConnection(IPostgreSqlConnectionProvider connectionProvider) - { - NpgsqlConnection connection = connectionProvider.GetConnection(); - - if (connection.State != ConnectionState.Open) - connection.Open(); - + var connection = new NpgsqlConnection(_configuration.ConnectionString); + connection.Open(); return connection; } - private async Task GetOpenConnectionAsync(IPostgreSqlConnectionProvider connectionProvider, CancellationToken cancellationToken = default) + private async Task GetOpenConnectionAsync(IAmARelationalDbConnectionProvider connectionProvider, CancellationToken cancellationToken = default) { - NpgsqlConnection connection = await connectionProvider.GetConnectionAsync(cancellationToken).ConfigureAwait(ContinueOnCapturedContext); + DbConnection connection = await connectionProvider.GetConnectionAsync(cancellationToken).ConfigureAwait(ContinueOnCapturedContext); if (connection.State != ConnectionState.Open) await connection.OpenAsync(cancellationToken).ConfigureAwait(ContinueOnCapturedContext); @@ -263,8 +236,7 @@ private DbParameter[] InitAddDbParameters(T command, string contextKey) where private T ExecuteCommand(Func execute, string sql, int timeoutInMilliseconds, params DbParameter[] parameters) { - var connectionProvider = GetConnectionProvider(); - var connection = GetOpenConnection(connectionProvider); + var connection = GetConnection(); try { @@ -281,10 +253,7 @@ private T ExecuteCommand(Func execute, string sql, int timeoutI } finally { - if (!connectionProvider.IsSharedConnection) - connection.Dispose(); - else if (!connectionProvider.HasOpenTransaction) - connection.Close(); + connection.Dispose(); } } @@ -295,8 +264,7 @@ private async Task ExecuteCommandAsync( CancellationToken cancellationToken = default, params DbParameter[] parameters) { - var connectionProvider = GetConnectionProvider(); - var connection = await GetOpenConnectionAsync(connectionProvider, cancellationToken).ConfigureAwait(ContinueOnCapturedContext); + var connection = GetConnection(); try { @@ -313,10 +281,7 @@ private async Task ExecuteCommandAsync( } finally { - if (!connectionProvider.IsSharedConnection) - await connection.DisposeAsync().ConfigureAwait(ContinueOnCapturedContext); - else if (!connectionProvider.HasOpenTransaction) - await connection.CloseAsync().ConfigureAwait(ContinueOnCapturedContext); + connection.Dispose(); } } diff --git a/src/Paramore.Brighter.Inbox.Postgres/PostgresSqlInboxBuilder.cs b/src/Paramore.Brighter.Inbox.Postgres/PostgreSqlInboxBuilder.cs similarity index 93% rename from src/Paramore.Brighter.Inbox.Postgres/PostgresSqlInboxBuilder.cs rename to src/Paramore.Brighter.Inbox.Postgres/PostgreSqlInboxBuilder.cs index 0b5c21bc05..4738df31da 100644 --- a/src/Paramore.Brighter.Inbox.Postgres/PostgresSqlInboxBuilder.cs +++ b/src/Paramore.Brighter.Inbox.Postgres/PostgreSqlInboxBuilder.cs @@ -28,7 +28,7 @@ namespace Paramore.Brighter.Inbox.Postgres /// /// Provide SQL statement helpers for creation of an Inbox /// - public class PostgresSqlInboxBuilder + public class PostgreSqlInboxBuilder { private const string OutboxDDL = @" CREATE TABLE {0} @@ -41,7 +41,7 @@ ContextKey VARCHAR(256) NULL, PRIMARY KEY (CommandId, ContextKey) );"; - private const string InboxExistsSQL = @"SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = N'{0}'"; + private const string InboxExistsSQL = @"SELECT EXISTS(SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = '{0}')"; /// /// Get the DDL statements to create an Inbox in Postgres diff --git a/src/Paramore.Brighter.Inbox.Sqlite/DDL Scripts/SQLite/CommandStore.sql b/src/Paramore.Brighter.Inbox.Sqlite/DDL Scripts/SQLite/CommandStore.sql deleted file mode 100644 index 43f3392238..0000000000 --- a/src/Paramore.Brighter.Inbox.Sqlite/DDL Scripts/SQLite/CommandStore.sql +++ /dev/null @@ -1,13 +0,0 @@ -SELECT 1; -PRAGMA foreign_keys=OFF; -BEGIN TRANSACTION; -CREATE TABLE [Commands] ( - [CommandId] uniqueidentifier NOT NULL -, [CommandType] nvarchar(256) NULL -, [CommandBody] ntext NULL -, [Timestamp] datetime NULL -, [ContextKey] nvarchar(256) NULL -, CONSTRAINT [PK_MessageId] PRIMARY KEY ([CommandId]) -); -COMMIT; - diff --git a/src/Paramore.Brighter.Inbox.Sqlite/README.md b/src/Paramore.Brighter.Inbox.Sqlite/README.md deleted file mode 100644 index 8a9e30370d..0000000000 --- a/src/Paramore.Brighter.Inbox.Sqlite/README.md +++ /dev/null @@ -1,47 +0,0 @@ -# Brighter Sqlite inbox - -## Setup - -To setup Brighter with a Sqlite inbox, some steps are required: - -#### Create a table with the schema in the example - -You can use the following example as a reference for SQL Server: - -```sql - CREATE TABLE MyOutbox ( - Id uniqueidentifier CONSTRAINT PK_MessageId PRIMARY KEY, - Topic nvarchar(255), - MessageType nvarchar(32), - Body nvarchar(max) - ) -``` -If you're using SQL CE you have to replace `nvarchar(max)` with a supported type, for example `ntext`. - -#### Configure the command processor - -The following is an example of how to configure a command processor with a SQL Server outbox. - -```csharp -var msSqlOutbox = new MsSqlOutbox(new MsSqlOutboxConfiguration( - "myconnectionstring", - "MyOutboxTable", - MsSqlOutboxConfiguration.DatabaseType.MsSqlServer - ), myLogger), - -var commandProcessor = CommandProcessorBuilder.With() - .Handlers(new HandlerConfiguration(mySubscriberRegistry, myHandlerFactory)) - .Policies(myPolicyRegistry) - .Logger(myLogger) - .TaskQueues(new MessagingConfiguration( - outbox: msSqlOutbox, - messagingGateway: myGateway, - messageMapperRegistry: myMessageMapperRegistry - )) - .RequestContextFactory(new InMemoryRequestContextFactory()) - .Build(); -``` - -> The values for the `MsSqlOutboxConfiguration.DatabaseType` enum are the following: -> `MsSqlServer` -> `SqlCe` diff --git a/src/Paramore.Brighter.Inbox.Sqlite/README.txt b/src/Paramore.Brighter.Inbox.Sqlite/README.txt deleted file mode 100644 index 8ee71c1a95..0000000000 --- a/src/Paramore.Brighter.Inbox.Sqlite/README.txt +++ /dev/null @@ -1,47 +0,0 @@ -# Brighter MSSQL outbox - -## Setup - -To setup Brighter with a SQL Server or SQL CE outbox, some steps are required: - -#### Create a table with the schema in the example - -You can use the following example as a reference for SQL Server: - -```sql - CREATE TABLE MyOutbox ( - Id uniqueidentifier CONSTRAINT PK_MessageId PRIMARY KEY, - Topic nvarchar(255), - MessageType nvarchar(32), - Body nvarchar(max) - ) -``` -If you're using SQL CE you have to replace `nvarchar(max)` with a supported type, for example `ntext`. - -#### Configure the command processor - -The following is an example of how to configure a command processor with a SQL Server outbox. - -```csharp -var msSqlOutbox = new MsSqlOutbox(new MsSqlOutboxConfiguration( - "myconnectionstring", - "MyOutboxTable", - MsSqlOutboxConfiguration.DatabaseType.MsSqlServer - ), myLogger), - -var commandProcessor = CommandProcessorBuilder.With() - .Handlers(new HandlerConfiguration(mySubscriberRegistry, myHandlerFactory)) - .Policies(myPolicyRegistry) - .Logger(myLogger) - .TaskQueues(new MessagingConfiguration( - outbox: msSqlOutbox, - messagingGateway: myGateway, - messageMapperRegistry: myMessageMapperRegistry - )) - .RequestContextFactory(new InMemoryRequestContextFactory()) - .Build(); -``` - -> The values for the `MsSqlOutboxConfiguration.DatabaseType` enum are the following: -> `MsSqlServer` -> `SqlCe` diff --git a/src/Paramore.Brighter.Inbox.Sqlite/SqliteInbox.cs b/src/Paramore.Brighter.Inbox.Sqlite/SqliteInbox.cs index 5dd83bc4bb..b9b6550b21 100644 --- a/src/Paramore.Brighter.Inbox.Sqlite/SqliteInbox.cs +++ b/src/Paramore.Brighter.Inbox.Sqlite/SqliteInbox.cs @@ -50,7 +50,7 @@ public class SqliteInbox : IAmAnInboxSync, IAmAnInboxAsync /// Initializes a new instance of the class. /// /// The configuration. - public SqliteInbox(SqliteInboxConfiguration configuration) + public SqliteInbox(IAmARelationalDatabaseConfiguration configuration) { Configuration = configuration; ContinueOnCapturedContext = false; @@ -210,7 +210,7 @@ public async Task GetAsync(Guid id, string contextKey, int timeoutInMillis /// public bool ContinueOnCapturedContext { get; set; } - public SqliteInboxConfiguration Configuration { get; } + public IAmARelationalDatabaseConfiguration Configuration { get; } public string OutboxTableName => Configuration.InBoxTableName; diff --git a/src/Paramore.Brighter.Inbox.Sqlite/SqliteInboxConfiguration.cs b/src/Paramore.Brighter.Inbox.Sqlite/SqliteInboxConfiguration.cs deleted file mode 100644 index 48d150c2c7..0000000000 --- a/src/Paramore.Brighter.Inbox.Sqlite/SqliteInboxConfiguration.cs +++ /dev/null @@ -1,54 +0,0 @@ -#region Licence -/* The MIT License (MIT) -Copyright © 2014 Francesco Pighi - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the “Software”), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. */ - -#endregion - -namespace Paramore.Brighter.Inbox.Sqlite -{ - /// - /// Class SqliteInboxConfiguration. - /// - public class SqliteInboxConfiguration - { - /// - /// Initializes a new instance of the class. - /// - /// The subscription string. - /// Name of the Inbox table. - public SqliteInboxConfiguration(string connectionString, string inBoxTableName) - { - InBoxTableName = inBoxTableName; - ConnectionString = connectionString; - } - - /// - /// Gets the subscription string. - /// - /// The subscription string. - public string ConnectionString { get; private set; } - /// - /// Gets the name of the outbox table. - /// - /// The name of the outbox table. - public string InBoxTableName { get; private set; } - } -} diff --git a/src/Paramore.Brighter.MessagingGateway.Kafka/KafkaDefaultMessageHeaderBuilder.cs b/src/Paramore.Brighter.MessagingGateway.Kafka/KafkaDefaultMessageHeaderBuilder.cs index 045a78cb53..ee63098801 100644 --- a/src/Paramore.Brighter.MessagingGateway.Kafka/KafkaDefaultMessageHeaderBuilder.cs +++ b/src/Paramore.Brighter.MessagingGateway.Kafka/KafkaDefaultMessageHeaderBuilder.cs @@ -28,9 +28,13 @@ public Headers Build(Message message) new Header(HeaderNames.MESSAGE_TYPE, message.Header.MessageType.ToString().ToByteArray()), new Header(HeaderNames.TOPIC, message.Header.Topic.ToByteArray()), new Header(HeaderNames.MESSAGE_ID, message.Header.Id.ToString().ToByteArray()), - new Header(HeaderNames.TIMESTAMP, BitConverter.GetBytes(UnixTimestamp.GetCurrentUnixTimestampSeconds())) }; + if (message.Header.TimeStamp != default) + headers.Add(HeaderNames.TIMESTAMP, BitConverter.GetBytes(new DateTimeOffset(message.Header.TimeStamp).ToUnixTimeMilliseconds())); + else + headers.Add(HeaderNames.TIMESTAMP, BitConverter.GetBytes(DateTimeOffset.UtcNow.ToUnixTimeMilliseconds())); + if (message.Header.CorrelationId != Guid.Empty) headers.Add(HeaderNames.CORRELATION_ID, message.Header.CorrelationId.ToString().ToByteArray()); @@ -42,7 +46,7 @@ public Headers Build(Message message) if (!string.IsNullOrEmpty(message.Header.ReplyTo)) headers.Add(HeaderNames.REPLY_TO, message.Header.ReplyTo.ToByteArray()); - + message.Header.Bag.Each((header) => { if (!s_headersToReset.Any(htr => htr.Equals(header.Key))) @@ -55,6 +59,12 @@ public Headers Build(Message message) case int intValue: headers.Add(header.Key, BitConverter.GetBytes(intValue)); break; + case Guid guidValue: + headers.Add(header.Key, guidValue.ToByteArray()); + break; + case byte[] byteArray: + headers.Add(header.Key, byteArray); + break; default: headers.Add(header.Key, header.Value.ToString().ToByteArray()); break; diff --git a/src/Paramore.Brighter.MessagingGateway.Kafka/KafkaMessageCreator.cs b/src/Paramore.Brighter.MessagingGateway.Kafka/KafkaMessageCreator.cs index acf81c3d3f..6828c7c813 100644 --- a/src/Paramore.Brighter.MessagingGateway.Kafka/KafkaMessageCreator.cs +++ b/src/Paramore.Brighter.MessagingGateway.Kafka/KafkaMessageCreator.cs @@ -143,7 +143,7 @@ private HeaderResult ReadTimeStamp(Headers headers) try { - return new HeaderResult(UnixTimestamp.DateTimeFromUnixTimestampSeconds(BitConverter.ToInt64(lastHeader, 0)), true); + return new HeaderResult(DateTimeOffset.FromUnixTimeMilliseconds(BitConverter.ToInt64(lastHeader, 0)).DateTime, true); } catch (Exception) { diff --git a/src/Paramore.Brighter.MessagingGateway.Kafka/UnixTimestamp.cs b/src/Paramore.Brighter.MessagingGateway.Kafka/UnixTimestamp.cs deleted file mode 100644 index 9825296345..0000000000 --- a/src/Paramore.Brighter.MessagingGateway.Kafka/UnixTimestamp.cs +++ /dev/null @@ -1,48 +0,0 @@ -#region Licence -/* The MIT License (MIT) -Copyright © 2014 Ian Cooper - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the “Software”), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. */ - -#endregion - -using System; - -namespace Paramore.Brighter.MessagingGateway.Kafka -{ - internal static class UnixTimestamp - { - private static readonly DateTime s_unixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); - - public static DateTime DateTimeFromUnixTimestampSeconds(long seconds) - { - return s_unixEpoch.AddSeconds(seconds); - } - - public static long GetCurrentUnixTimestampSeconds() - { - return (long)(DateTime.UtcNow - s_unixEpoch).TotalSeconds; - } - - public static long GetUnixTimestampSeconds(DateTime dateTime) - { - return (long)(dateTime - s_unixEpoch).TotalSeconds; - } - } -} diff --git a/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlMessageConsumer.cs b/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlMessageConsumer.cs index f1acd15fc2..f29611e605 100644 --- a/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlMessageConsumer.cs +++ b/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlMessageConsumer.cs @@ -14,16 +14,18 @@ public class MsSqlMessageConsumer : IAmAMessageConsumer private readonly MsSqlMessageQueue _sqlQ; public MsSqlMessageConsumer( - MsSqlConfiguration msSqlConfiguration, - string topic, IMsSqlConnectionProvider connectionProvider) + RelationalDatabaseConfiguration msSqlConfiguration, + string topic, + RelationalDbConnectionProvider connectionProvider + ) { _topic = topic ?? throw new ArgumentNullException(nameof(topic)); _sqlQ = new MsSqlMessageQueue(msSqlConfiguration, connectionProvider); } public MsSqlMessageConsumer( - MsSqlConfiguration msSqlConfiguration, - string topic) :this(msSqlConfiguration, topic, new MsSqlSqlAuthConnectionProvider(msSqlConfiguration)) + RelationalDatabaseConfiguration msSqlConfiguration, + string topic) :this(msSqlConfiguration, topic, new MsSqlConnectionProvider(msSqlConfiguration)) { } diff --git a/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlMessageConsumerFactory.cs b/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlMessageConsumerFactory.cs index b3c49493e3..09100f5910 100644 --- a/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlMessageConsumerFactory.cs +++ b/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlMessageConsumerFactory.cs @@ -8,9 +8,9 @@ namespace Paramore.Brighter.MessagingGateway.MsSql public class MsSqlMessageConsumerFactory : IAmAMessageConsumerFactory { private static readonly ILogger s_logger = ApplicationLogging.CreateLogger(); - private readonly MsSqlConfiguration _msSqlConfiguration; + private readonly RelationalDatabaseConfiguration _msSqlConfiguration; - public MsSqlMessageConsumerFactory(MsSqlConfiguration msSqlConfiguration) + public MsSqlMessageConsumerFactory(RelationalDatabaseConfiguration msSqlConfiguration) { _msSqlConfiguration = msSqlConfiguration ?? throw new ArgumentNullException( diff --git a/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlMessageProducer.cs b/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlMessageProducer.cs index a3657efc62..5d8e1b4dc9 100644 --- a/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlMessageProducer.cs +++ b/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlMessageProducer.cs @@ -58,11 +58,11 @@ public class MsSqlMessageProducer : IAmAMessageProducerSync, IAmAMessageProducer private Publication _publication; // -- placeholder for future use public MsSqlMessageProducer( - MsSqlConfiguration msSqlConfiguration, - IMsSqlConnectionProvider connectionProvider, + RelationalDatabaseConfiguration msSqlConfiguration, + IAmARelationalDbConnectionProvider connectonProvider, Publication publication = null) { - _sqlQ = new MsSqlMessageQueue(msSqlConfiguration, connectionProvider); + _sqlQ = new MsSqlMessageQueue(msSqlConfiguration, connectonProvider); _publication = publication ?? new Publication {MakeChannels = OnMissingChannel.Create}; MaxOutStandingMessages = _publication.MaxOutStandingMessages; MaxOutStandingCheckIntervalMilliSeconds = _publication.MaxOutStandingCheckIntervalMilliSeconds; @@ -70,8 +70,8 @@ public MsSqlMessageProducer( } public MsSqlMessageProducer( - MsSqlConfiguration msSqlConfiguration, - Publication publication = null) : this(msSqlConfiguration, new MsSqlSqlAuthConnectionProvider(msSqlConfiguration), publication) + RelationalDatabaseConfiguration msSqlConfiguration, + Publication publication = null) : this(msSqlConfiguration, new MsSqlConnectionProvider(msSqlConfiguration), publication) { } diff --git a/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlProducerRegistryFactory.cs b/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlProducerRegistryFactory.cs index 9d311b5068..1be902715f 100644 --- a/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlProducerRegistryFactory.cs +++ b/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlProducerRegistryFactory.cs @@ -8,12 +8,12 @@ namespace Paramore.Brighter.MessagingGateway.MsSql { public class MsSqlProducerRegistryFactory : IAmAProducerRegistryFactory { - private readonly MsSqlConfiguration _msSqlConfiguration; + private readonly RelationalDatabaseConfiguration _msSqlConfiguration; private static readonly ILogger s_logger = ApplicationLogging.CreateLogger(); private readonly IEnumerable _publications; //-- placeholder for future use public MsSqlProducerRegistryFactory( - MsSqlConfiguration msSqlConfiguration, + RelationalDatabaseConfiguration msSqlConfiguration, IEnumerable publications) { _msSqlConfiguration = diff --git a/src/Paramore.Brighter.MessagingGateway.MsSql/SqlQueues/MsSqlMessageQueue.cs b/src/Paramore.Brighter.MessagingGateway.MsSql/SqlQueues/MsSqlMessageQueue.cs index 8c73f45840..830397468e 100644 --- a/src/Paramore.Brighter.MessagingGateway.MsSql/SqlQueues/MsSqlMessageQueue.cs +++ b/src/Paramore.Brighter.MessagingGateway.MsSql/SqlQueues/MsSqlMessageQueue.cs @@ -1,11 +1,12 @@ using System; +using System.Data; +using System.Data.Common; using Microsoft.Data.SqlClient; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Paramore.Brighter.Logging; -using Paramore.Brighter.MsSql; namespace Paramore.Brighter.MessagingGateway.MsSql.SqlQueues { @@ -17,15 +18,15 @@ public class MsSqlMessageQueue { private const int RetryDelay = 100; private static readonly ILogger s_logger = ApplicationLogging.CreateLogger>(); - private readonly MsSqlConfiguration _configuration; - private readonly IMsSqlConnectionProvider _connectionProvider; + private readonly RelationalDatabaseConfiguration _configuration; + private readonly IAmARelationalDbConnectionProvider _connectionProvider; /// /// Initializes a new instance of the class. /// /// /// - public MsSqlMessageQueue(MsSqlConfiguration configuration, IMsSqlConnectionProvider connectionProvider) + public MsSqlMessageQueue(RelationalDatabaseConfiguration configuration, IAmARelationalDbConnectionProvider connectionProvider) { _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); _connectionProvider = connectionProvider; @@ -58,7 +59,6 @@ public void Send(T message, string topic, int timeoutInMilliseconds = -1) using (var connection = _connectionProvider.GetConnection()) { - connection.Open(); var sqlCmd = InitAddDbCommand(timeoutInMilliseconds, connection, parameters); sqlCmd.ExecuteNonQuery(); } @@ -72,8 +72,7 @@ public void Send(T message, string topic, int timeoutInMilliseconds = -1) /// Timeout in milliseconds; -1 for default timeout /// The active CancellationToken /// - public async Task SendAsync(T message, string topic, int timeoutInMilliseconds = -1, - CancellationToken cancellationToken = default) + public async Task SendAsync(T message, string topic, int timeoutInMilliseconds = -1, CancellationToken cancellationToken = default) { if (s_logger.IsEnabled(LogLevel.Debug)) s_logger.LogDebug("SendAsync<{CommandType}>(..., {Topic})", typeof(T).FullName, topic); @@ -81,7 +80,6 @@ public async Task SendAsync(T message, string topic, int timeoutInMilliseconds = using (var connection = await _connectionProvider.GetConnectionAsync(cancellationToken)) { - await connection.OpenAsync(cancellationToken).ConfigureAwait(ContinueOnCapturedContext); var sqlCmd = InitAddDbCommand(timeoutInMilliseconds, connection, parameters); await sqlCmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(ContinueOnCapturedContext); } @@ -122,7 +120,6 @@ private ReceivedResult TryReceive(string topic) using (var connection = _connectionProvider.GetConnection()) { - connection.Open(); var sqlCmd = InitRemoveDbCommand(connection, parameters); var reader = sqlCmd.ExecuteReader(); if (!reader.Read()) @@ -150,7 +147,6 @@ public async Task> TryReceiveAsync(string topic, using (var connection = await _connectionProvider.GetConnectionAsync(cancellationToken)) { - await connection.OpenAsync(cancellationToken).ConfigureAwait(ContinueOnCapturedContext); var sqlCmd = InitRemoveDbCommand(connection, parameters); var reader = await sqlCmd.ExecuteReaderAsync(cancellationToken) .ConfigureAwait(ContinueOnCapturedContext); @@ -189,29 +185,28 @@ public void Purge() using (var connection = _connectionProvider.GetConnection()) { - connection.Open(); var sqlCmd = InitPurgeDbCommand(connection); sqlCmd.ExecuteNonQuery(); } } - private static SqlParameter CreateSqlParameter(string parameterName, object value) + private static IDbDataParameter CreateDbDataParameter(string parameterName, object value) { return new SqlParameter(parameterName, value); } - private static SqlParameter[] InitAddDbParameters(string topic, T message) + private static IDbDataParameter[] InitAddDbParameters(string topic, T message) { var parameters = new[] { - CreateSqlParameter("topic", topic), - CreateSqlParameter("messageType", typeof(T).FullName), - CreateSqlParameter("payload", JsonSerializer.Serialize(message, JsonSerialisationOptions.Options)) + CreateDbDataParameter("topic", topic), + CreateDbDataParameter("messageType", typeof(T).FullName), + CreateDbDataParameter("payload", JsonSerializer.Serialize(message, JsonSerialisationOptions.Options)) }; return parameters; } - private SqlCommand InitAddDbCommand(int timeoutInMilliseconds, SqlConnection connection, SqlParameter[] parameters) + private DbCommand InitAddDbCommand(int timeoutInMilliseconds, DbConnection connection, IDbDataParameter[] parameters) { var sql = $"set nocount on;insert into [{_configuration.QueueStoreTable}] (Topic, MessageType, Payload) values(@topic, @messageType, @payload);"; @@ -223,16 +218,16 @@ private SqlCommand InitAddDbCommand(int timeoutInMilliseconds, SqlConnection con return sqlCmd; } - private static SqlParameter[] InitRemoveDbParameters(string topic) + private static IDbDataParameter[] InitRemoveDbParameters(string topic) { var parameters = new[] { - CreateSqlParameter("topic", topic) + CreateDbDataParameter("topic", topic) }; return parameters; } - private SqlCommand InitRemoveDbCommand(SqlConnection connection, SqlParameter[] parameters) + private DbCommand InitRemoveDbCommand(DbConnection connection, IDbDataParameter[] parameters) { var sql = $"set nocount on;with cte as (select top(1) Payload, MessageType, Topic, Id from [{_configuration.QueueStoreTable}]" + @@ -243,7 +238,7 @@ private SqlCommand InitRemoveDbCommand(SqlConnection connection, SqlParameter[] return sqlCmd; } - private SqlCommand InitPurgeDbCommand(SqlConnection connection) + private DbCommand InitPurgeDbCommand(DbConnection connection) { var sql = $"delete from [{_configuration.QueueStoreTable}]"; var sqlCmd = connection.CreateCommand(); diff --git a/src/Paramore.Brighter.MessagingGateway.RMQ/RmqMessagePublisher.cs b/src/Paramore.Brighter.MessagingGateway.RMQ/RmqMessagePublisher.cs index f7c89572ee..859c3bc9db 100644 --- a/src/Paramore.Brighter.MessagingGateway.RMQ/RmqMessagePublisher.cs +++ b/src/Paramore.Brighter.MessagingGateway.RMQ/RmqMessagePublisher.cs @@ -143,7 +143,7 @@ public void RequeueMessage(Message message, string queueName, int delayMilliseco {HeaderNames.MESSAGE_TYPE, message.Header.MessageType.ToString()}, {HeaderNames.TOPIC, message.Header.Topic}, {HeaderNames.HANDLED_COUNT, message.Header.HandledCount}, - }; + }; if (message.Header.CorrelationId != Guid.Empty) headers.Add(HeaderNames.CORRELATION_ID, message.Header.CorrelationId.ToString()); diff --git a/src/Paramore.Brighter.MsSql.Azure/MsSqlAzureConnectionProviderBase.cs b/src/Paramore.Brighter.MsSql.Azure/MsSqlAzureConnectionProviderBase.cs index b26b4c86c5..bfb3af6347 100644 --- a/src/Paramore.Brighter.MsSql.Azure/MsSqlAzureConnectionProviderBase.cs +++ b/src/Paramore.Brighter.MsSql.Azure/MsSqlAzureConnectionProviderBase.cs @@ -1,4 +1,5 @@ using System; +using System.Data.Common; using System.Threading; using System.Threading.Tasks; using Azure.Core; @@ -7,45 +8,48 @@ namespace Paramore.Brighter.MsSql.Azure { - public abstract class MsSqlAzureConnectionProviderBase : IMsSqlConnectionProvider + public abstract class MsSqlAzureConnectionProviderBase : RelationalDbConnectionProvider { private readonly bool _cacheTokens; - private const string _azureScope = "https://database.windows.net/.default"; - private const int _cacheLifeTime = 5; + private const string AZURE_SCOPE = "https://database.windows.net/.default"; + private const int CACHE_LIFE_TIME = 5; private readonly string _connectionString; - protected readonly string[] _authenticationTokenScopes; + protected readonly string[] AuthenticationTokenScopes; - private static AccessToken _token; - private static SemaphoreSlim _semaphoreToken = new SemaphoreSlim(1, 1); + private static AccessToken s_token; + private static readonly SemaphoreSlim _semaphoreToken = new SemaphoreSlim(1, 1); /// /// The Abstract Base class /// /// Ms Sql Configuration. /// Cache Access Tokens until they have less than 5 minutes of life left. - protected MsSqlAzureConnectionProviderBase(MsSqlConfiguration configuration, bool cacheTokens = true) + protected MsSqlAzureConnectionProviderBase(RelationalDatabaseConfiguration configuration, bool cacheTokens = true) { _cacheTokens = cacheTokens; _connectionString = configuration.ConnectionString; - _authenticationTokenScopes = new string[1] {_azureScope}; + AuthenticationTokenScopes = new string[1] {AZURE_SCOPE}; } - public SqlConnection GetConnection() + public override DbConnection GetConnection() { - var sqlConnection = new SqlConnection(_connectionString); - sqlConnection.AccessToken = GetAccessToken(); + var connection = new SqlConnection(_connectionString); + connection.AccessToken = GetAccessToken(); + if (connection.State != System.Data.ConnectionState.Open) + connection.Open(); - return sqlConnection; + return connection; } - public async Task GetConnectionAsync( - CancellationToken cancellationToken = default) + public override async Task GetConnectionAsync(CancellationToken cancellationToken = default) { - var sqlConnection = new SqlConnection(_connectionString); - sqlConnection.AccessToken = await GetAccessTokenAsync(cancellationToken); + var connection = new SqlConnection(_connectionString); + connection.AccessToken = await GetAccessTokenAsync(cancellationToken); + if (connection.State != System.Data.ConnectionState.Open) + await connection.OpenAsync(cancellationToken); - return sqlConnection; + return connection; } private string GetAccessToken() @@ -55,12 +59,11 @@ private string GetAccessToken() try { //If the Token has more than 5 minutes Validity - if (DateTime.UtcNow.AddMinutes(_cacheLifeTime) <= _token.ExpiresOn.UtcDateTime) return _token.Token; + if (DateTime.UtcNow.AddMinutes(CACHE_LIFE_TIME) <= s_token.ExpiresOn.UtcDateTime) return s_token.Token; - var credential = new ManagedIdentityCredential(); var token = GetAccessTokenFromProvider(); - _token = token; + s_token = token; return token.Token; } @@ -77,12 +80,11 @@ private async Task GetAccessTokenAsync(CancellationToken cancellationTok try { //If the Token has more than 5 minutes Validity - if (DateTime.UtcNow.AddMinutes(_cacheLifeTime) <= _token.ExpiresOn.UtcDateTime) return _token.Token; + if (DateTime.UtcNow.AddMinutes(CACHE_LIFE_TIME) <= s_token.ExpiresOn.UtcDateTime) return s_token.Token; - var credential = new ManagedIdentityCredential(); var token = await GetAccessTokenFromProviderAsync(cancellationToken); - _token = token; + s_token = token; return token.Token; } @@ -96,13 +98,5 @@ private async Task GetAccessTokenAsync(CancellationToken cancellationTok protected abstract Task GetAccessTokenFromProviderAsync(CancellationToken cancellationToken); - public SqlTransaction GetTransaction() - { - //This Connection Factory does not support Transactions - return null; - } - - public bool HasOpenTransaction { get => false; } - public bool IsSharedConnection { get => false; } } } diff --git a/src/Paramore.Brighter.MsSql.Azure/MsSqlChainedConnectionProvider.cs b/src/Paramore.Brighter.MsSql.Azure/MsSqlChainedConnectionProvider.cs index 307e4ee481..1cfd35d21d 100644 --- a/src/Paramore.Brighter.MsSql.Azure/MsSqlChainedConnectionProvider.cs +++ b/src/Paramore.Brighter.MsSql.Azure/MsSqlChainedConnectionProvider.cs @@ -6,7 +6,7 @@ namespace Paramore.Brighter.MsSql.Azure { - public class ServiceBusChainedClientProvider : MsSqlAzureConnectionProviderBase + public class ServiceBusChainedClientConnectionProvider : MsSqlAzureConnectionProviderBase { private readonly ChainedTokenCredential _credential; @@ -15,7 +15,7 @@ public class ServiceBusChainedClientProvider : MsSqlAzureConnectionProviderBase /// /// Ms Sql Configuration /// List of Token Providers to use when trying to obtain a token. - public ServiceBusChainedClientProvider(MsSqlConfiguration configuration, + public ServiceBusChainedClientConnectionProvider(RelationalDatabaseConfiguration configuration, params TokenCredential[] credentialSources) : base(configuration) { if (credentialSources == null || credentialSources.Length < 1) @@ -34,7 +34,7 @@ protected override AccessToken GetAccessTokenFromProvider() protected override async Task GetAccessTokenFromProviderAsync(CancellationToken cancellationToken) { - return await _credential.GetTokenAsync(new TokenRequestContext(_authenticationTokenScopes), cancellationToken); + return await _credential.GetTokenAsync(new TokenRequestContext(AuthenticationTokenScopes), cancellationToken); } } } diff --git a/src/Paramore.Brighter.MsSql.Azure/MsSqlDefaultAzureConnectionProvider.cs b/src/Paramore.Brighter.MsSql.Azure/MsSqlDefaultAzureConnectionProvider.cs index b35eda2e19..e263cd0573 100644 --- a/src/Paramore.Brighter.MsSql.Azure/MsSqlDefaultAzureConnectionProvider.cs +++ b/src/Paramore.Brighter.MsSql.Azure/MsSqlDefaultAzureConnectionProvider.cs @@ -11,7 +11,7 @@ public class MsSqlDefaultAzureConnectionProvider : MsSqlAzureConnectionProviderB /// Initialise a new instance of Ms Sql Connection provider using Default Azure Credentials to acquire Access Tokens. /// /// Ms Sql Configuration - public MsSqlDefaultAzureConnectionProvider(MsSqlConfiguration configuration) : base(configuration) { } + public MsSqlDefaultAzureConnectionProvider(RelationalDatabaseConfiguration configuration) : base(configuration) { } protected override AccessToken GetAccessTokenFromProvider() { @@ -22,7 +22,7 @@ protected override async Task GetAccessTokenFromProviderAsync(Cance { var credential = new DefaultAzureCredential(); - return await credential.GetTokenAsync(new TokenRequestContext(_authenticationTokenScopes), cancellationToken); + return await credential.GetTokenAsync(new TokenRequestContext(AuthenticationTokenScopes), cancellationToken); } } } diff --git a/src/Paramore.Brighter.MsSql.Azure/MsSqlManagedIdentityConnectionProvider.cs b/src/Paramore.Brighter.MsSql.Azure/MsSqlManagedIdentityConnectionProvider.cs index b2b3f93b9f..b77fcffdd5 100644 --- a/src/Paramore.Brighter.MsSql.Azure/MsSqlManagedIdentityConnectionProvider.cs +++ b/src/Paramore.Brighter.MsSql.Azure/MsSqlManagedIdentityConnectionProvider.cs @@ -11,7 +11,7 @@ public class MsSqlManagedIdentityConnectionProvider : MsSqlAzureConnectionProvid /// Initialise a new instance of Ms Sql Connection provider using Managed Identity Credentials to acquire Access Tokens. /// /// Ms Sql Configuration - public MsSqlManagedIdentityConnectionProvider(MsSqlConfiguration configuration) : base(configuration) + public MsSqlManagedIdentityConnectionProvider(RelationalDatabaseConfiguration configuration) : base(configuration) { } @@ -23,7 +23,7 @@ protected override AccessToken GetAccessTokenFromProvider() protected override async Task GetAccessTokenFromProviderAsync(CancellationToken cancellationToken) { var credential = new ManagedIdentityCredential(); - return await credential.GetTokenAsync(new TokenRequestContext(_authenticationTokenScopes), cancellationToken); + return await credential.GetTokenAsync(new TokenRequestContext(AuthenticationTokenScopes), cancellationToken); } } } diff --git a/src/Paramore.Brighter.MsSql.Azure/MsSqlSharedTokenCacheConnectionProvider.cs b/src/Paramore.Brighter.MsSql.Azure/MsSqlSharedTokenCacheConnectionProvider.cs index 9e131aa7f8..765110b511 100644 --- a/src/Paramore.Brighter.MsSql.Azure/MsSqlSharedTokenCacheConnectionProvider.cs +++ b/src/Paramore.Brighter.MsSql.Azure/MsSqlSharedTokenCacheConnectionProvider.cs @@ -18,7 +18,7 @@ public class MsSqlSharedTokenCacheConnectionProvider : MsSqlAzureConnectionProvi /// Initialise a new instance of Ms Sql Connection provider using Shared Token Cache Credentials to acquire Access Tokens. /// /// Ms Sql Configuration - public MsSqlSharedTokenCacheConnectionProvider(MsSqlConfiguration configuration) : base(configuration) + public MsSqlSharedTokenCacheConnectionProvider(RelationalDatabaseConfiguration configuration) : base(configuration) { _azureUserName = Environment.GetEnvironmentVariable(_azureUserNameKey); _azureTenantId = Environment.GetEnvironmentVariable(_azureTenantIdKey); @@ -28,7 +28,7 @@ public MsSqlSharedTokenCacheConnectionProvider(MsSqlConfiguration configuration) /// Initialise a new instance of Ms Sql Connection provider using Shared Token Cache Credentials to acquire Access Tokens. /// /// Ms Sql Configuration - public MsSqlSharedTokenCacheConnectionProvider(MsSqlConfiguration configuration, string userName, string tenantId) : base(configuration) + public MsSqlSharedTokenCacheConnectionProvider(RelationalDatabaseConfiguration configuration, string userName, string tenantId) : base(configuration) { _azureUserName = userName; _azureTenantId = tenantId; @@ -42,7 +42,7 @@ protected override AccessToken GetAccessTokenFromProvider() protected override async Task GetAccessTokenFromProviderAsync(CancellationToken cancellationToken) { var credential = GetCredential(); - return await credential.GetTokenAsync(new TokenRequestContext(_authenticationTokenScopes), cancellationToken); + return await credential.GetTokenAsync(new TokenRequestContext(AuthenticationTokenScopes), cancellationToken); } private SharedTokenCacheCredential GetCredential() diff --git a/src/Paramore.Brighter.MsSql.Azure/MsSqlVisualStudioConnectionProvider.cs b/src/Paramore.Brighter.MsSql.Azure/MsSqlVisualStudioConnectionProvider.cs index 80728c509e..84b6308882 100644 --- a/src/Paramore.Brighter.MsSql.Azure/MsSqlVisualStudioConnectionProvider.cs +++ b/src/Paramore.Brighter.MsSql.Azure/MsSqlVisualStudioConnectionProvider.cs @@ -11,7 +11,7 @@ public class MsSqlVisualStudioConnectionProvider : MsSqlAzureConnectionProviderB /// Initialise a new instance of Ms Sql Connection provider using Visual Studio Credentials to acquire Access Tokens. /// /// Ms Sql Configuration - public MsSqlVisualStudioConnectionProvider(MsSqlConfiguration configuration) : base(configuration) + public MsSqlVisualStudioConnectionProvider(RelationalDatabaseConfiguration configuration) : base(configuration) { } @@ -23,7 +23,7 @@ protected override AccessToken GetAccessTokenFromProvider() protected override async Task GetAccessTokenFromProviderAsync(CancellationToken cancellationToken) { var credential = new VisualStudioCredential(); - return await credential.GetTokenAsync(new TokenRequestContext(_authenticationTokenScopes), cancellationToken); + return await credential.GetTokenAsync(new TokenRequestContext(AuthenticationTokenScopes), cancellationToken); } } } diff --git a/src/Paramore.Brighter.MsSql.Dapper/MsSqlDapperConnectionProvider.cs b/src/Paramore.Brighter.MsSql.Dapper/MsSqlDapperConnectionProvider.cs deleted file mode 100644 index 2565a5413e..0000000000 --- a/src/Paramore.Brighter.MsSql.Dapper/MsSqlDapperConnectionProvider.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Data.SqlClient; -using Paramore.Brighter.Dapper; -using Paramore.Brighter.MsSql; - -namespace Paramore.Brighter.MySql.Dapper -{ - public class MsSqlDapperConnectionProvider : IMsSqlTransactionConnectionProvider - { - private readonly IUnitOfWork _unitOfWork; - - public MsSqlDapperConnectionProvider(IUnitOfWork unitOfWork) - { - _unitOfWork = unitOfWork; - } - - public SqlConnection GetConnection() - { - return (SqlConnection)_unitOfWork.Database; - } - - public Task GetConnectionAsync(CancellationToken cancellationToken = default) - { - var tcs = new TaskCompletionSource(); - tcs.SetResult(GetConnection()); - return tcs.Task; - } - - public SqlTransaction GetTransaction() - { - return (SqlTransaction)_unitOfWork.BeginOrGetTransaction(); - } - - public bool HasOpenTransaction - { - get - { - return _unitOfWork.HasTransaction(); - } - } - - public bool IsSharedConnection - { - get { return true; } - - } - } -} diff --git a/src/Paramore.Brighter.MsSql.Dapper/Paramore.Brighter.MsSql.Dapper.csproj b/src/Paramore.Brighter.MsSql.Dapper/Paramore.Brighter.MsSql.Dapper.csproj index 5c69501f29..a4a50a0808 100644 --- a/src/Paramore.Brighter.MsSql.Dapper/Paramore.Brighter.MsSql.Dapper.csproj +++ b/src/Paramore.Brighter.MsSql.Dapper/Paramore.Brighter.MsSql.Dapper.csproj @@ -1,3 +1,4 @@ + @@ -19,4 +20,4 @@ - + \ No newline at end of file diff --git a/src/Paramore.Brighter.MsSql.Dapper/UnitOfWork.cs b/src/Paramore.Brighter.MsSql.Dapper/UnitOfWork.cs deleted file mode 100644 index 91bfbc67e3..0000000000 --- a/src/Paramore.Brighter.MsSql.Dapper/UnitOfWork.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System; -using System.Data; -using System.Data.Common; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Data.SqlClient; -using Paramore.Brighter.Dapper; - -namespace Paramore.Brighter.MySql.Dapper -{ - public class UnitOfWork : IUnitOfWork - { - private readonly SqlConnection _connection; - private SqlTransaction _transaction; - - public UnitOfWork(DbConnectionStringProvider dbConnectionStringProvider) - { - _connection = new SqlConnection(dbConnectionStringProvider.ConnectionString); - } - - public void Commit() - { - if (HasTransaction()) - { - _transaction.Commit(); - _transaction = null; - } - } - - public DbConnection Database - { - get { return _connection; } - } - - public DbTransaction BeginOrGetTransaction() - { - //ToDo: make this thread safe - if (!HasTransaction()) - { - if (_connection.State != ConnectionState.Open) - { - _connection.Open(); - } - _transaction = _connection.BeginTransaction(); - } - - return _transaction; - } - - public async Task BeginOrGetTransactionAsync(CancellationToken cancellationToken) - { - if (!HasTransaction()) - { - if (_connection.State != ConnectionState.Open) - { - await _connection.OpenAsync(cancellationToken); - } - _transaction = _connection.BeginTransaction(); - } - - return _transaction; - } - - public bool HasTransaction() - { - return _transaction != null; - } - - public void Dispose() - { - if (_transaction != null) - { - try { _transaction.Rollback(); } catch (Exception) { /*can't check transaction status, so it will throw if already committed*/ } - } - - if (_connection.State == ConnectionState.Open) - { - _connection.Close(); - } - } - } -} diff --git a/src/Paramore.Brighter.MsSql.EntityFrameworkCore/MsSqlEntityFrameworkCoreConnectionProvider.cs b/src/Paramore.Brighter.MsSql.EntityFrameworkCore/MsSqlEntityFrameworkCoreConnectionProvider.cs index 36ded089ec..2246105037 100644 --- a/src/Paramore.Brighter.MsSql.EntityFrameworkCore/MsSqlEntityFrameworkCoreConnectionProvider.cs +++ b/src/Paramore.Brighter.MsSql.EntityFrameworkCore/MsSqlEntityFrameworkCoreConnectionProvider.cs @@ -1,4 +1,6 @@ -using System.Threading; +using System; +using System.Data.Common; +using System.Threading; using System.Threading.Tasks; using Microsoft.Data.SqlClient; using Microsoft.EntityFrameworkCore; @@ -6,7 +8,7 @@ namespace Paramore.Brighter.MsSql.EntityFrameworkCore { - public class MsSqlEntityFrameworkCoreConnectionProvider : IMsSqlTransactionConnectionProvider where T : DbContext + public class MsSqlEntityFrameworkCoreConnectionProvider : RelationalDbTransactionProvider where T : DbContext { private readonly T _context; @@ -18,27 +20,80 @@ public MsSqlEntityFrameworkCoreConnectionProvider(T context) _context = context; } - public SqlConnection GetConnection() + /// + /// Commit the transaction + /// + public override void Commit() + { + if (HasOpenTransaction) + { + _context.Database.CurrentTransaction?.Commit(); + } + } + + /// + /// Commit the transaction + /// + /// An awaitable Task + public override Task CommitAsync(CancellationToken cancellationToken) + { + if (HasOpenTransaction) + { + _context.Database.CurrentTransaction?.CommitAsync(cancellationToken); + } + + return Task.CompletedTask; + } + /// + /// Gets a existing Connection; creates a new one if it does not exist + /// Opens the connection if it is not opened + /// + /// A database connection + public override DbConnection GetConnection() { //This line ensure that the connection has been initialised and that any required interceptors have been run before getting the connection _context.Database.CanConnect(); - return (SqlConnection)_context.Database.GetDbConnection(); + var connection = _context.Database.GetDbConnection(); + if (connection.State != System.Data.ConnectionState.Open) connection.Open(); + return connection; } - public async Task GetConnectionAsync(CancellationToken cancellationToken = default) + /// + /// Gets a existing Connection; creates a new one if it does not exist + /// The connection is not opened, you need to open it yourself. + /// The base class just returns a new or existing connection, but derived types may perform async i/o + /// + /// A database connection + public override async Task GetConnectionAsync(CancellationToken cancellationToken = default) { //This line ensure that the connection has been initialised and that any required interceptors have been run before getting the connection await _context.Database.CanConnectAsync(cancellationToken); - return (SqlConnection)_context.Database.GetDbConnection(); + var connection = _context.Database.GetDbConnection(); + if (connection.State != System.Data.ConnectionState.Open) await connection.OpenAsync(cancellationToken); + return connection; } - public SqlTransaction GetTransaction() + /// + /// Gets an existing transaction; creates a new one from the connection if it does not exist. + /// You should use the commit transaction using the Commit method. + /// + /// A database transaction + public override DbTransaction GetTransaction() { var trans = (SqlTransaction)_context.Database.CurrentTransaction?.GetDbTransaction(); return trans; } - public bool HasOpenTransaction { get => _context.Database.CurrentTransaction != null; } - public bool IsSharedConnection { get => true; } + /// + /// Rolls back a transaction + /// + public override async Task RollbackAsync(CancellationToken cancellationToken = default) + { + if (HasOpenTransaction) + { + try { await ((SqlTransaction)GetTransaction()).RollbackAsync(cancellationToken); } catch (Exception) { /* Ignore*/} + Transaction = null; + } + } } } diff --git a/src/Paramore.Brighter.MsSql/IMsSqlConnectionProvider.cs b/src/Paramore.Brighter.MsSql/IMsSqlConnectionProvider.cs deleted file mode 100644 index ed4442770f..0000000000 --- a/src/Paramore.Brighter.MsSql/IMsSqlConnectionProvider.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Data.SqlClient; - -namespace Paramore.Brighter.MsSql -{ - public interface IMsSqlConnectionProvider - { - SqlConnection GetConnection(); - Task GetConnectionAsync(CancellationToken cancellationToken = default); - SqlTransaction GetTransaction(); - bool HasOpenTransaction { get; } - bool IsSharedConnection { get; } - } -} diff --git a/src/Paramore.Brighter.MsSql/IMsSqlTransactionConnectionProvider.cs b/src/Paramore.Brighter.MsSql/IMsSqlTransactionConnectionProvider.cs deleted file mode 100644 index e7c3329da3..0000000000 --- a/src/Paramore.Brighter.MsSql/IMsSqlTransactionConnectionProvider.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Microsoft.Data.SqlClient; - -namespace Paramore.Brighter.MsSql -{ - public interface IMsSqlTransactionConnectionProvider : IMsSqlConnectionProvider, IAmABoxTransactionConnectionProvider - { - - } -} diff --git a/src/Paramore.Brighter.MsSql/MsSqlConfiguration.cs b/src/Paramore.Brighter.MsSql/MsSqlConfiguration.cs deleted file mode 100644 index 596629d17c..0000000000 --- a/src/Paramore.Brighter.MsSql/MsSqlConfiguration.cs +++ /dev/null @@ -1,44 +0,0 @@ -namespace Paramore.Brighter.MsSql -{ - public class MsSqlConfiguration - { - /// - /// Initializes a new instance of the class. - /// - /// The connection string. Please note the latest library defaults Encryption to on - /// if this is a issue add 'Encrypt=false' to your connection string. - /// Name of the outbox table. - /// Name of the inbox table. - /// Name of the queue store table. - public MsSqlConfiguration(string connectionString, string outBoxTableName = null, string inboxTableName = null, string queueStoreTable = null) - { - OutBoxTableName = outBoxTableName; - ConnectionString = connectionString; - InBoxTableName = inboxTableName; - QueueStoreTable = queueStoreTable; - } - - /// - /// Gets the connection string. - /// - /// The connection string. - public string ConnectionString { get; private set; } - - /// - /// Gets the name of the outbox table. - /// - /// The name of the outbox table. - public string OutBoxTableName { get; private set; } - - /// - /// Gets the name of the inbox table. - /// - /// The name of the inbox table. - public string InBoxTableName { get; private set; } - - /// - /// Gets the name of the queue table. - /// - public string QueueStoreTable { get; private set; } - } -} diff --git a/src/Paramore.Brighter.MsSql/MsSqlConnectionProvider.cs b/src/Paramore.Brighter.MsSql/MsSqlConnectionProvider.cs new file mode 100644 index 0000000000..3c3fb0bc44 --- /dev/null +++ b/src/Paramore.Brighter.MsSql/MsSqlConnectionProvider.cs @@ -0,0 +1,51 @@ +using System; +using System.Data.Common; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Data.SqlClient; + +namespace Paramore.Brighter.MsSql +{ + /// + /// A connection provider for Sqlite + /// + public class MsSqlConnectionProvider : RelationalDbConnectionProvider + { + private readonly string _connectionString; + + /// + /// Create a connection provider for MSSQL using a connection string for Db access + /// + /// The configuration for this database + public MsSqlConnectionProvider(IAmARelationalDatabaseConfiguration configuration) + { + if (string.IsNullOrWhiteSpace(configuration?.ConnectionString)) + throw new ArgumentNullException(nameof(configuration.ConnectionString)); + _connectionString = configuration.ConnectionString; + } + + /// + /// Create a new Sql Connection and open it + /// This is not a shared connection and you should manage it's lifetime + /// + /// + public override DbConnection GetConnection() + { + var connection = new SqlConnection(_connectionString); + if (connection.State != System.Data.ConnectionState.Open) connection.Open(); + return connection; + } + + /// + /// Create a new Sql Connection and open it + /// This is not a shared connection and you should manage it's lifetime + /// + /// + public override async Task GetConnectionAsync(CancellationToken cancellationToken = default) + { + var connection = new SqlConnection(_connectionString); + if (connection.State != System.Data.ConnectionState.Open) await connection.OpenAsync(cancellationToken); + return connection; + } + } +} diff --git a/src/Paramore.Brighter.MsSql/MsSqlSqlAuthConnectionProvider.cs b/src/Paramore.Brighter.MsSql/MsSqlSqlAuthConnectionProvider.cs deleted file mode 100644 index 84a028ab9d..0000000000 --- a/src/Paramore.Brighter.MsSql/MsSqlSqlAuthConnectionProvider.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Data.SqlClient; - -namespace Paramore.Brighter.MsSql -{ - public class MsSqlSqlAuthConnectionProvider : IMsSqlConnectionProvider - { - private readonly string _connectionString; - - /// - /// Initialise a new instance of Ms Sql Connection provider using Sql Authentication. - /// - /// Ms Sql Configuration - public MsSqlSqlAuthConnectionProvider(MsSqlConfiguration configuration) - { - _connectionString = configuration.ConnectionString; - } - - public SqlConnection GetConnection() - { - return new SqlConnection(_connectionString); - } - - public async Task GetConnectionAsync(CancellationToken cancellationToken = default) - { - var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - - tcs.SetResult(GetConnection()); - return await tcs.Task; - } - - public SqlTransaction GetTransaction() - { - //This Connection Factory does not support Transactions - return null; - } - - public bool HasOpenTransaction { get => false; } - public bool IsSharedConnection { get => false; } - } -} diff --git a/src/Paramore.Brighter.MsSql/MsSqlUnitOfWork.cs b/src/Paramore.Brighter.MsSql/MsSqlUnitOfWork.cs new file mode 100644 index 0000000000..6a831f07ab --- /dev/null +++ b/src/Paramore.Brighter.MsSql/MsSqlUnitOfWork.cs @@ -0,0 +1,76 @@ +using System; +using System.Data; +using System.Data.Common; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Data.SqlClient; + +namespace Paramore.Brighter.MsSql +{ + /// + /// A connection provider for Sqlite + /// + public class MsSqlUnitOfWork : RelationalDbTransactionProvider + { + private readonly string _connectionString; + + /// + /// Create a connection provider for MSSQL using a connection string for Db access + /// + /// The configuration for this database + public MsSqlUnitOfWork(IAmARelationalDatabaseConfiguration configuration) + { + if (string.IsNullOrWhiteSpace(configuration?.ConnectionString)) + throw new ArgumentNullException(nameof(configuration.ConnectionString)); + _connectionString = configuration.ConnectionString; + } + + /// + /// Create a new Sql Connection and open it + /// + /// + public override DbConnection GetConnection() + { + if (Connection == null) Connection = new SqlConnection(_connectionString); + if (Connection.State != System.Data.ConnectionState.Open) Connection.Open(); + return Connection; + } + + /// + /// Create a new Sql Connection and open it + /// + /// + public override async Task GetConnectionAsync(CancellationToken cancellationToken = default) + { + if (Connection == null) Connection = new SqlConnection(_connectionString); + if (Connection.State != System.Data.ConnectionState.Open) await Connection.OpenAsync(cancellationToken); + return Connection; + } + + /// + /// Creates and opens a Sql Transaction + /// This is a shared transaction and you should manage it through the unit of work + /// + /// A shared transaction + public override DbTransaction GetTransaction() + { + if (Connection == null) Connection = GetConnection(); + if (!HasOpenTransaction) + Transaction = ((SqlConnection) Connection).BeginTransaction(); + return Transaction; + } + + /// + /// Creates and opens a Sql Transaction + /// This is a shared transaction and you should manage it through the unit of work + /// + /// A shared transaction + public override async Task GetTransactionAsync(CancellationToken cancellationToken = default) + { + if (Connection == null) Connection = await GetConnectionAsync(cancellationToken); + if (!HasOpenTransaction) + Transaction = ((SqlConnection) Connection).BeginTransaction(); + return Transaction; + } + } +} diff --git a/src/Paramore.Brighter.MySql.Dapper/MySqlDapperConnectionProvider.cs b/src/Paramore.Brighter.MySql.Dapper/MySqlDapperConnectionProvider.cs deleted file mode 100644 index 4643fc8391..0000000000 --- a/src/Paramore.Brighter.MySql.Dapper/MySqlDapperConnectionProvider.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; -using MySqlConnector; -using Paramore.Brighter.Dapper; - -namespace Paramore.Brighter.MySql.Dapper -{ - public class MySqlDapperConnectionProvider : IMySqlTransactionConnectionProvider - { - private readonly IUnitOfWork _unitOfWork; - - public MySqlDapperConnectionProvider(IUnitOfWork unitOfWork) - { - _unitOfWork = unitOfWork; - } - - public MySqlConnection GetConnection() - { - return (MySqlConnection)_unitOfWork.Database; - } - - public Task GetConnectionAsync(CancellationToken cancellationToken = default) - { - var tcs = new TaskCompletionSource(); - tcs.SetResult(GetConnection()); - return tcs.Task; - } - - public MySqlTransaction GetTransaction() - { - return (MySqlTransaction)_unitOfWork.BeginOrGetTransaction(); - } - - public bool HasOpenTransaction - { - get - { - return _unitOfWork.HasTransaction(); - } - } - - public bool IsSharedConnection - { - get { return true; } - - } - } -} diff --git a/src/Paramore.Brighter.MySql.Dapper/UnitOfWork.cs b/src/Paramore.Brighter.MySql.Dapper/UnitOfWork.cs deleted file mode 100644 index 04591f7190..0000000000 --- a/src/Paramore.Brighter.MySql.Dapper/UnitOfWork.cs +++ /dev/null @@ -1,99 +0,0 @@ -using System; -using System.Data; -using System.Data.Common; -using System.Threading; -using System.Threading.Tasks; -using MySqlConnector; -using Paramore.Brighter.Dapper; - -namespace Paramore.Brighter.MySql.Dapper -{ - public class UnitOfWork : IUnitOfWork - { - private readonly MySqlConnection _connection; - private MySqlTransaction _transaction; - - public UnitOfWork(DbConnectionStringProvider dbConnectionStringProvider) - { - _connection = new MySqlConnection(dbConnectionStringProvider.ConnectionString); - } - - public void Commit() - { - if (HasTransaction()) - { - _transaction.Commit(); - _transaction = null; - } - } - - public DbConnection Database - { - get { return _connection; } - } - - /// - /// Begins a new transaction against the database. Will open the connection if it is not already open, - /// - /// A transaction - public DbTransaction BeginOrGetTransaction() - { - //ToDo: make this thread safe - if (!HasTransaction()) - { - if (_connection.State != ConnectionState.Open) - { - _connection.Open(); - } - _transaction = _connection.BeginTransaction(); - } - - return _transaction; - } - - /// - /// Begins a new transaction asynchronously against the database. Will open the connection if it is not already open, - /// - /// - /// A transaction - public async Task BeginOrGetTransactionAsync(CancellationToken cancellationToken) - { - if (!HasTransaction()) - { - if (_connection.State != ConnectionState.Open) - { - await _connection.OpenAsync(cancellationToken); - } - _transaction = await _connection.BeginTransactionAsync(cancellationToken); - } - - return _transaction; - } - - /// - /// Is there an extant transaction - /// - /// True if a transaction is already open on this unit of work, false otherwise - public bool HasTransaction() - { - return _transaction != null; - } - - /// - /// Rolls back a transaction if one is open; closes any connection to the Db - /// - public void Dispose() - { - if (_transaction != null) - { - //can't check transaction status, so it will throw if already committed - try { _transaction.Rollback(); } catch (Exception) { } - } - - if (_connection.State == ConnectionState.Open) - { - _connection.Close(); - } - } - } -} diff --git a/src/Paramore.Brighter.MySql.EntityFrameworkCore/MySqlEntityFrameworkConnectionProvider.cs b/src/Paramore.Brighter.MySql.EntityFrameworkCore/MySqlEntityFrameworkConnectionProvider.cs index e8e419d7f8..abdbe63ff5 100644 --- a/src/Paramore.Brighter.MySql.EntityFrameworkCore/MySqlEntityFrameworkConnectionProvider.cs +++ b/src/Paramore.Brighter.MySql.EntityFrameworkCore/MySqlEntityFrameworkConnectionProvider.cs @@ -1,4 +1,6 @@ -using System.Threading; +using System; +using System.Data.Common; +using System.Threading; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Storage; @@ -10,7 +12,7 @@ namespace Paramore.Brighter.MySql.EntityFrameworkCore /// A connection provider that uses the same connection as EF Core /// /// The Db Context to take the connection from - public class MySqlEntityFrameworkConnectionProvider : IMySqlTransactionConnectionProvider where T: DbContext + public class MySqlEntityFrameworkConnectionProvider : RelationalDbTransactionProvider where T: DbContext { private readonly T _context; @@ -23,15 +25,40 @@ public MySqlEntityFrameworkConnectionProvider(T context) _context = context; } + /// + /// Commit the transaction + /// + public override void Commit() + { + if (HasOpenTransaction) + { + _context.Database.CurrentTransaction?.Commit(); + } + } + + /// + /// Commit the transaction + /// + /// An awaitable Task + public override Task CommitAsync(CancellationToken cancellationToken) + { + if (HasOpenTransaction) + { + _context.Database.CurrentTransaction?.CommitAsync(cancellationToken); + } + + return Task.CompletedTask; + } + /// /// Get the current connection of the DB context /// /// The Sqlite Connection that is in use - public MySqlConnection GetConnection() + public override DbConnection GetConnection() { //This line ensure that the connection has been initialised and that any required interceptors have been run before getting the connection _context.Database.CanConnect(); - return (MySqlConnection) _context.Database.GetDbConnection(); + return _context.Database.GetDbConnection(); } /// @@ -39,23 +66,33 @@ public MySqlConnection GetConnection() /// /// A cancellation token /// - public async Task GetConnectionAsync(CancellationToken cancellationToken = default) + public override async Task GetConnectionAsync(CancellationToken cancellationToken = default) { //This line ensure that the connection has been initialised and that any required interceptors have been run before getting the connection await _context.Database.CanConnectAsync(cancellationToken); - return (MySqlConnection)_context.Database.GetDbConnection(); + return _context.Database.GetDbConnection(); } /// /// Get the ambient EF Core Transaction /// /// The Sqlite Transaction - public MySqlTransaction GetTransaction() + public override DbTransaction GetTransaction() + { + return _context.Database.CurrentTransaction?.GetDbTransaction(); + } + + /// + /// Rolls back a transaction + /// + public override async Task RollbackAsync(CancellationToken cancellationToken = default) { - return (MySqlTransaction)_context.Database.CurrentTransaction?.GetDbTransaction(); + if (HasOpenTransaction) + { + try { await ((MySqlTransaction)GetTransaction()).RollbackAsync(cancellationToken); } catch (Exception) { /* Ignore*/} + Transaction = null; + } } - public bool HasOpenTransaction { get => _context.Database.CurrentTransaction != null; } - public bool IsSharedConnection { get => true; } } } diff --git a/src/Paramore.Brighter.MySql/IMySqlConnectionProvider.cs b/src/Paramore.Brighter.MySql/IMySqlConnectionProvider.cs deleted file mode 100644 index f7292c8457..0000000000 --- a/src/Paramore.Brighter.MySql/IMySqlConnectionProvider.cs +++ /dev/null @@ -1,65 +0,0 @@ -#region Licence - /* The MIT License (MIT) - Copyright © 2021 Ian Cooper - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the “Software”), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. */ - -#endregion - -using System.Threading; -using System.Threading.Tasks; -using MySqlConnector; - -namespace Paramore.Brighter.MySql -{ - /// - /// Use to get a connection for a MySql store, used with the Outbox to ensure that we can have a transaction that spans the entity and the message to be sent - /// - public interface IMySqlConnectionProvider - { - /// - /// Gets the connection we should use for the database - /// - /// A Sqlite connection - MySqlConnection GetConnection(); - - /// - /// Gets the connections we should use for the database - /// - /// Cancels the operation - /// A Sqlite connection - Task GetConnectionAsync(CancellationToken cancellationToken = default); - - /// - /// Is there an ambient transaction? If so return it - /// - /// A Sqlite Transaction - MySqlTransaction GetTransaction(); - - /// - /// Is there an open transaction - /// - bool HasOpenTransaction { get; } - - /// - /// Is this connection created externally? In which case don't close it as you don't own it. - /// - bool IsSharedConnection { get; } - } -} diff --git a/src/Paramore.Brighter.MySql/IMySqlTransactionConnectionProvider.cs b/src/Paramore.Brighter.MySql/IMySqlTransactionConnectionProvider.cs deleted file mode 100644 index 16324cbb75..0000000000 --- a/src/Paramore.Brighter.MySql/IMySqlTransactionConnectionProvider.cs +++ /dev/null @@ -1,31 +0,0 @@ -#region Licence - /* The MIT License (MIT) - Copyright © 2021 Ian Cooper - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the “Software”), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. */ - - #endregion - -namespace Paramore.Brighter.MySql -{ - public interface IMySqlTransactionConnectionProvider : IMySqlConnectionProvider, IAmABoxTransactionConnectionProvider - { - - } -} diff --git a/src/Paramore.Brighter.MySql/MySqlConfiguration.cs b/src/Paramore.Brighter.MySql/MySqlConfiguration.cs deleted file mode 100644 index cca6c8d97c..0000000000 --- a/src/Paramore.Brighter.MySql/MySqlConfiguration.cs +++ /dev/null @@ -1,67 +0,0 @@ -#region Licence - /* The MIT License (MIT) - Copyright © 2021 Ian Cooper - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the “Software”), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. */ - -#endregion - -namespace Paramore.Brighter.MySql -{ - public class MySqlConfiguration - { - /// - /// Initializes a new instance of the class. - /// - /// The connection string. - /// Name of the outbox table. - /// Name of the inbox table. - /// Name of the queue store table. - public MySqlConfiguration(string connectionString, string outBoxTableName = null, string inboxTableName = null, string queueStoreTable = null) - { - OutBoxTableName = outBoxTableName; - ConnectionString = connectionString; - InBoxTableName = inboxTableName; - QueueStoreTable = queueStoreTable; - } - - /// - /// Gets the connection string. - /// - /// The connection string. - public string ConnectionString { get; private set; } - - /// - /// Gets the name of the outbox table. - /// - /// The name of the outbox table. - public string OutBoxTableName { get; private set; } - - /// - /// Gets the name of the inbox table. - /// - /// The name of the inbox table. - public string InBoxTableName { get; private set; } - - /// - /// Gets the name of the queue table. - /// - public string QueueStoreTable { get; private set; } - } -} diff --git a/src/Paramore.Brighter.MySql/MySqlConnectionProvider.cs b/src/Paramore.Brighter.MySql/MySqlConnectionProvider.cs index 739199e129..a16aa16057 100644 --- a/src/Paramore.Brighter.MySql/MySqlConnectionProvider.cs +++ b/src/Paramore.Brighter.MySql/MySqlConnectionProvider.cs @@ -1,27 +1,31 @@ #region Licence - /* The MIT License (MIT) - Copyright © 2021 Ian Cooper - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the “Software”), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. */ - + +/* The MIT License (MIT) +Copyright © 2021 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the “Software”), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + #endregion +using System; +using System.Data; +using System.Data.Common; using System.Threading; using System.Threading.Tasks; using MySqlConnector; @@ -31,39 +35,43 @@ namespace Paramore.Brighter.MySql /// /// A connection provider that uses the connection string to create a connection /// - public class MySqlConnectionProvider : IMySqlConnectionProvider + public class MySqlConnectionProvider : RelationalDbConnectionProvider { private readonly string _connectionString; /// - /// Initialise a new instance of Sqlte Connection provider from a connection string + /// Initialise a new instance of MySql Connection provider from a connection string /// - /// Ms Sql Configuration - public MySqlConnectionProvider(MySqlConfiguration configuration) + /// MySql Configuration + public MySqlConnectionProvider(IAmARelationalDatabaseConfiguration configuration) { + if (string.IsNullOrWhiteSpace(configuration?.ConnectionString)) + throw new ArgumentNullException(nameof(configuration.ConnectionString)); _connectionString = configuration.ConnectionString; } - public MySqlConnection GetConnection() - { - return new MySqlConnection(_connectionString); - } - - public async Task GetConnectionAsync(CancellationToken cancellationToken = default) + /// + /// Creates and opens a MySql Connection + /// This is not a shared connection and you should manage its lifetime + /// + /// + public override DbConnection GetConnection() { - var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - - tcs.SetResult(GetConnection()); - return await tcs.Task; + var connection = new MySqlConnection(_connectionString); + if (connection.State != ConnectionState.Open) connection.Open(); + return connection; } - public MySqlTransaction GetTransaction() + /// + /// Creates and opens a MySql Connection + /// This is not a shared connection and you should manage its lifetime + /// + /// + public override async Task GetConnectionAsync(CancellationToken cancellationToken = default) { - //This Connection Factory does not support Transactions - return null; + var connection = new MySqlConnection(_connectionString); + if (connection.State != ConnectionState.Open) await connection.OpenAsync(cancellationToken); + return connection; } - - public bool HasOpenTransaction { get => false; } - public bool IsSharedConnection { get => false; } } } diff --git a/src/Paramore.Brighter.MySql/MySqlUnitOfWork.cs b/src/Paramore.Brighter.MySql/MySqlUnitOfWork.cs new file mode 100644 index 0000000000..a30c435d20 --- /dev/null +++ b/src/Paramore.Brighter.MySql/MySqlUnitOfWork.cs @@ -0,0 +1,118 @@ +#region Licence + +/* The MIT License (MIT) +Copyright © 2021 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the “Software”), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +using System; +using System.Data; +using System.Data.Common; +using System.Threading; +using System.Threading.Tasks; +using MySqlConnector; + +namespace Paramore.Brighter.MySql +{ + /// + /// A connection provider that uses the connection string to create a connection + /// + public class MySqlUnitOfWork : RelationalDbTransactionProvider + { + private readonly string _connectionString; + + /// + /// Initialise a new instance of MySql Connection provider from a connection string + /// + /// MySql Configuration + public MySqlUnitOfWork(IAmARelationalDatabaseConfiguration configuration) + { + if (string.IsNullOrWhiteSpace(configuration?.ConnectionString)) + throw new ArgumentNullException(nameof(configuration.ConnectionString)); + _connectionString = configuration.ConnectionString; + } + + /// + /// Commit the transaction + /// + /// An awaitable Task + public override Task CommitAsync(CancellationToken cancellationToken) + { + if (HasOpenTransaction) + { + ((MySqlTransaction)Transaction).CommitAsync(cancellationToken); + Transaction = null; + } + + return Task.CompletedTask; + } + + /// + /// Creates and opens a MySql Connection + /// + /// + public override DbConnection GetConnection() + { + if (Connection == null) Connection = new MySqlConnection(_connectionString); + if (Connection.State != ConnectionState.Open) Connection.Open(); + return Connection; + } + + /// + /// Creates and opens a MySql Connection + /// This is a shared connection and you should manage it through the unit of work + /// + /// + public override async Task GetConnectionAsync(CancellationToken cancellationToken = default) + { + if (Connection == null) Connection = new MySqlConnection(_connectionString); + if (Connection.State != ConnectionState.Open) await Connection.OpenAsync(); + return Connection; + } + + /// + /// Creates and opens a MySql Transaction + /// This is a shared transaction and you should manage it through the unit of work + /// + /// A shared transaction + public override DbTransaction GetTransaction() + { + if (Connection == null) Connection = GetConnection(); + if (!HasOpenTransaction) + Transaction = ((MySqlConnection) Connection).BeginTransaction(); + return Transaction; + } + + /// + /// Creates and opens a MySql Transaction + /// This is a shared transaction and you should manage it through the unit of work + /// + /// A shared transaction + public override async Task GetTransactionAsync(CancellationToken cancellationToken = default) + { + if (Connection == null) Connection = await GetConnectionAsync(cancellationToken); + if (!HasOpenTransaction) + Transaction = await ((MySqlConnection) Connection).BeginTransactionAsync(cancellationToken); + return Transaction; + } + + } +} diff --git a/src/Paramore.Brighter.Outbox.DynamoDB/DynamoDbOutbox.cs b/src/Paramore.Brighter.Outbox.DynamoDB/DynamoDbOutbox.cs index f41be03e0b..c31643455f 100644 --- a/src/Paramore.Brighter.Outbox.DynamoDB/DynamoDbOutbox.cs +++ b/src/Paramore.Brighter.Outbox.DynamoDB/DynamoDbOutbox.cs @@ -38,8 +38,8 @@ THE SOFTWARE. */ namespace Paramore.Brighter.Outbox.DynamoDB { public class DynamoDbOutbox : - IAmAnOutboxSync, - IAmAnOutboxAsync + IAmAnOutboxSync, + IAmAnOutboxAsync { private readonly DynamoDbConfiguration _configuration; private readonly DynamoDBContext _context; @@ -92,7 +92,11 @@ public DynamoDbOutbox(DynamoDBContext context, DynamoDbConfiguration configurati /// /// The message to be stored /// Timeout in milliseconds; -1 for default timeout - public void Add(Message message, int outBoxTimeout = -1, IAmABoxTransactionConnectionProvider transactionConnectionProvider = null) + public void Add( + Message message, + int outBoxTimeout = -1, + IAmABoxTransactionProvider transactionProvider = null + ) { AddAsync(message, outBoxTimeout).ConfigureAwait(ContinueOnCapturedContext).GetAwaiter().GetResult(); } @@ -103,16 +107,20 @@ public void Add(Message message, int outBoxTimeout = -1, IAmABoxTransactionConne /// /// The message to be stored /// Timeout in milliseconds; -1 for default timeout - /// Allows the sender to cancel the request pipeline. Optional - public async Task AddAsync(Message message, int outBoxTimeout = -1, CancellationToken cancellationToken = default, IAmABoxTransactionConnectionProvider transactionConnectionProvider = null) + /// Allows the sender to cancel the request pipeline. Optional + /// + public async Task AddAsync(Message message, + int outBoxTimeout = -1, + CancellationToken cancellationToken = default, + IAmABoxTransactionProvider transactionProvider = null) { var shard = GetShardNumber(); var expiresAt = GetExpirationTime(); var messageToStore = new MessageItem(message, shard, expiresAt); - if (transactionConnectionProvider != null) + if (transactionProvider != null) { - await AddToTransactionWrite(messageToStore, (DynamoDbUnitOfWork)transactionConnectionProvider); + await AddToTransactionWrite(messageToStore, (DynamoDbUnitOfWork)transactionProvider); } else { @@ -237,7 +245,12 @@ public Task> GetAsync( /// The id of the message to update /// When was the message dispatched, defaults to UTC now /// Allows the sender to cancel the request pipeline. Optional - public async Task MarkDispatchedAsync(Guid id, DateTime? dispatchedAt = null, Dictionary args = null, CancellationToken cancellationToken = default) + public async Task MarkDispatchedAsync( + Guid id, + DateTime? dispatchedAt = null, + Dictionary args = null, + CancellationToken cancellationToken = default + ) { var message = await _context.LoadAsync(id.ToString(), _dynamoOverwriteTableConfig, cancellationToken); MarkMessageDispatched(dispatchedAt, message); @@ -248,8 +261,12 @@ await _context.SaveAsync( cancellationToken); } - public async Task MarkDispatchedAsync(IEnumerable ids, DateTime? dispatchedAt = null, Dictionary args = null, - CancellationToken cancellationToken = default) + public async Task MarkDispatchedAsync( + IEnumerable ids, + DateTime? dispatchedAt = null, + Dictionary args = null, + CancellationToken cancellationToken = default + ) { foreach(var messageId in ids) { @@ -302,8 +319,9 @@ public void MarkDispatched(Guid id, DateTime? dispatchedAt = null, Dictionary @@ -385,7 +403,7 @@ private Task AddToTransactionWrite(MessageItem messag var tcs = new TaskCompletionSource(); var attributes = _context.ToDocument(messageToStore, _dynamoOverwriteTableConfig).ToAttributeMap(); - var transaction = dynamoDbUnitOfWork.BeginOrGetTransaction(); + var transaction = dynamoDbUnitOfWork.GetTransaction(); transaction.TransactItems.Add(new TransactWriteItem{Put = new Put{TableName = _configuration.TableName, Item = attributes}}); tcs.SetResult(transaction); return tcs.Task; diff --git a/src/Paramore.Brighter.Outbox.DynamoDB/MessageItem.cs b/src/Paramore.Brighter.Outbox.DynamoDB/MessageItem.cs index 32afa80fcf..a52d53e148 100644 --- a/src/Paramore.Brighter.Outbox.DynamoDB/MessageItem.cs +++ b/src/Paramore.Brighter.Outbox.DynamoDB/MessageItem.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Text; using System.Text.Json; using Amazon.DynamoDBv2.DataModel; @@ -34,7 +35,7 @@ public class MessageItem public string CorrelationId { get; set; } /// - /// The time at which the message was created, formatted as a string yyyy-MM-dd + /// The time at which the message was created, formatted as a string yyyy-MM-ddTHH:mm:ss.fffZ /// public string CreatedAt { get; set; } @@ -120,7 +121,7 @@ public MessageItem(Message message, int shard = 0, long? expiresAt = null) ContentType = message.Header.ContentType; CorrelationId = message.Header.CorrelationId.ToString(); CharacterEncoding = message.Body.CharacterEncoding.ToString(); - CreatedAt = $"{date}"; + CreatedAt = date.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"); CreatedTime = date.Ticks; DeliveryTime = 0; HeaderBag = JsonSerializer.Serialize(message.Header.Bag, JsonSerialisationOptions.Options); @@ -142,7 +143,7 @@ public Message ConvertToMessage() JsonSerialisationOptions.Options); var messageId = Guid.Parse(MessageId); var messageType = (MessageType)Enum.Parse(typeof(MessageType), MessageType); - var timestamp = DateTime.Parse(CreatedAt); + var timestamp = new DateTime(CreatedTime, DateTimeKind.Utc); var header = new MessageHeader( messageId: messageId, @@ -163,12 +164,6 @@ public Message ConvertToMessage() return new Message(header, body); } - - public void MarkMessageDelivered(DateTime deliveredAt) - { - DeliveryTime = deliveredAt.Ticks; - DeliveredAt = $"{deliveredAt:yyyy-MM-dd}"; - } } public class MessageItemBodyConverter : IPropertyConverter diff --git a/src/Paramore.Brighter.Outbox.DynamoDB/ServiceCollectionExtensions.cs b/src/Paramore.Brighter.Outbox.DynamoDB/ServiceCollectionExtensions.cs deleted file mode 100644 index 85e1765131..0000000000 --- a/src/Paramore.Brighter.Outbox.DynamoDB/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System; -using Amazon.DynamoDBv2; -using Microsoft.Extensions.DependencyInjection; -using Paramore.Brighter.Extensions.DependencyInjection; - -namespace Paramore.Brighter.Outbox.DynamoDB -{ - public static class ServiceCollectionExtensions - { - /// - /// Registers a DynamoDb Outbox. This helper registers - /// - IAmAnOutboxSync - /// - IAmAnOutboxAsync - /// - /// You will need to register the following BEFORE calling this extension - /// - IAmazonDynamoDb - /// - DynamoDbConfiguration - /// We do not register these, as we assume you will need to register them for your code's access to DynamoDb - /// So we assume that prerequisite has taken place beforehand - /// - /// The lifetime of the outbox connection - /// - public static IBrighterBuilder UseDynamoDbOutbox( - this IBrighterBuilder brighterBuilder, ServiceLifetime serviceLifetime = ServiceLifetime.Singleton) - { - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmAnOutboxSync), BuildDynamoDbOutbox, serviceLifetime)); - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmAnOutboxAsync), BuildDynamoDbOutbox, serviceLifetime)); - - return brighterBuilder; - } - - /// - /// Use this transaction provider to ensure that the Outbox and the Entity Store are correct - /// - /// Allows extension method - /// What is the type of the connection provider - /// What is the lifetime of registered interfaces - /// Allows fluent syntax - /// This is paired with Use Outbox (above) when required - /// Registers the following - /// -- IAmABoxTransactionConnectionProvider: the provider of a connection for any existing transaction - public static IBrighterBuilder UseDynamoDbTransactionConnectionProvider( - this IBrighterBuilder brighterBuilder, Type connectionProvider, - ServiceLifetime serviceLifetime = ServiceLifetime.Scoped) - { - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmABoxTransactionConnectionProvider), connectionProvider, serviceLifetime)); - - return brighterBuilder; - } - - private static DynamoDbOutbox BuildDynamoDbOutbox(IServiceProvider provider) - { - var config = provider.GetService(); - if (config == null) - throw new InvalidOperationException("No service of type DynamoDbConfiguration could be found, please register before calling this method"); - var dynamoDb = provider.GetService(); - if (dynamoDb == null) - throw new InvalidOperationException("No service of type IAmazonDynamoDb was found. Please register before calling this method"); - - - return new DynamoDbOutbox(dynamoDb, config); - } - } -} diff --git a/src/Paramore.Brighter.Outbox.EventStore/EventStoreOutboxSync.cs b/src/Paramore.Brighter.Outbox.EventStore/EventStoreOutboxSync.cs index 46120c7287..1bd2a457b7 100644 --- a/src/Paramore.Brighter.Outbox.EventStore/EventStoreOutboxSync.cs +++ b/src/Paramore.Brighter.Outbox.EventStore/EventStoreOutboxSync.cs @@ -31,6 +31,7 @@ THE SOFTWARE. */ using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using System.Transactions; using Microsoft.Extensions.Logging; using EventStore.ClientAPI; using Paramore.Brighter.Logging; @@ -41,9 +42,7 @@ namespace Paramore.Brighter.Outbox.EventStore /// /// Class EventStoreOutbox. /// - public class EventStoreOutboxSync : - IAmAnOutboxSync, - IAmAnOutboxAsync + public class EventStoreOutboxSync : IAmAnOutboxSync, IAmAnOutboxAsync { private static readonly ILogger s_logger = ApplicationLogging.CreateLogger(); @@ -74,8 +73,13 @@ public EventStoreOutboxSync(IEventStoreConnection eventStore) /// /// The message. /// The outBoxTimeout. + /// A transaction provider, leave null with an event store /// Task. - public void Add(Message message, int outBoxTimeout = -1, IAmABoxTransactionConnectionProvider transactionConnectionProvider = null) + public void Add( + Message message, + int outBoxTimeout = -1, + IAmABoxTransactionProvider transactionProvider = null + ) { s_logger.LogDebug("Adding message to Event Store Outbox: {Request}", JsonSerializer.Serialize(message, JsonSerialisationOptions.Options)); @@ -95,9 +99,12 @@ public void Add(Message message, int outBoxTimeout = -1, IAmABoxTransactionConne /// The message. /// The time allowed for the write in milliseconds; on a -1 default /// Allows the sender to cancel the request pipeline. Optional + /// A transaction provider, leave null with an event store /// . - public async Task AddAsync(Message message, int outBoxTimeout = -1, - CancellationToken cancellationToken = default, IAmABoxTransactionConnectionProvider transactionConnectionProvider = null) + public async Task AddAsync(Message message, + int outBoxTimeout = -1, + CancellationToken cancellationToken = default, + IAmABoxTransactionProvider transactionProvider = null) { s_logger.LogDebug("Adding message to Event Store Outbox: {Request}", JsonSerializer.Serialize(message, JsonSerialisationOptions.Options)); @@ -199,7 +206,9 @@ public Task GetAsync( throw new NotImplementedException(); } - public Task> GetAsync(IEnumerable messageIds, int outBoxTimeout = -1, + public Task> GetAsync( + IEnumerable messageIds, + int outBoxTimeout = -1, CancellationToken cancellationToken = default) { throw new NotImplementedException(); @@ -227,8 +236,12 @@ public async Task> GetAsync(string stream, int fromEventNumber, i /// When was the message dispatched, defaults to UTC now /// Additional parameters required for search, if any /// Allows the sender to cancel the request pipeline. Optional - public async Task MarkDispatchedAsync(Guid id, DateTime? dispatchedAt = null, Dictionary args = null, - CancellationToken cancellationToken = default) + public async Task MarkDispatchedAsync( + Guid id, + DateTime? dispatchedAt = null, + Dictionary args = null, + CancellationToken cancellationToken = default + ) { var stream = GetStreamFromArgs(args); @@ -262,14 +275,22 @@ public async Task MarkDispatchedAsync(Guid id, DateTime? dispatchedAt = null, Di await _eventStore.AppendToStreamAsync(stream, nextEventNumber.Value, eventData); } - public Task MarkDispatchedAsync(IEnumerable ids, DateTime? dispatchedAt = null, Dictionary args = null, - CancellationToken cancellationToken = default) + public Task MarkDispatchedAsync( + IEnumerable ids, DateTime? dispatchedAt = null, + Dictionary args = null, + CancellationToken cancellationToken = default + ) { throw new NotImplementedException(); } - public Task> DispatchedMessagesAsync(double millisecondsDispatchedSince, int pageSize = 100, int pageNumber = 1, - int outboxTimeout = -1, Dictionary args = null, CancellationToken cancellationToken = default) + public Task> DispatchedMessagesAsync( + double millisecondsDispatchedSince, + int pageSize = 100, + int pageNumber = 1, + int outboxTimeout = -1, + Dictionary args = null, + CancellationToken cancellationToken = default) { throw new NotImplementedException(); } diff --git a/src/Paramore.Brighter.Outbox.EventStore/ServiceCollectionExtensions.cs b/src/Paramore.Brighter.Outbox.EventStore/ServiceCollectionExtensions.cs index 46e8651860..be66c2676a 100644 --- a/src/Paramore.Brighter.Outbox.EventStore/ServiceCollectionExtensions.cs +++ b/src/Paramore.Brighter.Outbox.EventStore/ServiceCollectionExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Transactions; using EventStore.ClientAPI; using Microsoft.Extensions.DependencyInjection; using Paramore.Brighter.Extensions.DependencyInjection; @@ -12,8 +13,8 @@ public static IBrighterBuilder UseEventStoreOutbox( { brighterBuilder.Services.AddSingleton(connection); - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmAnOutboxSync), BuildEventStoreOutbox, serviceLifetime)); - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmAnOutboxAsync), BuildEventStoreOutbox, serviceLifetime)); + brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmAnOutboxSync), BuildEventStoreOutbox, serviceLifetime)); + brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmAnOutboxAsync), BuildEventStoreOutbox, serviceLifetime)); return brighterBuilder; } diff --git a/src/Paramore.Brighter.Outbox.MsSql/DDL_SCRIPTS/MSSQL/Outbox.sql b/src/Paramore.Brighter.Outbox.MsSql/DDL_SCRIPTS/MSSQL/Outbox.sql deleted file mode 100644 index 61e056d783..0000000000 --- a/src/Paramore.Brighter.Outbox.MsSql/DDL_SCRIPTS/MSSQL/Outbox.sql +++ /dev/null @@ -1,46 +0,0 @@ --- User Table information: --- Number of tables: 1 --- Messages: 0 row(s) - -PRINT 'Creating Messages table'; -CREATE TABLE [Messages] - ( - [Id] [BIGINT] NOT NULL IDENTITY , - [MessageId] UNIQUEIDENTIFIER NOT NULL , - [Topic] NVARCHAR(255) NULL , - [MessageType] NVARCHAR(32) NULL , - [Timestamp] DATETIME NULL , - [CorrelationId] UNIQUEIDENTIFIER NULL, - [ReplyTo] NVARCHAR(255) NULL, - [ContentType] NVARCHAR(128) NULL, - [Dispatched] DATETIME NULL , - [HeaderBag] NTEXT NULL , - [Body] NTEXT NULL , - PRIMARY KEY ( [Id] ) - ); -GO -IF ( NOT EXISTS ( SELECT * - FROM sys.indexes - WHERE name = 'UQ_Messages__MessageId' - AND object_id = OBJECT_ID('Messages') ) - ) - BEGIN - PRINT 'Creating a unique index on the MessageId column of the Messages table...'; - - CREATE UNIQUE NONCLUSTERED INDEX UQ_Messages__MessageId - ON Messages(MessageId); - END; -GO -IF ( NOT EXISTS -( - SELECT * - FROM sys.indexes - WHERE name = 'IQ_Messages__Dispatched' - AND object_id = Object_id('Messages') ) ) -BEGIN - PRINT 'Creating a non-unique index on the Dispatched column of the Messages table...'; - CREATE NONCLUSTERED INDEX IQ_Messages__Dispatched - ON Messages(Dispatched ASC); -END; -GO -PRINT 'Done'; diff --git a/src/Paramore.Brighter.Outbox.MsSql/MsSqlOutbox.cs b/src/Paramore.Brighter.Outbox.MsSql/MsSqlOutbox.cs index 84ed6610e8..8428039224 100644 --- a/src/Paramore.Brighter.Outbox.MsSql/MsSqlOutbox.cs +++ b/src/Paramore.Brighter.Outbox.MsSql/MsSqlOutbox.cs @@ -26,6 +26,7 @@ THE SOFTWARE. */ using System; using System.Collections.Generic; using System.Data; +using System.Data.Common; using System.Linq; using Microsoft.Data.SqlClient; using System.Text.Json; @@ -37,22 +38,21 @@ THE SOFTWARE. */ namespace Paramore.Brighter.Outbox.MsSql { /// - /// Class MsSqlOutbox. + /// Implements an Outbox using MSSQL as a backing store /// - public class MsSqlOutbox : - RelationDatabaseOutboxSync + public class MsSqlOutbox : RelationDatabaseOutbox { private const int MsSqlDuplicateKeyError_UniqueIndexViolation = 2601; private const int MsSqlDuplicateKeyError_UniqueConstraintViolation = 2627; - private readonly MsSqlConfiguration _configuration; - private readonly IMsSqlConnectionProvider _connectionProvider; - + private readonly IAmARelationalDatabaseConfiguration _configuration; + private readonly IAmARelationalDbConnectionProvider _connectionProvider; + /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The configuration. /// The connection factory. - public MsSqlOutbox(MsSqlConfiguration configuration, IMsSqlConnectionProvider connectionProvider) : base( + public MsSqlOutbox(IAmARelationalDatabaseConfiguration configuration, IAmARelationalDbConnectionProvider connectionProvider) : base( configuration.OutBoxTableName, new MsSqlQueries(), ApplicationLogging.CreateLogger()) { _configuration = configuration; @@ -61,16 +61,22 @@ public MsSqlOutbox(MsSqlConfiguration configuration, IMsSqlConnectionProvider co } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The configuration. - public MsSqlOutbox(MsSqlConfiguration configuration) : this(configuration, new MsSqlSqlAuthConnectionProvider(configuration)) { } + public MsSqlOutbox(IAmARelationalDatabaseConfiguration configuration) : this(configuration, + new MsSqlConnectionProvider(configuration)) + { + } - protected override void WriteToStore(IAmABoxTransactionConnectionProvider transactionConnectionProvider, Func commandFunc, Action loggingAction) + protected override void WriteToStore( + IAmABoxTransactionProvider transactionProvider, + Func commandFunc, + Action loggingAction) { var connectionProvider = _connectionProvider; - if (transactionConnectionProvider != null && transactionConnectionProvider is IMsSqlTransactionConnectionProvider provider) - connectionProvider = provider; + if (transactionProvider is IAmARelationalDbConnectionProvider transConnectionProvider) + connectionProvider = transConnectionProvider; var connection = connectionProvider.GetConnection(); @@ -80,8 +86,8 @@ protected override void WriteToStore(IAmABoxTransactionConnectionProvider transa { try { - if (transactionConnectionProvider != null && connectionProvider.HasOpenTransaction) - command.Transaction = connectionProvider.GetTransaction(); + if (transactionProvider != null && transactionProvider.HasOpenTransaction) + command.Transaction = transactionProvider.GetTransaction(); command.ExecuteNonQuery(); } catch (SqlException sqlException) @@ -97,21 +103,26 @@ protected override void WriteToStore(IAmABoxTransactionConnectionProvider transa } finally { - if (!connectionProvider.IsSharedConnection) - connection.Dispose(); - else if (!connectionProvider.HasOpenTransaction) + if (transactionProvider != null) + transactionProvider.Close(); + else connection.Close(); } } } - protected override async Task WriteToStoreAsync(IAmABoxTransactionConnectionProvider transactionConnectionProvider, Func commandFunc, Action loggingAction, CancellationToken cancellationToken) + protected override async Task WriteToStoreAsync( + IAmABoxTransactionProvider transactionProvider, + Func commandFunc, + Action loggingAction, + CancellationToken cancellationToken) { var connectionProvider = _connectionProvider; - if (transactionConnectionProvider != null && transactionConnectionProvider is IMsSqlTransactionConnectionProvider provider) - connectionProvider = provider; + if (transactionProvider is IAmARelationalDbConnectionProvider transConnectionProvider) + connectionProvider = transConnectionProvider; - var connection = await connectionProvider.GetConnectionAsync(cancellationToken).ConfigureAwait(ContinueOnCapturedContext); + var connection = await connectionProvider.GetConnectionAsync(cancellationToken) + .ConfigureAwait(ContinueOnCapturedContext); if (connection.State != ConnectionState.Open) await connection.OpenAsync(cancellationToken); @@ -119,8 +130,8 @@ protected override async Task WriteToStoreAsync(IAmABoxTransactionConnectionProv { try { - if (transactionConnectionProvider != null && connectionProvider.HasOpenTransaction) - command.Transaction = connectionProvider.GetTransaction(); + if (transactionProvider != null && transactionProvider.HasOpenTransaction) + command.Transaction = transactionProvider.GetTransaction(); await command.ExecuteNonQueryAsync(cancellationToken); } catch (SqlException sqlException) @@ -136,15 +147,18 @@ protected override async Task WriteToStoreAsync(IAmABoxTransactionConnectionProv } finally { - if (!connectionProvider.IsSharedConnection) - connection.Dispose(); - else if (!connectionProvider.HasOpenTransaction) + if (transactionProvider != null) + transactionProvider.Close(); + else connection.Close(); } } } - protected override T ReadFromStore(Func commandFunc, Func resultFunc) + protected override T ReadFromStore( + Func commandFunc, + Func resultFunc + ) { var connection = _connectionProvider.GetConnection(); @@ -158,15 +172,16 @@ protected override T ReadFromStore(Func commandFun } finally { - if (!_connectionProvider.IsSharedConnection) - connection.Dispose(); - else if (!_connectionProvider.HasOpenTransaction) - connection.Close(); + connection.Close(); } } } - protected override async Task ReadFromStoreAsync(Func commandFunc, Func> resultFunc, CancellationToken cancellationToken) + protected override async Task ReadFromStoreAsync( + Func commandFunc, + Func> resultFunc, + CancellationToken cancellationToken + ) { var connection = await _connectionProvider.GetConnectionAsync(cancellationToken); @@ -180,15 +195,17 @@ protected override async Task ReadFromStoreAsync(Func dr.GetString(dr.GetOrdinal("Topic")); + private static string GetTopic(DbDataReader dr) => dr.GetString(dr.GetOrdinal("Topic")); - private static MessageType GetMessageType(SqlDataReader dr) => (MessageType) Enum.Parse(typeof (MessageType), dr.GetString(dr.GetOrdinal("MessageType"))); + private static MessageType GetMessageType(DbDataReader dr) => + (MessageType)Enum.Parse(typeof(MessageType), dr.GetString(dr.GetOrdinal("MessageType"))); - private static Guid GetMessageId(SqlDataReader dr) => dr.GetGuid(dr.GetOrdinal("MessageId")); + private static Guid GetMessageId(DbDataReader dr) => dr.GetGuid(dr.GetOrdinal("MessageId")); - private string GetContentType(SqlDataReader dr) + private string GetContentType(DbDataReader dr) { var ordinal = dr.GetOrdinal("ContentType"); - if (dr.IsDBNull(ordinal)) return null; - - var replyTo = dr.GetString(ordinal); - return replyTo; + if (dr.IsDBNull(ordinal)) return null; + + var contentType = dr.GetString(ordinal); + return contentType; } - private string GetReplyTo(SqlDataReader dr) + private string GetReplyTo(DbDataReader dr) { - var ordinal = dr.GetOrdinal("ReplyTo"); - if (dr.IsDBNull(ordinal)) return null; - - var replyTo = dr.GetString(ordinal); - return replyTo; + var ordinal = dr.GetOrdinal("ReplyTo"); + if (dr.IsDBNull(ordinal)) return null; + + var replyTo = dr.GetString(ordinal); + return replyTo; } - private static Dictionary GetContextBag(SqlDataReader dr) + private static Dictionary GetContextBag(DbDataReader dr) { var i = dr.GetOrdinal("HeaderBag"); var headerBag = dr.IsDBNull(i) ? "" : dr.GetString(i); - var dictionaryBag = JsonSerializer.Deserialize>(headerBag, JsonSerialisationOptions.Options); + var dictionaryBag = + JsonSerializer.Deserialize>(headerBag, JsonSerialisationOptions.Options); return dictionaryBag; } - private Guid? GetCorrelationId(SqlDataReader dr) + private Guid? GetCorrelationId(DbDataReader dr) { var ordinal = dr.GetOrdinal("CorrelationId"); - if (dr.IsDBNull(ordinal)) return null; - + if (dr.IsDBNull(ordinal)) return null; + var correlationId = dr.GetGuid(ordinal); return correlationId; } - private static DateTime GetTimeStamp(SqlDataReader dr) + private static DateTime GetTimeStamp(DbDataReader dr) { var ordinal = dr.GetOrdinal("Timestamp"); var timeStamp = dr.IsDBNull(ordinal) @@ -288,59 +366,95 @@ private static DateTime GetTimeStamp(SqlDataReader dr) return timeStamp; } + private string GetPartitionKey(DbDataReader dr) + { + var ordinal = dr.GetOrdinal("PartitionKey"); + if (dr.IsDBNull(ordinal)) return null; + + var partitionKey = dr.GetString(ordinal); + return partitionKey; + } + + private byte[] GetBodyAsBytes(DbDataReader dr) + { + var ordinal = dr.GetOrdinal("Body"); + if (dr.IsDBNull(ordinal)) return null; + + var body = dr.GetStream(ordinal); + long bodyLength = body.Length; + var buffer = new byte[bodyLength]; + body.Read(buffer, 0, (int)bodyLength); + return buffer; + } + + private static string GetBodyAsText(DbDataReader dr) + { + var ordinal = dr.GetOrdinal("Body"); + return dr.IsDBNull(ordinal) ? null : dr.GetString(ordinal); + } + #endregion #region DataReader Operators - protected override Message MapFunction(SqlDataReader dr) + + protected override Message MapFunction(DbDataReader dr) { Message message = null; if (dr.Read()) { message = MapAMessage(dr); } + dr.Close(); return message ?? new Message(); } - - protected override async Task MapFunctionAsync(SqlDataReader dr, CancellationToken cancellationToken) + + protected override async Task MapFunctionAsync(DbDataReader dr, CancellationToken cancellationToken) { Message message = null; if (await dr.ReadAsync(cancellationToken)) { message = MapAMessage(dr); } + dr.Close(); return message ?? new Message(); } - protected override IEnumerable MapListFunction(SqlDataReader dr) + protected override IEnumerable MapListFunction(DbDataReader dr) { var messages = new List(); while (dr.Read()) { messages.Add(MapAMessage(dr)); } + dr.Close(); return messages; } - protected override async Task> MapListFunctionAsync(SqlDataReader dr, CancellationToken cancellationToken) + protected override async Task> MapListFunctionAsync( + DbDataReader dr, + CancellationToken cancellationToken + ) { var messages = new List(); while (await dr.ReadAsync(cancellationToken)) { messages.Add(MapAMessage(dr)); } + dr.Close(); return messages; } + #endregion - private Message MapAMessage(SqlDataReader dr) + private Message MapAMessage(DbDataReader dr) { var id = GetMessageId(dr); var messageType = GetMessageType(dr); @@ -348,41 +462,38 @@ private Message MapAMessage(SqlDataReader dr) var header = new MessageHeader(id, topic, messageType); - //new schema....we've got the extra header information - if (dr.FieldCount > 4) + DateTime timeStamp = GetTimeStamp(dr); + var correlationId = GetCorrelationId(dr); + var replyTo = GetReplyTo(dr); + var contentType = GetContentType(dr); + var partitionKey = GetPartitionKey(dr); + + header = new MessageHeader( + messageId: id, + topic: topic, + messageType: messageType, + timeStamp: timeStamp, + handledCount: 0, + delayedMilliseconds: 0, + correlationId: correlationId, + replyTo: replyTo, + contentType: contentType, + partitionKey: partitionKey); + + Dictionary dictionaryBag = GetContextBag(dr); + if (dictionaryBag != null) { - DateTime timeStamp = GetTimeStamp(dr); - var correlationId = GetCorrelationId(dr); - var replyTo = GetReplyTo(dr); - var contentType = GetContentType(dr); - - header = new MessageHeader( - messageId: id, - topic: topic, - messageType: messageType, - timeStamp: timeStamp, - handledCount: 0, - delayedMilliseconds: 0, - correlationId: correlationId, - replyTo: replyTo, - contentType: contentType); - - Dictionary dictionaryBag = GetContextBag(dr); - if (dictionaryBag != null) + foreach (var key in dictionaryBag.Keys) { - foreach (var key in dictionaryBag.Keys) - { - header.Bag.Add(key, dictionaryBag[key]); - } + header.Bag.Add(key, dictionaryBag[key]); } } var bodyOrdinal = dr.GetOrdinal("Body"); string messageBody = string.Empty; - if (!dr.IsDBNull(bodyOrdinal)) - messageBody = dr.GetString(bodyOrdinal); - var body = new MessageBody(messageBody); - + var body = _configuration.BinaryMessagePayload + ? new MessageBody(GetBodyAsBytes((SqlDataReader)dr), "application/octet-stream", CharacterEncoding.Raw) + : new MessageBody(GetBodyAsText(dr), "application/json", CharacterEncoding.UTF8); return new Message(header, body); } } diff --git a/src/Paramore.Brighter.Outbox.MsSql/MsSqlQueries.cs b/src/Paramore.Brighter.Outbox.MsSql/MsSqlQueries.cs index 422eb58633..195df1fb89 100644 --- a/src/Paramore.Brighter.Outbox.MsSql/MsSqlQueries.cs +++ b/src/Paramore.Brighter.Outbox.MsSql/MsSqlQueries.cs @@ -5,14 +5,13 @@ public class MsSqlQueries : IRelationDatabaseOutboxQueries public string PagedDispatchedCommand { get; } = "SELECT * FROM (SELECT ROW_NUMBER() OVER(ORDER BY Timestamp DESC) AS NUMBER, * FROM {0}) AS TBL WHERE DISPATCHED IS NOT NULL AND DISPATCHED < DATEADD(millisecond, @OutStandingSince, getutcdate()) AND NUMBER BETWEEN ((@PageNumber-1)*@PageSize+1) AND (@PageNumber*@PageSize) ORDER BY Timestamp DESC"; public string PagedReadCommand { get; } = "SELECT * FROM (SELECT ROW_NUMBER() OVER(ORDER BY Timestamp DESC) AS NUMBER, * FROM {0}) AS TBL WHERE NUMBER BETWEEN ((@PageNumber-1)*@PageSize+1) AND (@PageNumber*@PageSize) ORDER BY Timestamp DESC"; public string PagedOutstandingCommand { get; } = "SELECT * FROM (SELECT ROW_NUMBER() OVER(ORDER BY Timestamp ASC) AS NUMBER, * FROM {0} WHERE DISPATCHED IS NULL) AS TBL WHERE TIMESTAMP < DATEADD(millisecond, -@OutStandingSince, getutcdate()) AND NUMBER BETWEEN ((@PageNumber-1)*@PageSize+1) AND (@PageNumber*@PageSize) ORDER BY Timestamp ASC"; - public string AddCommand { get; } = "INSERT INTO {0} (MessageId, MessageType, Topic, Timestamp, CorrelationId, ReplyTo, ContentType, HeaderBag, Body) VALUES (@MessageId, @MessageType, @Topic, @Timestamp, @CorrelationId, @ReplyTo, @ContentType, @HeaderBag, @Body)"; - public string BulkAddCommand { get; } = "INSERT INTO {0} (MessageId, MessageType, Topic, Timestamp, CorrelationId, ReplyTo, ContentType, HeaderBag, Body) VALUES {1}"; + public string AddCommand { get; } = "INSERT INTO {0} (MessageId, MessageType, Topic, Timestamp, CorrelationId, ReplyTo, ContentType, PartitionKey, HeaderBag, Body) VALUES (@MessageId, @MessageType, @Topic, @Timestamp, @CorrelationId, @ReplyTo, @ContentType, @PartitionKey, @HeaderBag, @Body)"; + public string BulkAddCommand { get; } = "INSERT INTO {0} (MessageId, MessageType, Topic, Timestamp, CorrelationId, ReplyTo, ContentType, PartitionKey, HeaderBag, Body) VALUES {1}"; public string MarkDispatchedCommand { get; } = "UPDATE {0} SET Dispatched = @DispatchedAt WHERE MessageId = @MessageId"; public string MarkMultipleDispatchedCommand { get; } = "UPDATE {0} SET Dispatched = @DispatchedAt WHERE MessageId in ( {1} )"; public string GetMessageCommand { get; } = "SELECT * FROM {0} WHERE MessageId = @MessageId"; public string GetMessagesCommand { get; } = "SELECT * FROM {0} WHERE MessageId IN ( {1} )"; public string DeleteMessagesCommand { get; } = "DELETE FROM {0} WHERE MessageId IN ( {1} )"; - public string DispatchedCommand { get; } = "Select top(@PageSize) * FROM {0} WHERE Dispatched is not NULL and Dispatched < DATEADD(hour, @DispatchedSince, getutcdate()) Order BY Dispatched"; } } diff --git a/src/Paramore.Brighter.Outbox.MsSql/Paramore.Brighter.Outbox.MsSql.csproj b/src/Paramore.Brighter.Outbox.MsSql/Paramore.Brighter.Outbox.MsSql.csproj index dedbec7273..c1a0e16c2f 100644 --- a/src/Paramore.Brighter.Outbox.MsSql/Paramore.Brighter.Outbox.MsSql.csproj +++ b/src/Paramore.Brighter.Outbox.MsSql/Paramore.Brighter.Outbox.MsSql.csproj @@ -8,9 +8,6 @@ - - - diff --git a/src/Paramore.Brighter.Outbox.MsSql/README.md b/src/Paramore.Brighter.Outbox.MsSql/README.md deleted file mode 100644 index 8ee71c1a95..0000000000 --- a/src/Paramore.Brighter.Outbox.MsSql/README.md +++ /dev/null @@ -1,47 +0,0 @@ -# Brighter MSSQL outbox - -## Setup - -To setup Brighter with a SQL Server or SQL CE outbox, some steps are required: - -#### Create a table with the schema in the example - -You can use the following example as a reference for SQL Server: - -```sql - CREATE TABLE MyOutbox ( - Id uniqueidentifier CONSTRAINT PK_MessageId PRIMARY KEY, - Topic nvarchar(255), - MessageType nvarchar(32), - Body nvarchar(max) - ) -``` -If you're using SQL CE you have to replace `nvarchar(max)` with a supported type, for example `ntext`. - -#### Configure the command processor - -The following is an example of how to configure a command processor with a SQL Server outbox. - -```csharp -var msSqlOutbox = new MsSqlOutbox(new MsSqlOutboxConfiguration( - "myconnectionstring", - "MyOutboxTable", - MsSqlOutboxConfiguration.DatabaseType.MsSqlServer - ), myLogger), - -var commandProcessor = CommandProcessorBuilder.With() - .Handlers(new HandlerConfiguration(mySubscriberRegistry, myHandlerFactory)) - .Policies(myPolicyRegistry) - .Logger(myLogger) - .TaskQueues(new MessagingConfiguration( - outbox: msSqlOutbox, - messagingGateway: myGateway, - messageMapperRegistry: myMessageMapperRegistry - )) - .RequestContextFactory(new InMemoryRequestContextFactory()) - .Build(); -``` - -> The values for the `MsSqlOutboxConfiguration.DatabaseType` enum are the following: -> `MsSqlServer` -> `SqlCe` diff --git a/src/Paramore.Brighter.Outbox.MsSql/README.txt b/src/Paramore.Brighter.Outbox.MsSql/README.txt deleted file mode 100644 index 8ee71c1a95..0000000000 --- a/src/Paramore.Brighter.Outbox.MsSql/README.txt +++ /dev/null @@ -1,47 +0,0 @@ -# Brighter MSSQL outbox - -## Setup - -To setup Brighter with a SQL Server or SQL CE outbox, some steps are required: - -#### Create a table with the schema in the example - -You can use the following example as a reference for SQL Server: - -```sql - CREATE TABLE MyOutbox ( - Id uniqueidentifier CONSTRAINT PK_MessageId PRIMARY KEY, - Topic nvarchar(255), - MessageType nvarchar(32), - Body nvarchar(max) - ) -``` -If you're using SQL CE you have to replace `nvarchar(max)` with a supported type, for example `ntext`. - -#### Configure the command processor - -The following is an example of how to configure a command processor with a SQL Server outbox. - -```csharp -var msSqlOutbox = new MsSqlOutbox(new MsSqlOutboxConfiguration( - "myconnectionstring", - "MyOutboxTable", - MsSqlOutboxConfiguration.DatabaseType.MsSqlServer - ), myLogger), - -var commandProcessor = CommandProcessorBuilder.With() - .Handlers(new HandlerConfiguration(mySubscriberRegistry, myHandlerFactory)) - .Policies(myPolicyRegistry) - .Logger(myLogger) - .TaskQueues(new MessagingConfiguration( - outbox: msSqlOutbox, - messagingGateway: myGateway, - messageMapperRegistry: myMessageMapperRegistry - )) - .RequestContextFactory(new InMemoryRequestContextFactory()) - .Build(); -``` - -> The values for the `MsSqlOutboxConfiguration.DatabaseType` enum are the following: -> `MsSqlServer` -> `SqlCe` diff --git a/src/Paramore.Brighter.Outbox.MsSql/ServiceCollectionExtensions.cs b/src/Paramore.Brighter.Outbox.MsSql/ServiceCollectionExtensions.cs deleted file mode 100644 index c4d7368904..0000000000 --- a/src/Paramore.Brighter.Outbox.MsSql/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System; -using System.Runtime.CompilerServices; -using Microsoft.Extensions.DependencyInjection; -using Paramore.Brighter.Extensions.DependencyInjection; -using Paramore.Brighter.MsSql; - -namespace Paramore.Brighter.Outbox.MsSql -{ - public static class ServiceCollectionExtensions - { - /// - /// Use MsSql for the Outbox - /// - /// Allows extension method syntax - /// The connection for the Db and name of the Outbox table - /// What is the type for the class that lets us obtain connections for the Sqlite database - /// What is the lifetime of the services that we add - /// Allows fluent syntax - /// -- Registers the following - /// -- MsSqlConfiguration: connection string and outbox name - /// -- IMsSqlConnectionProvider: lets us get a connection for the outbox that matches the entity store - /// -- IAmAnOutbox: an outbox to store messages we want to send - /// -- IAmAnOutboxAsync: an outbox to store messages we want to send - /// -- IAmAnOutboxViewer: Lets us read the entries in the outbox - /// -- IAmAnOutboxViewerAsync: Lets us read the entries in the outbox - public static IBrighterBuilder UseMsSqlOutbox( - this IBrighterBuilder brighterBuilder, MsSqlConfiguration configuration, Type connectionProvider, ServiceLifetime serviceLifetime = ServiceLifetime.Singleton, int outboxBulkChunkSize = 100) - { - brighterBuilder.Services.AddSingleton(configuration); - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IMsSqlConnectionProvider), connectionProvider, serviceLifetime)); - - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmAnOutboxSync), BuildMsSqlOutbox, serviceLifetime)); - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmAnOutboxAsync), BuildMsSqlOutbox, serviceLifetime)); - - //Set chunk size - TODO: Bring this inline - brighterBuilder.UseExternalOutbox(null, outboxBulkChunkSize); - - return brighterBuilder; - } - - /// - /// Use this transaction provider to ensure that the Outbox and the Entity Store are correct - /// - /// Allows extension method - /// What is the type of the connection provider - /// What is the lifetime of registered interfaces - /// Allows fluent syntax - /// This is paired with Use Outbox (above) when required - /// Registers the following - /// -- IAmABoxTransactionConnectionProvider: the provider of a connection for any existing transaction - public static IBrighterBuilder UseMsSqlTransactionConnectionProvider( - this IBrighterBuilder brighterBuilder, Type connectionProvider, - ServiceLifetime serviceLifetime = ServiceLifetime.Scoped) - { - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmABoxTransactionConnectionProvider), connectionProvider, serviceLifetime)); - - return brighterBuilder; - } - - private static MsSqlOutbox BuildMsSqlOutbox(IServiceProvider provider) - { - var connectionProvider = provider.GetService(); - var config = provider.GetService(); - - return new MsSqlOutbox(config, connectionProvider); - } - } -} diff --git a/src/Paramore.Brighter.Outbox.MsSql/SqlOutboxBuilder.cs b/src/Paramore.Brighter.Outbox.MsSql/SqlOutboxBuilder.cs index d1ac1dc065..47e8656d81 100644 --- a/src/Paramore.Brighter.Outbox.MsSql/SqlOutboxBuilder.cs +++ b/src/Paramore.Brighter.Outbox.MsSql/SqlOutboxBuilder.cs @@ -22,6 +22,8 @@ THE SOFTWARE. */ #endregion +using System; + namespace Paramore.Brighter.Outbox.MsSql { /// @@ -29,34 +31,59 @@ namespace Paramore.Brighter.Outbox.MsSql /// public class SqlOutboxBuilder { - const string OutboxDdl = @" + const string TextOutboxDdl = @" CREATE TABLE {0} ( [Id] [BIGINT] NOT NULL IDENTITY , - [MessageId] UNIQUEIDENTIFIER NOT NULL , - [Topic] NVARCHAR(255) NULL , - [MessageType] NVARCHAR(32) NULL , - [Timestamp] DATETIME NULL , + [MessageId] UNIQUEIDENTIFIER NOT NULL, + [Topic] NVARCHAR(255) NULL, + [MessageType] NVARCHAR(32) NULL, + [Timestamp] DATETIME NULL, + [CorrelationId] UNIQUEIDENTIFIER NULL, + [ReplyTo] NVARCHAR(255) NULL, + [ContentType] NVARCHAR(128) NULL, + [PartitionKey] NVARCHAR(255) NULL, + [Dispatched] DATETIME NULL, + [HeaderBag] NVARCHAR(MAX) NULL, + [Body] NVARCHAR(MAX) NULL, + PRIMARY KEY ( [Id] ) + ); + "; + + const string BinaryOutboxDdl = @" + CREATE TABLE {0} + ( + [Id] [BIGINT] NOT NULL IDENTITY, + [MessageId] UNIQUEIDENTIFIER NOT NULL, + [Topic] NVARCHAR(255) NULL, + [MessageType] NVARCHAR(32) NULL, + [Timestamp] DATETIME NULL, [CorrelationId] UNIQUEIDENTIFIER NULL, [ReplyTo] NVARCHAR(255) NULL, [ContentType] NVARCHAR(128) NULL, + [PartitionKey] NVARCHAR(255) NULL, [Dispatched] DATETIME NULL, - [HeaderBag] NTEXT NULL , - [Body] NTEXT NULL , + [HeaderBag] NVARCHAR(MAX) NULL, + [Body] VARBINARY(MAX) NULL, PRIMARY KEY ( [Id] ) ); "; + - private const string OutboxExistsSQL = @"SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = N'{0}'"; + private const string OutboxExistsSQL = @"IF EXISTS (SELECT 1 FROM sys.tables WHERE name = '{0}') SELECT 1 AS TableExists; ELSE SELECT 0 AS TableExists;"; /// /// Gets the DDL statements required to create an Outbox in MSSQL /// /// The name of the Outbox table + /// Should the message body be stored as binary? Conversion of binary data to/from UTF-8 is lossy /// The required DDL - public static string GetDDL(string outboxTableName) + public static string GetDDL(string outboxTableName, bool hasBinaryMessagePayload = false) { - return string.Format(OutboxDdl, outboxTableName); + if (string.IsNullOrEmpty(outboxTableName)) + throw new ArgumentNullException(outboxTableName, $"You must provide a tablename for the OutBox table"); + + return string.Format(hasBinaryMessagePayload ? BinaryOutboxDdl : TextOutboxDdl, outboxTableName); } /// diff --git a/src/Paramore.Brighter.Outbox.MySql/MySqlOutboxSync.cs b/src/Paramore.Brighter.Outbox.MySql/MySqlOutbox.cs similarity index 54% rename from src/Paramore.Brighter.Outbox.MySql/MySqlOutboxSync.cs rename to src/Paramore.Brighter.Outbox.MySql/MySqlOutbox.cs index 985b94bed0..d2c92d5807 100644 --- a/src/Paramore.Brighter.Outbox.MySql/MySqlOutboxSync.cs +++ b/src/Paramore.Brighter.Outbox.MySql/MySqlOutbox.cs @@ -26,6 +26,8 @@ THE SOFTWARE. */ using System; using System.Collections.Generic; using System.Data; +using System.Data.Common; +using System.IO; using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -37,45 +39,58 @@ THE SOFTWARE. */ namespace Paramore.Brighter.Outbox.MySql { /// - /// Class MySqlOutbox. + /// Implements an outbox using Sqlite as a backing store /// - public class MySqlOutboxSync : RelationDatabaseOutboxSync + public class MySqlOutbox : RelationDatabaseOutbox { - private static readonly ILogger s_logger = ApplicationLogging.CreateLogger(); + private static readonly ILogger s_logger = ApplicationLogging.CreateLogger(); private const int MySqlDuplicateKeyError = 1062; - private readonly MySqlConfiguration _configuration; - private readonly IMySqlConnectionProvider _connectionProvider; - - public MySqlOutboxSync(MySqlConfiguration configuration, IMySqlConnectionProvider connectionProvider) : base( - configuration.OutBoxTableName, new MySqlQueries(), ApplicationLogging.CreateLogger()) + private readonly IAmARelationalDatabaseConfiguration _configuration; + private readonly IAmARelationalDbConnectionProvider _connectionProvider; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration to connect to this data store + /// Provides a connection to the Db that allows us to enlist in an ambient transaction + public MySqlOutbox(IAmARelationalDatabaseConfiguration configuration, IAmARelationalDbConnectionProvider connectionProvider) + : base(configuration.OutBoxTableName, new MySqlQueries(), ApplicationLogging.CreateLogger()) { _configuration = configuration; _connectionProvider = connectionProvider; ContinueOnCapturedContext = false; } - public MySqlOutboxSync(MySqlConfiguration configuration) : this(configuration, new MySqlConnectionProvider(configuration)) + /// + /// Initializes a new instance of the class. + /// + /// The configuration to connect to this data store + public MySqlOutbox(IAmARelationalDatabaseConfiguration configuration) + : this(configuration, new MySqlConnectionProvider(configuration)) { } - protected override void WriteToStore(IAmABoxTransactionConnectionProvider transactionConnectionProvider, Func commandFunc, - Action loggingAction) + protected override void WriteToStore( + IAmABoxTransactionProvider transactionProvider, + Func commandFunc, + Action loggingAction + ) { var connectionProvider = _connectionProvider; - if (transactionConnectionProvider != null && transactionConnectionProvider is IMySqlTransactionConnectionProvider provider) - connectionProvider = provider; + if (transactionProvider is IAmARelationalDbConnectionProvider transConnectionProvider) + connectionProvider = transConnectionProvider; var connection = connectionProvider.GetConnection(); if (connection.State != ConnectionState.Open) - connection.Open(); + connection.Open(); using (var command = commandFunc.Invoke(connection)) { try { - if (transactionConnectionProvider != null && connectionProvider.HasOpenTransaction) - command.Transaction = connectionProvider.GetTransaction(); + if (transactionProvider != null && transactionProvider.HasOpenTransaction) + command.Transaction = transactionProvider.GetTransaction(); command.ExecuteNonQuery(); } catch (MySqlException sqlException) @@ -91,22 +106,24 @@ protected override void WriteToStore(IAmABoxTransactionConnectionProvider transa } finally { - if (!connectionProvider.IsSharedConnection) - connection.Dispose(); - else if (!connectionProvider.HasOpenTransaction) - connection.Close(); + transactionProvider?.Close(); } } } - protected override async Task WriteToStoreAsync(IAmABoxTransactionConnectionProvider transactionConnectionProvider, Func commandFunc, - Action loggingAction, CancellationToken cancellationToken) + protected override async Task WriteToStoreAsync( + IAmABoxTransactionProvider transactionProvider, + Func commandFunc, + Action loggingAction, + CancellationToken cancellationToken + ) { var connectionProvider = _connectionProvider; - if (transactionConnectionProvider != null && transactionConnectionProvider is IMySqlTransactionConnectionProvider provider) - connectionProvider = provider; + if (transactionProvider is IAmARelationalDbConnectionProvider transConnectionProvider) + connectionProvider = transConnectionProvider; - var connection = await connectionProvider.GetConnectionAsync(cancellationToken).ConfigureAwait(ContinueOnCapturedContext); + var connection = await connectionProvider.GetConnectionAsync(cancellationToken) + .ConfigureAwait(ContinueOnCapturedContext); if (connection.State != ConnectionState.Open) await connection.OpenAsync(cancellationToken); @@ -114,8 +131,8 @@ protected override async Task WriteToStoreAsync(IAmABoxTransactionConnectionProv { try { - if (transactionConnectionProvider != null && connectionProvider.HasOpenTransaction) - command.Transaction = connectionProvider.GetTransaction(); + if (transactionProvider != null && transactionProvider.HasOpenTransaction) + command.Transaction = await transactionProvider.GetTransactionAsync(cancellationToken); await command.ExecuteNonQueryAsync(cancellationToken); } catch (MySqlException sqlException) @@ -131,15 +148,15 @@ protected override async Task WriteToStoreAsync(IAmABoxTransactionConnectionProv } finally { - if (!connectionProvider.IsSharedConnection) - connection.Dispose(); - else if (!connectionProvider.HasOpenTransaction) - await connection.CloseAsync(); + transactionProvider?.Close(); } } } - protected override T ReadFromStore(Func commandFunc, Func resultFunc) + protected override T ReadFromStore( + Func commandFunc, + Func resultFunc + ) { var connection = _connectionProvider.GetConnection(); @@ -153,15 +170,15 @@ protected override T ReadFromStore(Func comman } finally { - if (!_connectionProvider.IsSharedConnection) - connection.Dispose(); - else if (!_connectionProvider.HasOpenTransaction) - connection.Close(); + connection.Close(); } } } - protected override async Task ReadFromStoreAsync(Func commandFunc, Func> resultFunc, CancellationToken cancellationToken) + protected override async Task ReadFromStoreAsync( + Func commandFunc, + Func> resultFunc, + CancellationToken cancellationToken) { var connection = await _connectionProvider.GetConnectionAsync(cancellationToken); @@ -175,16 +192,16 @@ protected override async Task ReadFromStoreAsync(Func MapFunctionAsync(MySqlDataReader dr, CancellationToken cancellationToken) + protected override async Task MapFunctionAsync(DbDataReader dr, CancellationToken cancellationToken) { if (await dr.ReadAsync(cancellationToken)) { @@ -250,25 +311,29 @@ protected override async Task MapFunctionAsync(MySqlDataReader dr, Canc return new Message(); } - protected override IEnumerable MapListFunction(MySqlDataReader dr) + protected override IEnumerable MapListFunction(DbDataReader dr) { var messages = new List(); while (dr.Read()) { messages.Add(MapAMessage(dr)); } + dr.Close(); return messages; } - protected override async Task> MapListFunctionAsync(MySqlDataReader dr, CancellationToken cancellationToken) + protected override async Task> MapListFunctionAsync( + DbDataReader dr, + CancellationToken cancellationToken) { var messages = new List(); while (await dr.ReadAsync(cancellationToken)) { messages.Add(MapAMessage(dr)); } + dr.Close(); return messages; @@ -293,6 +358,7 @@ private Message MapAMessage(IDataReader dr) var correlationId = GetCorrelationId(dr); var replyTo = GetReplyTo(dr); var contentType = GetContentType(dr); + var partitionKey = GetPartitionKey(dr); header = new MessageHeader( messageId: id, @@ -303,7 +369,8 @@ private Message MapAMessage(IDataReader dr) delayedMilliseconds: 0, correlationId: correlationId, replyTo: replyTo, - contentType: contentType); + contentType: contentType, + partitionKey: partitionKey); Dictionary dictionaryBag = GetContextBag(dr); if (dictionaryBag != null) @@ -315,14 +382,64 @@ private Message MapAMessage(IDataReader dr) } } - var body = new MessageBody(dr.GetString(dr.GetOrdinal("Body"))); + var body = _configuration.BinaryMessagePayload + ? new MessageBody(GetBodyAsBytes((MySqlDataReader)dr), "application/octet-stream", CharacterEncoding.Raw) + : new MessageBody(GetBodyAsString(dr), "application/json", CharacterEncoding.UTF8); return new Message(header, body); } - private static string GetTopic(IDataReader dr) + private byte[] GetBodyAsBytes(MySqlDataReader dr) { - return dr.GetString(dr.GetOrdinal("Topic")); + var i = dr.GetOrdinal("Body"); + using (var ms = new MemoryStream()) + { + var buffer = new byte[1024]; + int offset = 0; + var bytesRead = dr.GetBytes(i, offset, buffer, 0, 1024); + while (bytesRead > 0) + { + ms.Write(buffer, offset, (int)bytesRead); + offset += (int)bytesRead; + bytesRead = dr.GetBytes(i, offset, buffer, 0, 1024); + } + + ms.Flush(); + var body = ms.ToArray(); + return body; + } + } + + private static string GetBodyAsString(IDataReader dr) + { + return dr.GetString(dr.GetOrdinal("Body")); + } + + private static Dictionary GetContextBag(IDataReader dr) + { + var i = dr.GetOrdinal("HeaderBag"); + var headerBag = dr.IsDBNull(i) ? "" : dr.GetString(i); + var dictionaryBag = + JsonSerializer.Deserialize>(headerBag, JsonSerialisationOptions.Options); + return dictionaryBag; + } + + private string GetContentType(IDataReader dr) + { + var ordinal = dr.GetOrdinal("ContentType"); + if (dr.IsDBNull(ordinal)) return null; + + var contentType = dr.GetString(ordinal); + return contentType; + } + + private Guid? GetCorrelationId(IDataReader dr) + { + var ordinal = dr.GetOrdinal("CorrelationId"); + if (dr.IsDBNull(ordinal)) return null; + + var correlationId = dr.GetGuid(ordinal); + return correlationId; } private static MessageType GetMessageType(IDataReader dr) @@ -335,13 +452,13 @@ private static Guid GetMessageId(IDataReader dr) return dr.GetGuid(0); } - private string GetContentType(IDataReader dr) + private string GetPartitionKey(IDataReader dr) { - var ordinal = dr.GetOrdinal("ContentType"); + var ordinal = dr.GetOrdinal("PartitionKey"); if (dr.IsDBNull(ordinal)) return null; - var replyTo = dr.GetString(ordinal); - return replyTo; + var partitionKey = dr.GetString(ordinal); + return partitionKey; } private string GetReplyTo(IDataReader dr) @@ -353,21 +470,9 @@ private string GetReplyTo(IDataReader dr) return replyTo; } - private static Dictionary GetContextBag(IDataReader dr) - { - var i = dr.GetOrdinal("HeaderBag"); - var headerBag = dr.IsDBNull(i) ? "" : dr.GetString(i); - var dictionaryBag = JsonSerializer.Deserialize>(headerBag, JsonSerialisationOptions.Options); - return dictionaryBag; - } - - private Guid? GetCorrelationId(IDataReader dr) + private static string GetTopic(IDataReader dr) { - var ordinal = dr.GetOrdinal("CorrelationId"); - if (dr.IsDBNull(ordinal)) return null; - - var correlationId = dr.GetGuid(ordinal); - return correlationId; + return dr.GetString(dr.GetOrdinal("Topic")); } private static DateTime GetTimeStamp(IDataReader dr) diff --git a/src/Paramore.Brighter.Outbox.MySql/MySqlOutboxBuilder.cs b/src/Paramore.Brighter.Outbox.MySql/MySqlOutboxBuilder.cs index 0341cb209d..bad152c290 100644 --- a/src/Paramore.Brighter.Outbox.MySql/MySqlOutboxBuilder.cs +++ b/src/Paramore.Brighter.Outbox.MySql/MySqlOutboxBuilder.cs @@ -23,23 +23,25 @@ THE SOFTWARE. */ #endregion +using System; using System.ComponentModel; namespace Paramore.Brighter.Outbox.MySql { - /// - /// Provide SQL statement helpers for creation of an Outbox - /// - public class MySqlOutboxBuilder + /// + /// Provide SQL statement helpers for creation of an Outbox + /// + public class MySqlOutboxBuilder { - const string OutboxDdl = @"CREATE TABLE {0} ( + const string TextOutboxDdl = @"CREATE TABLE {0} ( `MessageId` CHAR(36) NOT NULL , `Topic` VARCHAR(255) NOT NULL , `MessageType` VARCHAR(32) NOT NULL , `Timestamp` TIMESTAMP(3) NOT NULL , `CorrelationId` CHAR(36) NULL , `ReplyTo` VARCHAR(255) NULL , - `ContentType` VARCHAR(128) NULL , + `ContentType` VARCHAR(128) NULL , + `PartitionKey` VARCHAR(128) NULL , `Dispatched` TIMESTAMP(3) NULL , `HeaderBag` TEXT NOT NULL , `Body` TEXT NOT NULL , @@ -49,18 +51,38 @@ public class MySqlOutboxBuilder PRIMARY KEY (`MessageId`) ) ENGINE = InnoDB;"; - const string outboxExistsQuery = @"SHOW TABLES LIKE '{0}'; "; + const string BinaryOutboxDdl = @"CREATE TABLE {0} ( + `MessageId` CHAR(36) NOT NULL , + `Topic` VARCHAR(255) NOT NULL , + `MessageType` VARCHAR(32) NOT NULL , + `Timestamp` TIMESTAMP(3) NOT NULL , + `CorrelationId` CHAR(36) NULL , + `ReplyTo` VARCHAR(255) NULL , + `ContentType` VARCHAR(128) NULL , + `PartitionKey` VARCHAR(128) NULL , + `Dispatched` TIMESTAMP(3) NULL , + `HeaderBag` TEXT NOT NULL , + `Body` BLOB NOT NULL , + `Created` TIMESTAMP(3) NOT NULL DEFAULT NOW(3), + `CreatedID` INT(11) NOT NULL AUTO_INCREMENT, + UNIQUE(`CreatedID`), + PRIMARY KEY (`MessageId`) +) ENGINE = InnoDB;"; + + const string outboxExistsQuery = @"SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = '{0}') AS TableExists;"; /// /// Get the DDL that describes the table we will store messages in /// /// The name of the table to store messages in + /// Should the message body be stored as binary? Conversion of binary data to/from UTF-8 is lossy /// - public static string GetDDL(string outboxTableName) + public static string GetDDL(string outboxTableName, bool hasBinaryMessagePayload = false) { if (string.IsNullOrEmpty(outboxTableName)) - throw new InvalidEnumArgumentException($"You must provide a tablename for the OutBox table"); - return string.Format(OutboxDdl, outboxTableName); + throw new ArgumentNullException(outboxTableName, $"You must provide a tablename for the OutBox table"); + + return string.Format(hasBinaryMessagePayload ? BinaryOutboxDdl : TextOutboxDdl, outboxTableName); } /// @@ -74,7 +96,6 @@ public static string GetExistsQuery(string inboxTableName) if (string.IsNullOrEmpty(inboxTableName)) throw new InvalidEnumArgumentException($"You must provide a tablename for the OutBox table"); return string.Format(outboxExistsQuery, inboxTableName); - } } } diff --git a/src/Paramore.Brighter.Outbox.MySql/MySqlQueries.cs b/src/Paramore.Brighter.Outbox.MySql/MySqlQueries.cs index 1a2b809c53..f6b7828daf 100644 --- a/src/Paramore.Brighter.Outbox.MySql/MySqlQueries.cs +++ b/src/Paramore.Brighter.Outbox.MySql/MySqlQueries.cs @@ -2,17 +2,16 @@ { public class MySqlQueries : IRelationDatabaseOutboxQueries { - public string PagedDispatchedCommand { get; } = "SELECT * FROM {0} AS TBL WHERE `CreatedID` BETWEEN ((@PageNumber-1)*@PageSize+1) AND (@PageNumber*@PageSize) AND DISPATCHED IS NOT NULL AND DISPATCHED < DATE_ADD(UTC_TIMESTAMP(), INTERVAL @OutstandingSince MICROSECOND) AND NUMBER BETWEEN ((@PageNumber-1)*@PageSize+1) AND (@PageNumber*@PageSize) ORDER BY Timestamp DESC"; - public string PagedReadCommand { get; } = "SELECT * FROM {0} AS TBL WHERE `CreatedID` BETWEEN ((@PageNumber-1)*@PageSize+1) AND (@PageNumber*@PageSize) ORDER BY Timestamp DESC"; - public string PagedOutstandingCommand { get; } = "SELECT * FROM {0} WHERE DISPATCHED IS NULL AND Timestamp < DATE_ADD(UTC_TIMESTAMP(), INTERVAL @OutStandingSince SECOND) ORDER BY Timestamp DESC LIMIT @PageSize OFFSET @OffsetValue"; - public string AddCommand { get; } = "INSERT INTO {0} (MessageId, MessageType, Topic, Timestamp, CorrelationId, ReplyTo, ContentType, HeaderBag, Body) VALUES (@MessageId, @MessageType, @Topic, @Timestamp, @CorrelationId, @ReplyTo, @ContentType, @HeaderBag, @Body)"; - public string BulkAddCommand { get; } = "INSERT INTO {0} (MessageId, MessageType, Topic, Timestamp, CorrelationId, ReplyTo, ContentType, HeaderBag, Body) VALUES {1}"; - public string MarkDispatchedCommand { get; } = "UPDATE {0} SET Dispatched = @DispatchedAt WHERE MessageId = @MessageId"; - public string MarkMultipleDispatchedCommand { get; } = "UPDATE {0} SET Dispatched = @DispatchedAt WHERE MessageId IN ( {1} )"; - public string GetMessageCommand { get; } = "SELECT * FROM {0} WHERE MessageId = @MessageId"; - public string GetMessagesCommand { get; } = "SELECT * FROM {0} WHERE `MessageID` IN ( {1} )"; + public string PagedDispatchedCommand { get; } = "SELECT * FROM {0} AS TBL WHERE `CreatedID` BETWEEN ((?PageNumber-1)*?PageSize+1) AND (?PageNumber*?PageSize) AND DISPATCHED IS NOT NULL AND DISPATCHED < DATE_ADD(UTC_TIMESTAMP(), INTERVAL ?OutstandingSince MICROSECOND) AND NUMBER BETWEEN ((?PageNumber-1)*?PageSize+1) AND (?PageNumber*?PageSize) ORDER BY Timestamp DESC"; + public string PagedReadCommand { get; } = "SELECT * FROM {0} AS TBL WHERE `CreatedID` BETWEEN ((?PageNumber-1)*?PageSize+1) AND (?PageNumber*?PageSize) ORDER BY Timestamp ASC"; + public string PagedOutstandingCommand { get; } = "SELECT * FROM {0} WHERE DISPATCHED IS NULL AND Timestamp < DATE_ADD(UTC_TIMESTAMP(), INTERVAL -?OutStandingSince SECOND) ORDER BY Timestamp DESC LIMIT ?PageSize OFFSET ?OffsetValue"; + public string AddCommand { get; } = "INSERT INTO {0} (MessageId, MessageType, Topic, Timestamp, CorrelationId, ReplyTo, ContentType, PartitionKey, HeaderBag, Body) VALUES (?MessageId, ?MessageType, ?Topic, ?Timestamp, ?CorrelationId, ?ReplyTo, ?ContentType, ?PartitionKey, ?HeaderBag, ?Body)"; + public string BulkAddCommand { get; } = "INSERT INTO {0} (MessageId, MessageType, Topic, Timestamp, CorrelationId, ReplyTo, ContentType, PartitionKey, HeaderBag, Body) VALUES {1}"; + public string MarkDispatchedCommand { get; } = "UPDATE {0} SET Dispatched = ?DispatchedAt WHERE MessageId = ?MessageId"; + public string MarkMultipleDispatchedCommand { get; } = "UPDATE {0} SET Dispatched = ?DispatchedAt WHERE MessageId IN ( {1} )"; + public string GetMessageCommand { get; } = "SELECT * FROM {0} WHERE MessageId = ?MessageId"; + public string GetMessagesCommand { get; } = "SELECT * FROM {0} WHERE `MessageID` IN ( {1} )ORDER BY Timestamp ASC"; public string DeleteMessagesCommand { get; } = "DELETE FROM {0} WHERE MessageId IN ( {1} )"; - public string DispatchedCommand { get; } = "Select * FROM {0} WHERE Dispatched is not NULL and Dispatched < DATEADD(hour, @DispatchedSince, getutcdate()) LIMIT @PageSize Order BY Dispatched"; } } diff --git a/src/Paramore.Brighter.Outbox.MySql/ServiceCollectionExtensions.cs b/src/Paramore.Brighter.Outbox.MySql/ServiceCollectionExtensions.cs deleted file mode 100644 index c162c07b46..0000000000 --- a/src/Paramore.Brighter.Outbox.MySql/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System; -using Microsoft.Extensions.DependencyInjection; -using Paramore.Brighter.Extensions.DependencyInjection; -using Paramore.Brighter.MySql; - -namespace Paramore.Brighter.Outbox.MySql -{ - public static class ServiceCollectionExtensions - { - /// - /// Use MySql for the Outbox - /// - /// Allows extension method syntax - /// The connection for the Db and name of the Outbox table - /// What is the type for the class that lets us obtain connections for the Sqlite database - /// What is the lifetime of the services that we add - /// Allows fluent syntax - /// Registers the following - /// -- MySqlOutboxConfiguration: connection string and outbox name - /// -- IMySqlConnectionProvider: lets us get a connection for the outbox that matches the entity store - /// -- IAmAnOutbox: an outbox to store messages we want to send - /// -- IAmAnOutboxAsync: an outbox to store messages we want to send - /// -- IAmAnOutboxViewer: Lets us read the entries in the outbox - /// -- IAmAnOutboxViewerAsync: Lets us read the entries in the outbox - public static IBrighterBuilder UseMySqlOutbox( - this IBrighterBuilder brighterBuilder, MySqlConfiguration configuration, Type connectionProvider, ServiceLifetime serviceLifetime = ServiceLifetime.Singleton) - { - brighterBuilder.Services.AddSingleton(configuration); - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IMySqlConnectionProvider), connectionProvider, serviceLifetime)); - - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmAnOutboxSync), BuildMySqlOutboxOutbox, serviceLifetime)); - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmAnOutboxAsync), BuildMySqlOutboxOutbox, serviceLifetime)); - - return brighterBuilder; - } - - /// - /// Use this transaction provider to ensure that the Outbox and the Entity Store are correct - /// - /// Allows extension method - /// What is the type of the connection provider - /// What is the lifetime of registered interfaces - /// Allows fluent syntax - /// This is paired with Use Outbox (above) when required - /// Registers the following - /// -- IAmABoxTransactionConnectionProvider: the provider of a connection for any existing transaction - public static IBrighterBuilder UseMySqTransactionConnectionProvider( - this IBrighterBuilder brighterBuilder, Type connectionProvider, - ServiceLifetime serviceLifetime = ServiceLifetime.Scoped) - { - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmABoxTransactionConnectionProvider), connectionProvider, serviceLifetime)); - - return brighterBuilder; - } - - private static MySqlOutboxSync BuildMySqlOutboxOutbox(IServiceProvider provider) - { - var config = provider.GetService(); - var connectionProvider = provider.GetService(); - - return new MySqlOutboxSync(config, connectionProvider); - } - } -} diff --git a/src/Paramore.Brighter.Outbox.PostgreSql/DDL_SCRIPTS/POSTGRESQL/Outbox.sql b/src/Paramore.Brighter.Outbox.PostgreSql/DDL_SCRIPTS/POSTGRESQL/Outbox.sql deleted file mode 100644 index 6528e208e5..0000000000 --- a/src/Paramore.Brighter.Outbox.PostgreSql/DDL_SCRIPTS/POSTGRESQL/Outbox.sql +++ /dev/null @@ -1,15 +0,0 @@ ---Table:Messages - -CREATE TABLE Messages -( - Id BIGSERIAL PRIMARY KEY, - MessageId UUID UNIQUE NOT NULL, - Topic VARCHAR(255) NULL, - MessageType VARCHAR(32) NULL, - Timestamp timestamptz NULL, - CorrelationId uuid NULL, - ReplyTo VARCHAR(255) NULL, - ContentType VARCHAR(128) NULL, - HeaderBag TEXT NULL, - Body TEXT NULL -); \ No newline at end of file diff --git a/src/Paramore.Brighter.Outbox.PostgreSql/Paramore.Brighter.Outbox.PostgreSql.csproj b/src/Paramore.Brighter.Outbox.PostgreSql/Paramore.Brighter.Outbox.PostgreSql.csproj index e13948889b..7f4cff31aa 100644 --- a/src/Paramore.Brighter.Outbox.PostgreSql/Paramore.Brighter.Outbox.PostgreSql.csproj +++ b/src/Paramore.Brighter.Outbox.PostgreSql/Paramore.Brighter.Outbox.PostgreSql.csproj @@ -11,13 +11,10 @@ - - all runtime; build; native; contentfiles; analyzers; buildtransitive - \ No newline at end of file diff --git a/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutbox.cs b/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutbox.cs new file mode 100644 index 0000000000..a71cd47b3d --- /dev/null +++ b/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutbox.cs @@ -0,0 +1,470 @@ +#region Licence + +/* The MIT License (MIT) +Copyright © 2014 Francesco Pighi + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the “Software”), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.Common; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Npgsql; +using NpgsqlTypes; +using Paramore.Brighter.Logging; +using Paramore.Brighter.PostgreSql; + +namespace Paramore.Brighter.Outbox.PostgreSql +{ + /// + /// Implements an outbox using PostgreSQL as a backing store + /// + public class PostgreSqlOutbox : RelationDatabaseOutbox + { + private static readonly ILogger s_logger = ApplicationLogging.CreateLogger(); + + private readonly IAmARelationalDatabaseConfiguration _configuration; + private readonly IAmARelationalDbConnectionProvider _connectionProvider; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration to connect to this data store + /// Provides a connection to the Db that allows us to enlist in an ambient transaction + + public PostgreSqlOutbox( + IAmARelationalDatabaseConfiguration configuration, + IAmARelationalDbConnectionProvider connectionProvider) : base( + configuration.OutBoxTableName, new PostgreSqlQueries(), ApplicationLogging.CreateLogger()) + { + _configuration = configuration; + _connectionProvider = connectionProvider; + } + + /// + /// Initializes a new instance of the class. + /// + /// The configuration to connect to this data store + /// From v7.0 Npgsql uses an Npgsql data source, leave null to have Brighter manage + /// connections; Brighter will not manage type mapping for you in this case so you must register them + /// globally + public PostgreSqlOutbox( + IAmARelationalDatabaseConfiguration configuration, + NpgsqlDataSource dataSource = null) + : this(configuration, new PostgreSqlConnectionProvider(configuration, dataSource)) + { } + + protected override void WriteToStore( + IAmABoxTransactionProvider transactionProvider, + Func commandFunc, + Action loggingAction) + { + var connectionProvider = _connectionProvider; + if (transactionProvider is IAmARelationalDbConnectionProvider transConnectionProvider) + connectionProvider = transConnectionProvider; + + var connection = connectionProvider.GetConnection(); + + if (connection.State != ConnectionState.Open) + connection.Open(); + using (var command = commandFunc.Invoke(connection)) + { + try + { + if (transactionProvider != null && transactionProvider.HasOpenTransaction) + command.Transaction = transactionProvider.GetTransaction(); + command.ExecuteNonQuery(); + } + catch (PostgresException sqlException) + { + if (sqlException.SqlState == PostgresErrorCodes.UniqueViolation) + { + loggingAction.Invoke(); + return; + } + + throw; + } + finally + { + transactionProvider?.Close(); + } + } + } + + protected override async Task WriteToStoreAsync( + IAmABoxTransactionProvider transactionProvider, + Func commandFunc, + Action loggingAction, + CancellationToken cancellationToken) + { + var connectionProvider = _connectionProvider; + if (transactionProvider is IAmARelationalDbConnectionProvider transConnectionProvider) + connectionProvider = transConnectionProvider; + + var connection = await connectionProvider.GetConnectionAsync(cancellationToken) + .ConfigureAwait(ContinueOnCapturedContext); + + if (connection.State != ConnectionState.Open) + await connection.OpenAsync(cancellationToken); + using (var command = commandFunc.Invoke(connection)) + { + try + { + if (transactionProvider != null && transactionProvider.HasOpenTransaction) + command.Transaction = transactionProvider.GetTransaction(); + await command.ExecuteNonQueryAsync(cancellationToken); + } + catch (PostgresException sqlException) + { + if (sqlException.SqlState == PostgresErrorCodes.UniqueViolation) + { + s_logger.LogWarning( + "PostgresSqlOutbox: A duplicate was detected in the batch"); + return; + } + + throw; + } + finally + { + if (transactionProvider != null) + transactionProvider.Close(); + else + connection.Close(); + } + } + } + + protected override T ReadFromStore( + Func commandFunc, + Func resultFunc + ) + { + var connection = _connectionProvider.GetConnection(); + + if (connection.State != ConnectionState.Open) + connection.Open(); + using (var command = commandFunc.Invoke(connection)) + { + try + { + return resultFunc.Invoke(command.ExecuteReader()); + } + finally + { + connection.Close(); + } + } + } + + protected override async Task ReadFromStoreAsync( + Func commandFunc, + Func> resultFunc, + CancellationToken cancellationToken + ) + { + var connection = await _connectionProvider.GetConnectionAsync(cancellationToken); + + if (connection.State != ConnectionState.Open) + await connection.OpenAsync(cancellationToken); + using (var command = commandFunc.Invoke(connection)) + { + try + { + return await resultFunc.Invoke(await command.ExecuteReaderAsync(cancellationToken)); + } + finally + { + connection.Close(); + } + } + } + + protected override DbCommand CreateCommand( + DbConnection connection, + string sqlText, + int outBoxTimeout, + params IDbDataParameter[] parameters) + { + var command = connection.CreateCommand(); + + command.CommandTimeout = outBoxTimeout < 0 ? 0 : outBoxTimeout; + command.CommandText = sqlText; + command.Parameters.AddRange(parameters); + + return command; + } + + protected override IDbDataParameter[] CreatePagedOutstandingParameters( + double milliSecondsSinceAdded, + int pageSize, + int pageNumber) + { + var parameters = new IDbDataParameter[3]; + parameters[0] = CreateSqlParameter("OutstandingSince", milliSecondsSinceAdded); + parameters[1] = CreateSqlParameter("PageSize", pageSize); + parameters[2] = CreateSqlParameter("PageNumber", pageNumber); + + return parameters; + } + + protected override IDbDataParameter CreateSqlParameter(string parameterName, object value) + { + return new NpgsqlParameter { ParameterName = parameterName, Value = value }; + } + + protected override IDbDataParameter[] InitAddDbParameters(Message message, int? position = null) + { + var prefix = position.HasValue ? $"p{position}_" : ""; + var bagjson = JsonSerializer.Serialize(message.Header.Bag, JsonSerialisationOptions.Options); + return new[] + { + new NpgsqlParameter + { + ParameterName = $"{prefix}MessageId", NpgsqlDbType = NpgsqlDbType.Uuid, Value = message.Id + }, + new NpgsqlParameter + { + ParameterName = $"{prefix}MessageType", + NpgsqlDbType = NpgsqlDbType.Text, + Value = message.Header.MessageType.ToString() + }, + new NpgsqlParameter + { + ParameterName = $"{prefix}Topic", + NpgsqlDbType = NpgsqlDbType.Text, + Value = message.Header.Topic + }, + new NpgsqlParameter + { + ParameterName = $"{prefix}Timestamp", + NpgsqlDbType = NpgsqlDbType.TimestampTz, + Value = message.Header.TimeStamp + }, + new NpgsqlParameter + { + ParameterName = $"{prefix}CorrelationId", + NpgsqlDbType = NpgsqlDbType.Uuid, + Value = message.Header.CorrelationId + }, + new NpgsqlParameter + { + ParameterName = $"{prefix}ReplyTo", + NpgsqlDbType = NpgsqlDbType.Varchar, + Value = message.Header.ReplyTo + }, + new NpgsqlParameter + { + ParameterName = $"{prefix}ContentType", + NpgsqlDbType = NpgsqlDbType.Varchar, + Value = message.Header.ContentType + }, + new NpgsqlParameter + { + ParameterName = $"{prefix}PartitionKey", + NpgsqlDbType = NpgsqlDbType.Varchar, + Value = message.Header.PartitionKey + }, + new NpgsqlParameter + { + ParameterName = $"{prefix}HeaderBag", NpgsqlDbType = NpgsqlDbType.Text, Value = bagjson + }, + _configuration.BinaryMessagePayload ? new NpgsqlParameter + { + ParameterName = $"{prefix}Body", + NpgsqlDbType = NpgsqlDbType.Bytea, + Value = message.Body.Bytes + } + : new NpgsqlParameter + { + ParameterName = $"{prefix}Body", + NpgsqlDbType = NpgsqlDbType.Text, + Value = message.Body.Value + } + }; + } + + protected override Message MapFunction(DbDataReader dr) + { + if (dr.Read()) + { + return MapAMessage(dr); + } + + return new Message(); + } + + protected override async Task MapFunctionAsync(DbDataReader dr, CancellationToken cancellationToken) + { + if (await dr.ReadAsync(cancellationToken)) + { + return MapAMessage(dr); + } + + return new Message(); + } + + protected override IEnumerable MapListFunction(DbDataReader dr) + { + var messages = new List(); + while (dr.Read()) + { + messages.Add(MapAMessage(dr)); + } + + dr.Close(); + + return messages; + } + + protected override async Task> MapListFunctionAsync( + DbDataReader dr, + CancellationToken cancellationToken + ) + { + var messages = new List(); + while (await dr.ReadAsync(cancellationToken)) + { + messages.Add(MapAMessage(dr)); + } + + dr.Close(); + + return messages; + } + public Message MapAMessage(DbDataReader dr) + { + var id = GetMessageId(dr); + var messageType = GetMessageType(dr); + var topic = GetTopic(dr); + + DateTime timeStamp = GetTimeStamp(dr); + var correlationId = GetCorrelationId(dr); + var replyTo = GetReplyTo(dr); + var contentType = GetContentType(dr); + var partitionKey = GetPartitionKey(dr); + + var header = new MessageHeader( + messageId: id, + topic: topic, + messageType: messageType, + timeStamp: timeStamp, + handledCount: 0, + delayedMilliseconds: 0, + correlationId: correlationId, + replyTo: replyTo, + contentType: contentType, + partitionKey: partitionKey); + + Dictionary dictionaryBag = GetContextBag(dr); + if (dictionaryBag != null) + { + foreach (var key in dictionaryBag.Keys) + { + header.Bag.Add(key, dictionaryBag[key]); + } + } + + var body = _configuration.BinaryMessagePayload + ? new MessageBody(((NpgsqlDataReader)dr).GetFieldValue(dr.GetOrdinal("Body"))) + :new MessageBody(dr.GetString(dr.GetOrdinal("Body"))); + + return new Message(header, body); + } + + private string GetContentType(DbDataReader dr) + { + var ordinal = dr.GetOrdinal("ContentType"); + if (dr.IsDBNull(ordinal)) + return null; + + var replyTo = dr.GetString(ordinal); + return replyTo; + } + + private static Dictionary GetContextBag(DbDataReader dr) + { + var i = dr.GetOrdinal("HeaderBag"); + var headerBag = dr.IsDBNull(i) ? "" : dr.GetString(i); + var dictionaryBag = + JsonSerializer.Deserialize>(headerBag, JsonSerialisationOptions.Options); + return dictionaryBag; + } + + private Guid? GetCorrelationId(DbDataReader dr) + { + var ordinal = dr.GetOrdinal("CorrelationId"); + if (dr.IsDBNull(ordinal)) + return null; + + var correlationId = dr.GetGuid(ordinal); + return correlationId; + } + + private static string GetTopic(DbDataReader dr) + { + return dr.GetString(dr.GetOrdinal("Topic")); + } + + private static MessageType GetMessageType(DbDataReader dr) + { + return (MessageType)Enum.Parse(typeof(MessageType), dr.GetString(dr.GetOrdinal("MessageType"))); + } + + private static Guid GetMessageId(DbDataReader dr) + { + return dr.GetGuid(dr.GetOrdinal("MessageId")); + } + + private string GetPartitionKey(DbDataReader dr) + { + var ordinal = dr.GetOrdinal("PartitionKey"); + if (dr.IsDBNull(ordinal)) return null; + + var partitionKey = dr.GetString(ordinal); + return partitionKey; + } + + private string GetReplyTo(DbDataReader dr) + { + var ordinal = dr.GetOrdinal("ReplyTo"); + if (dr.IsDBNull(ordinal)) + return null; + + var replyTo = dr.GetString(ordinal); + return replyTo; + } + + private static DateTime GetTimeStamp(DbDataReader dr) + { + var ordinal = dr.GetOrdinal("Timestamp"); + var timeStamp = dr.IsDBNull(ordinal) + ? DateTime.MinValue + : dr.GetDateTime(ordinal); + return timeStamp; + } + } +} diff --git a/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutboxBuilder.cs b/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutboxBulder.cs similarity index 59% rename from src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutboxBuilder.cs rename to src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutboxBulder.cs index 3a1d587a49..4dc047485f 100644 --- a/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutboxBuilder.cs +++ b/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutboxBulder.cs @@ -29,33 +29,53 @@ namespace Paramore.Brighter.Outbox.PostgreSql /// public class PostgreSqlOutboxBulder { - const string OutboxDdl = @" + const string TextOutboxDdl = @" CREATE TABLE {0} ( - Id BIGSERIAL PRIMARY KEY, - MessageId UUID UNIQUE NOT NULL, - Topic VARCHAR(255) NULL, - MessageType VARCHAR(32) NULL, + Id bigserial PRIMARY KEY, + MessageId uuid UNIQUE NOT NULL, + Topic character varying(255) NULL, + MessageType character varying(32) NULL, Timestamp timestamptz NULL, CorrelationId uuid NULL, - ReplyTo VARCHAR(255) NULL, - ContentType VARCHAR(128) NULL, + ReplyTo character varying(255) NULL, + ContentType character varying(128) NULL, + PartitionKey character varying(128) NULL, Dispatched timestamptz NULL, - HeaderBag TEXT NULL, - Body TEXT NULL + HeaderBag text NULL, + Body text NULL ); "; - private const string OutboxExistsSQL = @"SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = N'{0}'"; + const string BinaryOutboxDdl = @" + CREATE TABLE {0} + ( + Id bigserial PRIMARY KEY, + MessageId uuid UNIQUE NOT NULL, + Topic character varying(255) NULL, + MessageType character varying(32) NULL, + Timestamp timestamptz NULL, + CorrelationId uuid NULL, + ReplyTo character varying(255) NULL, + ContentType character varying(128) NULL, + PartitionKey character varying(128) NULL, + Dispatched timestamptz NULL, + HeaderBag text NULL, + Body bytea NULL + ); + "; + private const string OutboxExistsSQL = @"SELECT EXISTS(SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = '{0}')"; + /// /// Get the DDL required to create the Outbox in Postgres /// /// The name you want to use for the table + /// /// The required DDL - public static string GetDDL(string outboxTableName) + public static string GetDDL(string outboxTableName, bool binaryMessagePayload = false) { - return string.Format(OutboxDdl, outboxTableName); + return binaryMessagePayload ? string.Format(BinaryOutboxDdl, outboxTableName) : string.Format(TextOutboxDdl, outboxTableName); } /// diff --git a/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutboxConfiguration.cs b/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutboxConfiguration.cs deleted file mode 100644 index 73a7a44c3c..0000000000 --- a/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutboxConfiguration.cs +++ /dev/null @@ -1,56 +0,0 @@ -#region Licence -/* The MIT License (MIT) -Copyright © 2014 Francesco Pighi - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the “Software”), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. */ - -#endregion - -using Paramore.Brighter.PostgreSql; - -namespace Paramore.Brighter.Outbox.PostgreSql -{ - public class PostgreSqlOutboxConfiguration : PostgreSqlConfiguration - { - /// - /// Initialises a new instance of - /// - /// The Subscription String - /// Name of the OutBox table - public PostgreSqlOutboxConfiguration(string connectionstring, string outBoxTablename) : base(connectionstring) - { - OutboxTableName = outBoxTablename; - } - - /// - /// Initialises a new instance of - /// - /// Name of the OutBox table - public PostgreSqlOutboxConfiguration(string outBoxTablename) : base(null) - { - OutboxTableName = outBoxTablename; - } - - /// - /// Gets the name of the outbox table. - /// - /// The name of the outbox table. - public string OutboxTableName { get; } - } -} diff --git a/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutboxSync.cs b/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutboxSync.cs deleted file mode 100644 index c4d505ba1d..0000000000 --- a/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutboxSync.cs +++ /dev/null @@ -1,693 +0,0 @@ -#region Licence - -/* The MIT License (MIT) -Copyright © 2014 Francesco Pighi - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the “Software”), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. */ - -#endregion - -using System; -using System.Collections.Generic; -using System.Data; -using System.Linq; -using System.Text.Json; -using Microsoft.Extensions.Logging; -using Npgsql; -using NpgsqlTypes; -using Paramore.Brighter.Logging; -using Paramore.Brighter.PostgreSql; - -namespace Paramore.Brighter.Outbox.PostgreSql -{ - public class PostgreSqlOutboxSync : IAmABulkOutboxSync - { - private readonly PostgreSqlOutboxConfiguration _configuration; - private readonly IPostgreSqlConnectionProvider _connectionProvider; - private static readonly ILogger s_logger = ApplicationLogging.CreateLogger(); - - - private const string _deleteMessageCommand = "DELETE FROM {0} WHERE MessageId IN ({1})"; - private readonly string _outboxTableName; - - public bool ContinueOnCapturedContext - { - get => throw new NotImplementedException(); - set => throw new NotImplementedException(); - } - - /// - /// Initialises a new instance of class. - /// - /// PostgreSql Outbox Configuration. - public PostgreSqlOutboxSync(PostgreSqlOutboxConfiguration configuration, IPostgreSqlConnectionProvider connectionProvider = null) - { - _configuration = configuration; - _connectionProvider = connectionProvider ?? new PostgreSqlNpgsqlConnectionProvider(configuration); - _outboxTableName = configuration.OutboxTableName; - } - - /// - /// Adds the specified message. - /// - /// The message. - /// The time allowed for the write in milliseconds; on a -1 default - public void Add(Message message, int outBoxTimeout = -1, IAmABoxTransactionConnectionProvider transactionConnectionProvider = null) - { - var connectionProvider = GetConnectionProvider(transactionConnectionProvider); - var parameters = InitAddDbParameters(message); - var connection = GetOpenConnection(connectionProvider); - - try - { - using (var command = InitAddDbCommand(connection, parameters)) - { - if (connectionProvider.HasOpenTransaction) - command.Transaction = connectionProvider.GetTransaction(); - command.ExecuteNonQuery(); - } - } - catch (PostgresException sqlException) - { - if (sqlException.SqlState == PostgresErrorCodes.UniqueViolation) - { - s_logger.LogWarning( - "PostgresSQLOutbox: A duplicate Message with the MessageId {Id} was inserted into the Outbox, ignoring and continuing", - message.Id); - return; - } - - throw; - } - finally - { - if (!connectionProvider.IsSharedConnection) - connection.Dispose(); - else if (!connectionProvider.HasOpenTransaction) - connection.Close(); - } - } - - /// - /// Awaitable add the specified message. - /// - /// The message. - /// The time allowed for the write in milliseconds; on a -1 default - /// The Connection Provider to use for this call - public void Add(IEnumerable messages, int outBoxTimeout = -1, - IAmABoxTransactionConnectionProvider transactionConnectionProvider = null) - { - var connectionProvider = GetConnectionProvider(transactionConnectionProvider); - var connection = GetOpenConnection(connectionProvider); - - try - { - using (var command = InitBulkAddDbCommand(connection, messages.ToList())) - { - if (connectionProvider.HasOpenTransaction) - command.Transaction = connectionProvider.GetTransaction(); - command.ExecuteNonQuery(); - } - } - catch (PostgresException sqlException) - { - if (sqlException.SqlState == PostgresErrorCodes.UniqueViolation) - { - s_logger.LogWarning( - "PostgresSQLOutbox: A duplicate Message was found in the batch"); - return; - } - - throw; - } - finally - { - if (!connectionProvider.IsSharedConnection) - connection.Dispose(); - else if (!connectionProvider.HasOpenTransaction) - connection.Close(); - } - } - - /// - /// Returns messages that have been successfully dispatched - /// - /// How long ago was the message dispatched? - /// How many messages returned at once? - /// Which page of the dispatched messages to return? - /// - /// Additional parameters required for search, if any - /// A list of dispatched messages - public IEnumerable DispatchedMessages( - double millisecondsDispatchedSince, - int pageSize = 100, - int pageNumber = 1, - int outboxTimeout = -1, - Dictionary args = null) - { - var connectionProvider = GetConnectionProvider(); - var connection = GetOpenConnection(connectionProvider); - - try - { - using (var command = InitPagedDispatchedCommand(connection, millisecondsDispatchedSince, pageSize, pageNumber)) - { - var messages = new List(); - - using (var dbDataReader = command.ExecuteReader()) - { - while (dbDataReader.Read()) - messages.Add(MapAMessage(dbDataReader)); - } - - return messages; - } - } - finally - { - if (!connectionProvider.IsSharedConnection) - connection.Dispose(); - else if (!connectionProvider.HasOpenTransaction) - connection.Close(); - } - } - - /// - /// Returns all messages in the store - /// - /// Number of messages to return in search results (default = 100) - /// Page number of results to return (default = 1) - /// Additional parameters required for search, if any - /// A list of messages - public IList Get(int pageSize = 100, int pageNumber = 1, Dictionary args = null) - { - var connectionProvider = GetConnectionProvider(); - var connection = GetOpenConnection(connectionProvider); - - try - { - using (var command = InitPagedReadCommand(connection, pageSize, pageNumber)) - { - var messages = new List(); - - using (var dbDataReader = command.ExecuteReader()) - { - while (dbDataReader.Read()) - { - messages.Add(MapAMessage(dbDataReader)); - } - } - - return messages; - } - } - finally - { - if (!connectionProvider.IsSharedConnection) - connection.Dispose(); - else if (!connectionProvider.HasOpenTransaction) - connection.Close(); - } - } - - /// - /// Gets the specified message identifier. - /// - /// The message identifier. - /// The time allowed for the read in milliseconds; on a -2 default - /// The message - public Message Get(Guid messageId, int outBoxTimeout = -1) - { - var sql = string.Format( - "SELECT Id, MessageId, Topic, MessageType, Timestamp, Correlationid, ReplyTo, ContentType, HeaderBag, Body FROM {0} WHERE MessageId = @MessageId", - _configuration.OutboxTableName); - var parameters = new[] { InitNpgsqlParameter("MessageId", messageId) }; - - return ExecuteCommand(command => MapFunction(command.ExecuteReader()), sql, outBoxTimeout, parameters); - } - - /// - /// Update a message to show it is dispatched - /// - /// The id of the message to update - /// When was the message dispatched, defaults to UTC now - public void MarkDispatched(Guid id, DateTime? dispatchedAt = null, Dictionary args = null) - { - var connectionProvider = GetConnectionProvider(); - var connection = GetOpenConnection(connectionProvider); - - try - { - using (var command = InitMarkDispatchedCommand(connection, id, dispatchedAt)) - { - command.ExecuteNonQuery(); - } - } - finally - { - if (!connectionProvider.IsSharedConnection) - connection.Dispose(); - else if (!connectionProvider.HasOpenTransaction) - connection.Close(); - } - } - - /// - /// Returns messages that have yet to be dispatched - /// - /// How long ago as the message sent? - /// How many messages to return at once? - /// Which page number of messages - /// Additional parameters required for search, if any - /// A list of messages that are outstanding for dispatch - public IEnumerable OutstandingMessages( - double millSecondsSinceSent, - int pageSize = 100, - int pageNumber = 1, - Dictionary args = null) - { - var connectionProvider = GetConnectionProvider(); - var connection = GetOpenConnection(connectionProvider); - - try - { - using (var command = InitPagedOutstandingCommand(connection, millSecondsSinceSent, pageSize, pageNumber)) - { - var messages = new List(); - - using (var dbDataReader = command.ExecuteReader()) - { - while (dbDataReader.Read()) - { - messages.Add(MapAMessage(dbDataReader)); - } - } - - return messages; - } - } - finally - { - if (!connectionProvider.IsSharedConnection) - connection.Dispose(); - else if (!connectionProvider.HasOpenTransaction) - connection.Close(); - } - } - - public void Delete(params Guid[] messageIds) - { - WriteToStore(null, connection => InitDeleteDispatchedCommand(connection, messageIds), null); - } - - private NpgsqlCommand InitDeleteDispatchedCommand(NpgsqlConnection connection, IEnumerable messageIds) - { - var inClause = GenerateInClauseAndAddParameters(messageIds.ToList()); - foreach (var p in inClause.parameters) - { - p.DbType = DbType.Object; - } - return CreateCommand(connection, GenerateSqlText(_deleteMessageCommand, inClause.inClause), 0, - inClause.parameters); - } - - private (string inClause, NpgsqlParameter[] parameters) GenerateInClauseAndAddParameters(List messageIds) - { - var paramNames = messageIds.Select((s, i) => "@p" + i).ToArray(); - - var parameters = new NpgsqlParameter[messageIds.Count]; - for (int i = 0; i < paramNames.Count(); i++) - { - parameters[i] = CreateSqlParameter(paramNames[i], messageIds[i]); - } - - return (string.Join(",", paramNames), parameters); - } - - private NpgsqlParameter CreateSqlParameter(string parameterName, object value) - { - return new NpgsqlParameter(parameterName, value ?? DBNull.Value); - } - - private string GenerateSqlText(string sqlFormat, params string[] orderedParams) - => string.Format(sqlFormat, orderedParams.Prepend(_outboxTableName).ToArray()); - - private NpgsqlCommand CreateCommand(NpgsqlConnection connection, string sqlText, int outBoxTimeout, - params NpgsqlParameter[] parameters) - - { - var command = connection.CreateCommand(); - - command.CommandTimeout = outBoxTimeout < 0 ? 0 : outBoxTimeout; - command.CommandText = sqlText; - command.Parameters.AddRange(parameters); - - return command; - } - - private void WriteToStore(IAmABoxTransactionConnectionProvider transactionConnectionProvider, Func commandFunc, Action loggingAction) - { - var connectionProvider = _connectionProvider; - if (transactionConnectionProvider != null && transactionConnectionProvider is IPostgreSqlConnectionProvider provider) - connectionProvider = provider; - - var connection = connectionProvider.GetConnection(); - - if (connection.State != ConnectionState.Open) - connection.Open(); - using (var command = commandFunc.Invoke(connection)) - { - try - { - if (transactionConnectionProvider != null && connectionProvider.HasOpenTransaction) - command.Transaction = connectionProvider.GetTransaction(); - command.ExecuteNonQuery(); - } - catch (PostgresException sqlException) - { - if (sqlException.SqlState == PostgresErrorCodes.UniqueViolation) - { - loggingAction.Invoke(); - return; - } - - throw; - } - finally - { - if (!connectionProvider.IsSharedConnection) - connection.Dispose(); - else if (!connectionProvider.HasOpenTransaction) - connection.Close(); - } - } - } - - private IPostgreSqlConnectionProvider GetConnectionProvider(IAmABoxTransactionConnectionProvider transactionConnectionProvider = null) - { - var connectionProvider = _connectionProvider ?? new PostgreSqlNpgsqlConnectionProvider(_configuration); - - if (transactionConnectionProvider != null) - { - if (transactionConnectionProvider is IPostgreSqlTransactionConnectionProvider provider) - connectionProvider = provider; - else - throw new Exception($"{nameof(transactionConnectionProvider)} does not implement interface {nameof(IPostgreSqlTransactionConnectionProvider)}."); - } - - return connectionProvider; - } - - private NpgsqlConnection GetOpenConnection(IPostgreSqlConnectionProvider connectionProvider) - { - NpgsqlConnection connection = connectionProvider.GetConnection(); - - if (connection.State != ConnectionState.Open) - connection.Open(); - - return connection; - } - - private NpgsqlParameter InitNpgsqlParameter(string parametername, object value) - { - if (value != null) - return new NpgsqlParameter(parametername, value); - else - return new NpgsqlParameter(parametername, DBNull.Value); - } - - private NpgsqlCommand InitPagedDispatchedCommand(NpgsqlConnection connection, double millisecondsDispatchedSince, - int pageSize, int pageNumber) - { - var command = connection.CreateCommand(); - - var parameters = new[] - { - InitNpgsqlParameter("PageNumber", pageNumber), - InitNpgsqlParameter("PageSize", pageSize), - InitNpgsqlParameter("OutstandingSince", -1 * millisecondsDispatchedSince) - }; - - var pagingSqlFormat = - "SELECT * FROM (SELECT ROW_NUMBER() OVER(ORDER BY Timestamp DESC) AS NUMBER, * FROM {0}) AS TBL WHERE DISPATCHED IS NOT NULL AND DISPATCHED < (CURRENT_TIMESTAMP + (@OutstandingSince || ' millisecond')::INTERVAL) AND NUMBER BETWEEN ((@PageNumber-1)*@PageSize+1) AND (@PageNumber*@PageSize) ORDER BY Timestamp DESC"; - - command.CommandText = string.Format(pagingSqlFormat, _configuration.OutboxTableName); - command.Parameters.AddRange(parameters); - - return command; - } - - private NpgsqlCommand InitPagedReadCommand(NpgsqlConnection connection, int pageSize, int pageNumber) - { - var command = connection.CreateCommand(); - - var parameters = new[] - { - InitNpgsqlParameter("PageNumber", pageNumber), - InitNpgsqlParameter("PageSize", pageSize) - }; - - var pagingSqlFormat = - "SELECT * FROM (SELECT ROW_NUMBER() OVER(ORDER BY Timestamp DESC) AS NUMBER, * FROM {0}) AS TBL WHERE NUMBER BETWEEN ((@PageNumber-1)*@PageSize+1) AND (@PageNumber*@PageSize) ORDER BY Timestamp DESC"; - - command.CommandText = string.Format(pagingSqlFormat, _configuration.OutboxTableName); - command.Parameters.AddRange(parameters); - - return command; - } - - private NpgsqlCommand InitPagedOutstandingCommand(NpgsqlConnection connection, double milliSecondsSinceAdded, int pageSize, - int pageNumber) - { - var command = connection.CreateCommand(); - - var parameters = new[] - { - InitNpgsqlParameter("PageNumber", pageNumber), - InitNpgsqlParameter("PageSize", pageSize), - InitNpgsqlParameter("OutstandingSince", milliSecondsSinceAdded) - }; - - var pagingSqlFormat = - "SELECT * FROM (SELECT ROW_NUMBER() OVER(ORDER BY Timestamp ASC) AS NUMBER, * FROM {0} WHERE DISPATCHED IS NULL) AS TBL WHERE TIMESTAMP < (CURRENT_TIMESTAMP + (@OutstandingSince || ' millisecond')::INTERVAL) AND NUMBER BETWEEN ((@PageNumber-1)*@PageSize+1) AND (@PageNumber*@PageSize) ORDER BY Timestamp ASC"; - - command.CommandText = string.Format(pagingSqlFormat, _configuration.OutboxTableName); - command.Parameters.AddRange(parameters); - - return command; - } - - private NpgsqlParameter[] InitAddDbParameters(Message message, int? position = null) - { - var prefix = position.HasValue ? $"p{position}_" : ""; - var bagjson = JsonSerializer.Serialize(message.Header.Bag, JsonSerialisationOptions.Options); - return new NpgsqlParameter[] - { - InitNpgsqlParameter($"{prefix}MessageId", message.Id), - InitNpgsqlParameter($"{prefix}MessageType", message.Header.MessageType.ToString()), - InitNpgsqlParameter($"{prefix}Topic", message.Header.Topic), - new NpgsqlParameter($"{prefix}Timestamp", NpgsqlDbType.TimestampTz) {Value = message.Header.TimeStamp}, - InitNpgsqlParameter($"{prefix}CorrelationId", message.Header.CorrelationId), - InitNpgsqlParameter($"{prefix}ReplyTo", message.Header.ReplyTo), - InitNpgsqlParameter($"{prefix}ContentType", message.Header.ContentType), - InitNpgsqlParameter($"{prefix}HeaderBag", bagjson), - InitNpgsqlParameter($"{prefix}Body", message.Body.Value) - }; - } - - private NpgsqlCommand InitMarkDispatchedCommand(NpgsqlConnection connection, Guid messageId, - DateTime? dispatchedAt) - { - var command = connection.CreateCommand(); - command.CommandText = $"UPDATE {_configuration.OutboxTableName} SET Dispatched = @DispatchedAt WHERE MessageId = @MessageId"; - command.Parameters.Add(InitNpgsqlParameter("MessageId", messageId)); - command.Parameters.Add(InitNpgsqlParameter("DispatchedAt", dispatchedAt)); - return command; - } - - private T ExecuteCommand(Func execute, string sql, int messageStoreTimeout, - NpgsqlParameter[] parameters) - { - var connectionProvider = GetConnectionProvider(); - var connection = GetOpenConnection(connectionProvider); - - try - { - using (var command = connection.CreateCommand()) - { - command.CommandText = sql; - command.Parameters.AddRange(parameters); - - if (messageStoreTimeout != -1) - command.CommandTimeout = messageStoreTimeout; - - return execute(command); - } - } - finally - { - if (!connectionProvider.IsSharedConnection) - connection.Dispose(); - else if (!connectionProvider.HasOpenTransaction) - connection.Close(); - } - } - - private NpgsqlCommand InitAddDbCommand(NpgsqlConnection connection, NpgsqlParameter[] parameters) - { - var command = connection.CreateCommand(); - - var addSqlFormat = "INSERT INTO {0} (MessageId, MessageType, Topic, Timestamp, CorrelationId, ReplyTo, ContentType, HeaderBag, Body) VALUES (@MessageId, @MessageType, @Topic, @Timestamp::timestamptz, @CorrelationId, @ReplyTo, @ContentType, @HeaderBag, @Body)"; - - command.CommandText = string.Format(addSqlFormat, _configuration.OutboxTableName); - command.Parameters.AddRange(parameters); - - return command; - } - - private NpgsqlCommand InitBulkAddDbCommand(NpgsqlConnection connection, List messages) - { - var messageParams = new List(); - var parameters = new List(); - - for (int i = 0; i < messages.Count; i++) - { - messageParams.Add($"(@p{i}_MessageId, @p{i}_MessageType, @p{i}_Topic, @p{i}_Timestamp, @p{i}_CorrelationId, @p{i}_ReplyTo, @p{i}_ContentType, @p{i}_HeaderBag, @p{i}_Body)"); - parameters.AddRange(InitAddDbParameters(messages[i], i)); - - } - var sql = $"INSERT INTO {_configuration.OutboxTableName} (MessageId, MessageType, Topic, Timestamp, CorrelationId, ReplyTo, ContentType, HeaderBag, Body) VALUES {string.Join(",", messageParams)}"; - - var command = connection.CreateCommand(); - - command.CommandText = sql; - command.Parameters.AddRange(parameters.ToArray()); - - return command; - } - - private Message MapFunction(IDataReader reader) - { - if (reader.Read()) - { - return MapAMessage(reader); - } - - return new Message(); - } - - private Message MapAMessage(IDataReader dr) - { - var id = GetMessageId(dr); - var messageType = GetMessageType(dr); - var topic = GetTopic(dr); - - DateTime timeStamp = GetTimeStamp(dr); - var correlationId = GetCorrelationId(dr); - var replyTo = GetReplyTo(dr); - var contentType = GetContentType(dr); - - var header = new MessageHeader( - messageId: id, - topic: topic, - messageType: messageType, - timeStamp: timeStamp, - handledCount: 0, - delayedMilliseconds: 0, - correlationId: correlationId, - replyTo: replyTo, - contentType: contentType); - - Dictionary dictionaryBag = GetContextBag(dr); - if (dictionaryBag != null) - { - foreach (var key in dictionaryBag.Keys) - { - header.Bag.Add(key, dictionaryBag[key]); - } - } - - var body = new MessageBody(dr.GetString(dr.GetOrdinal("Body"))); - - return new Message(header, body); - } - - private static string GetTopic(IDataReader dr) - { - return dr.GetString(dr.GetOrdinal("Topic")); - } - - private static MessageType GetMessageType(IDataReader dr) - { - return (MessageType)Enum.Parse(typeof(MessageType), dr.GetString(dr.GetOrdinal("MessageType"))); - } - - private static Guid GetMessageId(IDataReader dr) - { - return dr.GetGuid(dr.GetOrdinal("MessageId")); - } - - private string GetContentType(IDataReader dr) - { - var ordinal = dr.GetOrdinal("ContentType"); - if (dr.IsDBNull(ordinal)) - return null; - - var replyTo = dr.GetString(ordinal); - return replyTo; - } - - private string GetReplyTo(IDataReader dr) - { - var ordinal = dr.GetOrdinal("ReplyTo"); - if (dr.IsDBNull(ordinal)) - return null; - - var replyTo = dr.GetString(ordinal); - return replyTo; - } - - private static Dictionary GetContextBag(IDataReader dr) - { - var i = dr.GetOrdinal("HeaderBag"); - var headerBag = dr.IsDBNull(i) ? "" : dr.GetString(i); - var dictionaryBag = JsonSerializer.Deserialize>(headerBag, JsonSerialisationOptions.Options); - return dictionaryBag; - } - - private Guid? GetCorrelationId(IDataReader dr) - { - var ordinal = dr.GetOrdinal("CorrelationId"); - if (dr.IsDBNull(ordinal)) - return null; - - var correlationId = dr.GetGuid(ordinal); - return correlationId; - } - - private static DateTime GetTimeStamp(IDataReader dr) - { - var ordinal = dr.GetOrdinal("Timestamp"); - var timeStamp = dr.IsDBNull(ordinal) - ? DateTime.MinValue - : dr.GetDateTime(ordinal); - return timeStamp; - } - } -} diff --git a/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlQueries.cs b/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlQueries.cs new file mode 100644 index 0000000000..0c24f88671 --- /dev/null +++ b/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlQueries.cs @@ -0,0 +1,17 @@ +namespace Paramore.Brighter.Outbox.PostgreSql +{ + public class PostgreSqlQueries : IRelationDatabaseOutboxQueries + { + public string PagedDispatchedCommand { get; } = "SELECT * FROM (SELECT ROW_NUMBER() OVER(ORDER BY Timestamp DESC) AS NUMBER, * FROM {0}) AS TBL WHERE DISPATCHED IS NOT NULL AND DISPATCHED < (CURRENT_TIMESTAMP + (@OutstandingSince || ' millisecond')::INTERVAL) AND NUMBER BETWEEN ((@PageNumber-1)*@PageSize+1) AND (@PageNumber*@PageSize) ORDER BY Timestamp DESC"; + public string PagedReadCommand { get; } = "SELECT * FROM (SELECT ROW_NUMBER() OVER(ORDER BY Timestamp ASC) AS NUMBER, * FROM {0}) AS TBL WHERE NUMBER BETWEEN ((@PageNumber-1)*@PageSize+1) AND (@PageNumber*@PageSize) ORDER BY Timestamp ASC"; + public string PagedOutstandingCommand { get; } = "SELECT * FROM (SELECT ROW_NUMBER() OVER(ORDER BY Timestamp ASC) AS NUMBER, * FROM {0} WHERE DISPATCHED IS NULL) AS TBL WHERE TIMESTAMP < (CURRENT_TIMESTAMP + (@OutstandingSince || ' millisecond')::INTERVAL) AND NUMBER BETWEEN ((@PageNumber-1)*@PageSize+1) AND (@PageNumber*@PageSize) ORDER BY Timestamp ASC"; + public string AddCommand { get; } = "INSERT INTO {0} (MessageId, MessageType, Topic, Timestamp, CorrelationId, ReplyTo, ContentType, PartitionKey, HeaderBag, Body) VALUES (@MessageId, @MessageType, @Topic, @Timestamp::timestamptz, @CorrelationId, @ReplyTo, @ContentType, @PartitionKey, @HeaderBag, @Body)"; + public string BulkAddCommand { get; } = "INSERT INTO {0} (MessageId, MessageType, Topic, Timestamp, CorrelationId, ReplyTo, ContentType, PartitionKey, HeaderBag, Body) VALUES {1}"; + public string MarkDispatchedCommand { get; } = "UPDATE {0} SET Dispatched = @DispatchedAt WHERE MessageId = @MessageId"; + public string MarkMultipleDispatchedCommand { get; } = "UPDATE {0} SET Dispatched = @DispatchedAt WHERE MessageId IN ( {1} )"; + public string GetMessageCommand { get; } = "SELECT * FROM {0} WHERE MessageId = @MessageId"; + public string GetMessagesCommand { get; } = "SELECT * FROM {0} WHERE MessageID IN ( {1} )ORDER BY Timestamp ASC"; + public string DeleteMessagesCommand { get; }= "DELETE FROM {0} WHERE MessageId IN ( {1} )"; + public string DispatchedCommand { get; }="SELECT * FROM {0} WHERE Dispatched IS NOT NULL AND Dispatched < (CURRENT_TIMESTAMP - INTERVAL '@DispatchedSince' HOUR) ORDER BY Dispatched LIMIT @PageSize;"; + } +} diff --git a/src/Paramore.Brighter.Outbox.PostgreSql/ServiceCollectionExtensions.cs b/src/Paramore.Brighter.Outbox.PostgreSql/ServiceCollectionExtensions.cs deleted file mode 100644 index 16637e5e10..0000000000 --- a/src/Paramore.Brighter.Outbox.PostgreSql/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System; -using Microsoft.Extensions.DependencyInjection; -using Paramore.Brighter.Extensions.DependencyInjection; -using Paramore.Brighter.PostgreSql; - -namespace Paramore.Brighter.Outbox.PostgreSql -{ - public static class ServiceCollectionExtensions - { - public static IBrighterBuilder UsePostgreSqlOutbox( - this IBrighterBuilder brighterBuilder, PostgreSqlOutboxConfiguration configuration, Type connectionProvider = null, ServiceLifetime serviceLifetime = ServiceLifetime.Singleton) - { - if (brighterBuilder is null) - throw new ArgumentNullException($"{nameof(brighterBuilder)} cannot be null.", nameof(brighterBuilder)); - - if (configuration is null) - throw new ArgumentNullException($"{nameof(configuration)} cannot be null.", nameof(configuration)); - - brighterBuilder.Services.AddSingleton(configuration); - - if (connectionProvider is object) - { - if (!typeof(IPostgreSqlConnectionProvider).IsAssignableFrom(connectionProvider)) - throw new Exception($"Unable to register provider of type {connectionProvider.GetType().Name}. Class does not implement interface {nameof(IPostgreSqlConnectionProvider)}."); - - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IPostgreSqlConnectionProvider), connectionProvider, serviceLifetime)); - } - - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmAnOutboxSync), BuildPostgreSqlOutboxSync, serviceLifetime)); - - return brighterBuilder; - } - - /// - /// Use this transaction provider to ensure that the Outbox and the Entity Store are correct - /// - /// Allows extension method - /// What is the type of the connection provider. Must implement interface IPostgreSqlTransactionConnectionProvider - /// What is the lifetime of registered interfaces - /// Allows fluent syntax - /// This is paired with Use Outbox (above) when required - /// Registers the following - /// -- IAmABoxTransactionConnectionProvider: the provider of a connection for any existing transaction - public static IBrighterBuilder UsePostgreSqlTransactionConnectionProvider( - this IBrighterBuilder brighterBuilder, Type connectionProvider, - ServiceLifetime serviceLifetime = ServiceLifetime.Scoped) - { - if (brighterBuilder is null) - throw new ArgumentNullException($"{nameof(brighterBuilder)} cannot be null.", nameof(brighterBuilder)); - - if (connectionProvider is null) - throw new ArgumentNullException($"{nameof(connectionProvider)} cannot be null.", nameof(connectionProvider)); - - if (!typeof(IPostgreSqlTransactionConnectionProvider).IsAssignableFrom(connectionProvider)) - throw new Exception($"Unable to register provider of type {connectionProvider.GetType().Name}. Class does not implement interface {nameof(IPostgreSqlTransactionConnectionProvider)}."); - - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmABoxTransactionConnectionProvider), connectionProvider, serviceLifetime)); - - return brighterBuilder; - } - - private static PostgreSqlOutboxSync BuildPostgreSqlOutboxSync(IServiceProvider provider) - { - var config = provider.GetService(); - var connectionProvider = provider.GetService(); - - return new PostgreSqlOutboxSync(config, connectionProvider); - } - } -} diff --git a/src/Paramore.Brighter.Outbox.Sqlite/DDL_SCRIPTS/SQLite/Outbox.sql b/src/Paramore.Brighter.Outbox.Sqlite/DDL_SCRIPTS/SQLite/Outbox.sql deleted file mode 100644 index e3c8076d1d..0000000000 --- a/src/Paramore.Brighter.Outbox.Sqlite/DDL_SCRIPTS/SQLite/Outbox.sql +++ /dev/null @@ -1,27 +0,0 @@ --- Script Date: 03/07/2015 12:03 - ErikEJ.SqlCeScripting version 3.5.2.49 --- Database information: --- Locale Identifier: 1033 --- Encryption Mode: --- Case Sensitive: False --- ServerVersion: 4.0.8876.1 --- DatabaseSize: 84 KB --- Created: 30/06/2015 23:53 - --- User Table information: --- Number of tables: 1 --- Messages: 0 row(s) - -SELECT 1; -PRAGMA foreign_keys=OFF; -BEGIN TRANSACTION; -CREATE TABLE [Messages] ( - [MessageId] uniqueidentifier NOT NULL -, [Topic] nvarchar(255) NULL -, [MessageType] nvarchar(32) NULL -, [Timestamp] datetime NULL -, [HeaderBag] ntext NULL -, [Body] ntext NULL -, CONSTRAINT [PK_MessageId] PRIMARY KEY ([MessageId]) -); -COMMIT; - diff --git a/src/Paramore.Brighter.Outbox.Sqlite/README.md b/src/Paramore.Brighter.Outbox.Sqlite/README.md deleted file mode 100644 index 8ee71c1a95..0000000000 --- a/src/Paramore.Brighter.Outbox.Sqlite/README.md +++ /dev/null @@ -1,47 +0,0 @@ -# Brighter MSSQL outbox - -## Setup - -To setup Brighter with a SQL Server or SQL CE outbox, some steps are required: - -#### Create a table with the schema in the example - -You can use the following example as a reference for SQL Server: - -```sql - CREATE TABLE MyOutbox ( - Id uniqueidentifier CONSTRAINT PK_MessageId PRIMARY KEY, - Topic nvarchar(255), - MessageType nvarchar(32), - Body nvarchar(max) - ) -``` -If you're using SQL CE you have to replace `nvarchar(max)` with a supported type, for example `ntext`. - -#### Configure the command processor - -The following is an example of how to configure a command processor with a SQL Server outbox. - -```csharp -var msSqlOutbox = new MsSqlOutbox(new MsSqlOutboxConfiguration( - "myconnectionstring", - "MyOutboxTable", - MsSqlOutboxConfiguration.DatabaseType.MsSqlServer - ), myLogger), - -var commandProcessor = CommandProcessorBuilder.With() - .Handlers(new HandlerConfiguration(mySubscriberRegistry, myHandlerFactory)) - .Policies(myPolicyRegistry) - .Logger(myLogger) - .TaskQueues(new MessagingConfiguration( - outbox: msSqlOutbox, - messagingGateway: myGateway, - messageMapperRegistry: myMessageMapperRegistry - )) - .RequestContextFactory(new InMemoryRequestContextFactory()) - .Build(); -``` - -> The values for the `MsSqlOutboxConfiguration.DatabaseType` enum are the following: -> `MsSqlServer` -> `SqlCe` diff --git a/src/Paramore.Brighter.Outbox.Sqlite/README.txt b/src/Paramore.Brighter.Outbox.Sqlite/README.txt deleted file mode 100644 index 8ee71c1a95..0000000000 --- a/src/Paramore.Brighter.Outbox.Sqlite/README.txt +++ /dev/null @@ -1,47 +0,0 @@ -# Brighter MSSQL outbox - -## Setup - -To setup Brighter with a SQL Server or SQL CE outbox, some steps are required: - -#### Create a table with the schema in the example - -You can use the following example as a reference for SQL Server: - -```sql - CREATE TABLE MyOutbox ( - Id uniqueidentifier CONSTRAINT PK_MessageId PRIMARY KEY, - Topic nvarchar(255), - MessageType nvarchar(32), - Body nvarchar(max) - ) -``` -If you're using SQL CE you have to replace `nvarchar(max)` with a supported type, for example `ntext`. - -#### Configure the command processor - -The following is an example of how to configure a command processor with a SQL Server outbox. - -```csharp -var msSqlOutbox = new MsSqlOutbox(new MsSqlOutboxConfiguration( - "myconnectionstring", - "MyOutboxTable", - MsSqlOutboxConfiguration.DatabaseType.MsSqlServer - ), myLogger), - -var commandProcessor = CommandProcessorBuilder.With() - .Handlers(new HandlerConfiguration(mySubscriberRegistry, myHandlerFactory)) - .Policies(myPolicyRegistry) - .Logger(myLogger) - .TaskQueues(new MessagingConfiguration( - outbox: msSqlOutbox, - messagingGateway: myGateway, - messageMapperRegistry: myMessageMapperRegistry - )) - .RequestContextFactory(new InMemoryRequestContextFactory()) - .Build(); -``` - -> The values for the `MsSqlOutboxConfiguration.DatabaseType` enum are the following: -> `MsSqlServer` -> `SqlCe` diff --git a/src/Paramore.Brighter.Outbox.Sqlite/ServiceCollectionExtensions.cs b/src/Paramore.Brighter.Outbox.Sqlite/ServiceCollectionExtensions.cs deleted file mode 100644 index a2649dbbb6..0000000000 --- a/src/Paramore.Brighter.Outbox.Sqlite/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System; -using Microsoft.Extensions.DependencyInjection; -using Paramore.Brighter.Extensions.DependencyInjection; -using Paramore.Brighter.Sqlite; - -namespace Paramore.Brighter.Outbox.Sqlite -{ - public static class ServiceCollectionExtensions - { - /// - /// Use Sqlite for the Outbox - /// - /// Allows extension method syntax - /// The connection for the Db and name of the Outbox table - /// What is the type for the class that lets us obtain connections for the Sqlite database - /// What is the lifetime of the services that we add - /// Allows fluent syntax - /// Registers the following - /// -- SqliteOutboxConfigutation: connection string and outbox name - /// -- ISqliteConnectionProvider: lets us get a connection for the outbox that matches the entity store - /// -- IAmAnOutbox: an outbox to store messages we want to send - /// -- IAmAnOutboxAsync: an outbox to store messages we want to send - /// -- IAmAnOutboxViewer: Lets us read the entries in the outbox - /// -- IAmAnOutboxViewerAsync: Lets us read the entries in the outbox - public static IBrighterBuilder UseSqliteOutbox( - this IBrighterBuilder brighterBuilder, SqliteConfiguration configuration, Type connectionProvider, ServiceLifetime serviceLifetime = ServiceLifetime.Singleton) - { - brighterBuilder.Services.AddSingleton(configuration); - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(ISqliteConnectionProvider), connectionProvider, serviceLifetime)); - - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmAnOutboxSync), BuildSqliteOutbox, serviceLifetime)); - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmAnOutboxAsync), BuildSqliteOutbox, serviceLifetime)); - - return brighterBuilder; - } - - /// - /// Use this transaction provider to ensure that the Outbox and the Entity Store are correct - /// - /// Allows extension method - /// What is the type of the connection provider - /// What is the lifetime of registered interfaces - /// Allows fluent syntax - /// This is paired with Use Outbox (above) when required - /// Registers the following - /// -- IAmABoxTransactionConnectionProvider: the provider of a connection for any existing transaction - public static IBrighterBuilder UseSqliteTransactionConnectionProvider( - this IBrighterBuilder brighterBuilder, Type connectionProvider, - ServiceLifetime serviceLifetime = ServiceLifetime.Scoped) - { - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmABoxTransactionConnectionProvider), connectionProvider, serviceLifetime)); - - return brighterBuilder; - } - - private static SqliteOutboxSync BuildSqliteOutbox(IServiceProvider provider) - { - var config = provider.GetService(); - var connectionProvider = provider.GetService(); - - return new SqliteOutboxSync(config, connectionProvider); - } - } -} diff --git a/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutboxSync.cs b/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutbox.cs similarity index 59% rename from src/Paramore.Brighter.Outbox.Sqlite/SqliteOutboxSync.cs rename to src/Paramore.Brighter.Outbox.Sqlite/SqliteOutbox.cs index 833bb01052..1fe9c232d6 100644 --- a/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutboxSync.cs +++ b/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutbox.cs @@ -26,6 +26,7 @@ THE SOFTWARE. */ using System; using System.Collections.Generic; using System.Data; +using System.Data.Common; using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -39,22 +40,22 @@ namespace Paramore.Brighter.Outbox.Sqlite /// /// Implements an outbox using Sqlite as a backing store /// - public class SqliteOutboxSync : RelationDatabaseOutboxSync + public class SqliteOutbox : RelationDatabaseOutbox { - private static readonly ILogger s_logger = ApplicationLogging.CreateLogger(); + private static readonly ILogger s_logger = ApplicationLogging.CreateLogger(); private const int SqliteDuplicateKeyError = 1555; private const int SqliteUniqueKeyError = 19; - private readonly SqliteConfiguration _configuration; - private readonly ISqliteConnectionProvider _connectionProvider; + private readonly IAmARelationalDatabaseConfiguration _configuration; + private readonly IAmARelationalDbConnectionProvider _connectionProvider; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The configuration to connect to this data store /// Provides a connection to the Db that allows us to enlist in an ambient transaction - public SqliteOutboxSync(SqliteConfiguration configuration, ISqliteConnectionProvider connectionProvider) : base( - configuration.OutBoxTableName, new SqliteQueries(), ApplicationLogging.CreateLogger()) + public SqliteOutbox(IAmARelationalDatabaseConfiguration configuration, IAmARelationalDbConnectionProvider connectionProvider) + : base(configuration.OutBoxTableName, new SqliteQueries(), ApplicationLogging.CreateLogger()) { _configuration = configuration; ContinueOnCapturedContext = false; @@ -62,19 +63,23 @@ public SqliteOutboxSync(SqliteConfiguration configuration, ISqliteConnectionProv } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The configuration to connect to this data store - public SqliteOutboxSync(SqliteConfiguration configuration) : this(configuration, new SqliteConnectionProvider(configuration)) + public SqliteOutbox(IAmARelationalDatabaseConfiguration configuration) + : this(configuration, new SqliteConnectionProvider(configuration)) { } - protected override void WriteToStore(IAmABoxTransactionConnectionProvider transactionConnectionProvider, Func commandFunc, - Action loggingAction) + protected override void WriteToStore( + IAmABoxTransactionProvider transactionProvider, + Func commandFunc, + Action loggingAction + ) { var connectionProvider = _connectionProvider; - if (transactionConnectionProvider != null && transactionConnectionProvider is ISqliteTransactionConnectionProvider provider) - connectionProvider = provider; + if (transactionProvider is IAmARelationalDbConnectionProvider transConnectionProvider) + connectionProvider = transConnectionProvider; var connection = connectionProvider.GetConnection(); @@ -84,8 +89,8 @@ protected override void WriteToStore(IAmABoxTransactionConnectionProvider transa { try { - if (transactionConnectionProvider != null && connectionProvider.HasOpenTransaction) - command.Transaction = connectionProvider.GetTransaction(); + if (transactionProvider != null && transactionProvider.HasOpenTransaction) + command.Transaction = transactionProvider.GetTransaction(); command.ExecuteNonQuery(); } catch (SqliteException sqlException) @@ -100,20 +105,23 @@ protected override void WriteToStore(IAmABoxTransactionConnectionProvider transa } finally { - if (!connectionProvider.IsSharedConnection) - connection.Dispose(); - else if (!connectionProvider.HasOpenTransaction) + if (transactionProvider != null) + transactionProvider.Close(); + else connection.Close(); } } } - protected override async Task WriteToStoreAsync(IAmABoxTransactionConnectionProvider transactionConnectionProvider, Func commandFunc, - Action loggingAction, CancellationToken cancellationToken) + protected override async Task WriteToStoreAsync( + IAmABoxTransactionProvider transactionProvider, + Func commandFunc, + Action loggingAction, + CancellationToken cancellationToken) { var connectionProvider = _connectionProvider; - if (transactionConnectionProvider != null && transactionConnectionProvider is ISqliteTransactionConnectionProvider provider) - connectionProvider = provider; + if (transactionProvider is IAmARelationalDbConnectionProvider transConnectionProvider) + connectionProvider = transConnectionProvider; var connection = await connectionProvider.GetConnectionAsync(cancellationToken); @@ -123,8 +131,8 @@ protected override async Task WriteToStoreAsync(IAmABoxTransactionConnectionProv { try { - if (transactionConnectionProvider != null && connectionProvider.HasOpenTransaction) - command.Transaction = connectionProvider.GetTransaction(); + if (transactionProvider != null && transactionProvider.HasOpenTransaction) + command.Transaction = await transactionProvider.GetTransactionAsync(cancellationToken); await command.ExecuteNonQueryAsync(cancellationToken); } catch (SqliteException sqlException) @@ -139,15 +147,18 @@ protected override async Task WriteToStoreAsync(IAmABoxTransactionConnectionProv } finally { - if (!connectionProvider.IsSharedConnection) - connection.Dispose(); - else if (!connectionProvider.HasOpenTransaction) - await connection.CloseAsync(); + if (transactionProvider != null) + transactionProvider.Close(); + else + connection.Close(); } } } - protected override T ReadFromStore(Func commandFunc, Func resultFunc) + protected override T ReadFromStore( + Func commandFunc, + Func resultFunc + ) { var connection = _connectionProvider.GetConnection(); @@ -161,15 +172,15 @@ protected override T ReadFromStore(Func comm } finally { - if (!_connectionProvider.IsSharedConnection) - connection.Dispose(); - else if (!_connectionProvider.HasOpenTransaction) - connection.Close(); + connection.Close(); } } } - protected override async Task ReadFromStoreAsync(Func commandFunc, Func> resultFunc, CancellationToken cancellationToken) + protected override async Task ReadFromStoreAsync( + Func commandFunc, + Func> resultFunc, + CancellationToken cancellationToken) { var connection = await _connectionProvider.GetConnectionAsync(cancellationToken); @@ -183,16 +194,16 @@ protected override async Task ReadFromStoreAsync(Func MapFunctionAsync(SqliteDataReader dr, CancellationToken cancellationToken) + protected override async Task MapFunctionAsync(DbDataReader dr, CancellationToken cancellationToken) { using (dr) { @@ -262,25 +326,27 @@ protected override async Task MapFunctionAsync(SqliteDataReader dr, Can } } - protected override IEnumerable MapListFunction(SqliteDataReader dr) + protected override IEnumerable MapListFunction(DbDataReader dr) { var messages = new List(); while (dr.Read()) { messages.Add(MapAMessage(dr)); } + dr.Close(); return messages; } - protected override async Task> MapListFunctionAsync(SqliteDataReader dr, CancellationToken cancellationToken) + protected override async Task> MapListFunctionAsync(DbDataReader dr, CancellationToken cancellationToken) { var messages = new List(); while (await dr.ReadAsync(cancellationToken)) { messages.Add(MapAMessage(dr)); } + dr.Close(); return messages; @@ -306,6 +372,7 @@ private Message MapAMessage(IDataReader dr) var correlationId = GetCorrelationId(dr); var replyTo = GetReplyTo(dr); var contentType = GetContentType(dr); + var partitionKey = GetPartitionKey(dr); header = new MessageHeader( messageId: id, @@ -316,7 +383,8 @@ private Message MapAMessage(IDataReader dr) delayedMilliseconds: 0, correlationId: correlationId, replyTo: replyTo, - contentType: contentType); + contentType: contentType, + partitionKey: partitionKey); Dictionary dictionaryBag = GetContextBag(dr); if (dictionaryBag != null) @@ -328,14 +396,51 @@ private Message MapAMessage(IDataReader dr) } } - var body = new MessageBody(dr.GetString(dr.GetOrdinal("Body"))); + var body = _configuration.BinaryMessagePayload + ? new MessageBody(GetBodyAsBytes((SqliteDataReader)dr), "application/octet-stream", + CharacterEncoding.Raw) + : new MessageBody(dr.GetString(dr.GetOrdinal("Body")), "application/json", CharacterEncoding.UTF8); + return new Message(header, body); } - private static string GetTopic(IDataReader dr) + + private byte[] GetBodyAsBytes(DbDataReader dr) { - return dr.GetString(dr.GetOrdinal("Topic")); + var i = dr.GetOrdinal("Body"); + var body = dr.GetStream(i); + var buffer = new byte[body.Length]; + body.Read(buffer); + return buffer; + } + + private static Dictionary GetContextBag(IDataReader dr) + { + var i = dr.GetOrdinal("HeaderBag"); + var headerBag = dr.IsDBNull(i) ? "" : dr.GetString(i); + var dictionaryBag = + JsonSerializer.Deserialize>(headerBag, JsonSerialisationOptions.Options); + return dictionaryBag; + } + + private string GetContentType(IDataReader dr) + { + var ordinal = dr.GetOrdinal("ContentType"); + if (dr.IsDBNull(ordinal)) return null; + + var contentType = dr.GetString(ordinal); + return contentType; + } + + + private Guid? GetCorrelationId(IDataReader dr) + { + var ordinal = dr.GetOrdinal("CorrelationId"); + if (dr.IsDBNull(ordinal)) return null; + + var correlationId = dr.GetGuid(ordinal); + return correlationId; } private static MessageType GetMessageType(IDataReader dr) @@ -348,15 +453,16 @@ private static Guid GetMessageId(IDataReader dr) return Guid.Parse(dr.GetString(dr.GetOrdinal("MessageId"))); } - private string GetContentType(IDataReader dr) + private string GetPartitionKey(IDataReader dr) { - var ordinal = dr.GetOrdinal("ContentType"); + var ordinal = dr.GetOrdinal("PartitionKey"); if (dr.IsDBNull(ordinal)) return null; - var replyTo = dr.GetString(ordinal); - return replyTo; + var partitionKey = dr.GetString(ordinal); + return partitionKey; } + private string GetReplyTo(IDataReader dr) { var ordinal = dr.GetOrdinal("ReplyTo"); @@ -366,22 +472,11 @@ private string GetReplyTo(IDataReader dr) return replyTo; } - private static Dictionary GetContextBag(IDataReader dr) + private static string GetTopic(IDataReader dr) { - var i = dr.GetOrdinal("HeaderBag"); - var headerBag = dr.IsDBNull(i) ? "" : dr.GetString(i); - var dictionaryBag = JsonSerializer.Deserialize>(headerBag, JsonSerialisationOptions.Options); - return dictionaryBag; + return dr.GetString(dr.GetOrdinal("Topic")); } - private Guid? GetCorrelationId(IDataReader dr) - { - var ordinal = dr.GetOrdinal("CorrelationId"); - if (dr.IsDBNull(ordinal)) return null; - - var correlationId = dr.GetGuid(ordinal); - return correlationId; - } private static DateTime GetTimeStamp(IDataReader dr) { @@ -391,6 +486,5 @@ private static DateTime GetTimeStamp(IDataReader dr) : dr.GetDateTime(ordinal); return timeStamp; } - } } diff --git a/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutboxBuilder.cs b/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutboxBuilder.cs index 14ed93aee3..0d7a168ef8 100644 --- a/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutboxBuilder.cs +++ b/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutboxBuilder.cs @@ -30,7 +30,7 @@ namespace Paramore.Brighter.Outbox.Sqlite /// public class SqliteOutboxBuilder { - const string OutboxDdl = @"CREATE TABLE {0} + const string TextOutboxDdl = @"CREATE TABLE {0} ( [MessageId] TEXT NOT NULL COLLATE NOCASE, [Topic] TEXT NULL, @@ -39,22 +39,41 @@ public class SqliteOutboxBuilder [CorrelationId] TEXT NULL, [ReplyTo] TEXT NULL, [ContentType] TEXT NULL, + [PartitionKey] TEXT NULL, [Dispatched] TEXT NULL, [HeaderBag] TEXT NULL, [Body] TEXT NULL, CONSTRAINT[PK_MessageId] PRIMARY KEY([MessageId]) );"; + + const string BinaryOutboxDdl = @"CREATE TABLE {0} + ( + [MessageId] TEXT NOT NULL COLLATE NOCASE, + [Topic] TEXT NULL, + [MessageType] TEXT NULL, + [Timestamp] TEXT NULL, + [CorrelationId] TEXT NULL, + [ReplyTo] TEXT NULL, + [ContentType] TEXT NULL, + [PartitionKey] TEXT NULL, + [Dispatched] TEXT NULL, + [HeaderBag] TEXT NULL, + [Body] BLOB NULL, + CONSTRAINT[PK_MessageId] PRIMARY KEY([MessageId]) + );"; + - private const string InboxExistsQuery = "SELECT name FROM sqlite_master WHERE type='table' AND name='{0}';"; + private const string OutboxExistsQuery = "SELECT name FROM sqlite_master WHERE type='table' AND name='{0}';"; - /// - /// Get the DDL statements to create an Outbox in Sqlite - /// - /// The name you want to use for the table - /// The required DDL - public static string GetDDL(string outboxTableName) + /// + /// Get the DDL statements to create an Outbox in Sqlite + /// + /// + /// Is the payload for the message binary or UTF-8. Defaults to false, or UTF-8 + /// The required DDL + public static string GetDDL(string outboxTableName, bool hasBinaryMessagePayload = false) { - return string.Format(OutboxDdl, outboxTableName); + return hasBinaryMessagePayload ? string.Format(BinaryOutboxDdl, outboxTableName) : string.Format(TextOutboxDdl, outboxTableName); } /// @@ -64,7 +83,7 @@ public static string GetDDL(string outboxTableName) /// The required SQL public static string GetExistsQuery(string outboxTableName) { - return string.Format(InboxExistsQuery, outboxTableName); + return string.Format(OutboxExistsQuery, outboxTableName); } } } diff --git a/src/Paramore.Brighter.Outbox.Sqlite/SqliteQueries.cs b/src/Paramore.Brighter.Outbox.Sqlite/SqliteQueries.cs index 66b87a23c7..885e25de52 100644 --- a/src/Paramore.Brighter.Outbox.Sqlite/SqliteQueries.cs +++ b/src/Paramore.Brighter.Outbox.Sqlite/SqliteQueries.cs @@ -3,16 +3,15 @@ public class SqliteQueries : IRelationDatabaseOutboxQueries { public string PagedDispatchedCommand { get; } = "SELECT * FROM {0} WHERE DISPATCHED IS NOT NULL AND (strftime('%s', 'now') - strftime('%s', Dispatched)) * 1000 < @OutstandingSince ORDER BY Timestamp ASC LIMIT @PageSize OFFSET (@PageNumber-1) * @PageSize"; - public string PagedReadCommand { get; } = "SELECT * FROM {0} ORDER BY Timestamp DESC LIMIT @PageSize OFFSET (@PageNumber-1) * @PageSize"; + public string PagedReadCommand { get; } = "SELECT * FROM {0} ORDER BY Timestamp ASC LIMIT @PageSize OFFSET (@PageNumber-1) * @PageSize"; public string PagedOutstandingCommand { get; } = "SELECT * FROM {0} WHERE DISPATCHED IS NULL AND (strftime('%s', 'now') - strftime('%s', TimeStamp)) * 1000 > @OutstandingSince ORDER BY Timestamp ASC LIMIT @PageSize OFFSET (@PageNumber-1) * @PageSize"; - public string AddCommand { get; } = "INSERT INTO {0} (MessageId, MessageType, Topic, Timestamp, CorrelationId, ReplyTo, ContentType, HeaderBag, Body) VALUES (@MessageId, @MessageType, @Topic, @Timestamp, @CorrelationId, @ReplyTo, @ContentType, @HeaderBag, @Body)"; - public string BulkAddCommand { get; } = "INSERT INTO {0} (MessageId, MessageType, Topic, Timestamp, CorrelationId, ReplyTo, ContentType, HeaderBag, Body) VALUES {1}"; + public string AddCommand { get; } = "INSERT INTO {0} (MessageId, MessageType, Topic, Timestamp, CorrelationId, ReplyTo, ContentType, PartitionKey, HeaderBag, Body) VALUES (@MessageId, @MessageType, @Topic, @Timestamp, @CorrelationId, @ReplyTo, @ContentType, @PartitionKey, @HeaderBag, @Body)"; + public string BulkAddCommand { get; } = "INSERT INTO {0} (MessageId, MessageType, Topic, Timestamp, CorrelationId, ReplyTo, ContentType, PartitionKey, HeaderBag, Body) VALUES {1}"; public string MarkDispatchedCommand { get; } = "UPDATE {0} SET Dispatched = @DispatchedAt WHERE MessageId = @MessageId"; public string MarkMultipleDispatchedCommand { get; } = "UPDATE {0} SET Dispatched = @DispatchedAt WHERE MessageId in ( {1} )"; public string GetMessageCommand { get; } = "SELECT * FROM {0} WHERE MessageId = @MessageId"; public string GetMessagesCommand { get; } = "SELECT * FROM {0} WHERE MessageId IN ( {1} )"; public string DeleteMessagesCommand { get; } = "DELETE FROM {0} WHERE MessageId IN ( {1} )"; - public string DispatchedCommand { get; } = "Select top(@PageSize) * FROM {0} WHERE Dispatched is not NULL and Dispatched < DATEADD(hour, @DispatchedSince, getutcdate()) Order BY Dispatched"; } } diff --git a/src/Paramore.Brighter.PostgreSql.EntityFrameworkCore/Paramore.Brighter.PostgreSql.EntityFrameworkCore.csproj b/src/Paramore.Brighter.PostgreSql.EntityFrameworkCore/Paramore.Brighter.PostgreSql.EntityFrameworkCore.csproj index 6bf84b7043..442f52b20a 100644 --- a/src/Paramore.Brighter.PostgreSql.EntityFrameworkCore/Paramore.Brighter.PostgreSql.EntityFrameworkCore.csproj +++ b/src/Paramore.Brighter.PostgreSql.EntityFrameworkCore/Paramore.Brighter.PostgreSql.EntityFrameworkCore.csproj @@ -8,9 +8,7 @@ - - all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -36,5 +34,4 @@ - diff --git a/src/Paramore.Brighter.PostgreSql.EntityFrameworkCore/PostgreSqlEntityFrameworkConnectionProvider.cs b/src/Paramore.Brighter.PostgreSql.EntityFrameworkCore/PostgreSqlEntityFrameworkConnectionProvider.cs index 9f14c6d1e8..5e11982224 100644 --- a/src/Paramore.Brighter.PostgreSql.EntityFrameworkCore/PostgreSqlEntityFrameworkConnectionProvider.cs +++ b/src/Paramore.Brighter.PostgreSql.EntityFrameworkCore/PostgreSqlEntityFrameworkConnectionProvider.cs @@ -1,4 +1,6 @@ -using System.Threading; +using System.Data; +using System.Data.Common; +using System.Threading; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Storage; @@ -10,7 +12,7 @@ namespace Paramore.Brighter.PostgreSql.EntityFrameworkCore /// A connection provider that uses the same connection as EF Core /// /// The Db Context to take the connection from - public class PostgreSqlEntityFrameworkConnectionProvider : IPostgreSqlTransactionConnectionProvider where T : DbContext + public class PostgreSqlEntityFrameworkConnectionProvider : RelationalDbTransactionProvider where T : DbContext { private readonly T _context; @@ -22,14 +24,39 @@ public PostgreSqlEntityFrameworkConnectionProvider(T context) { _context = context; } + + /// + /// Commit the transaction + /// + public override void Commit() + { + if (HasOpenTransaction) + { + _context.Database.CurrentTransaction?.Commit(); + } + } + + /// + /// Commit the transaction + /// + /// An awaitable Task + public override Task CommitAsync(CancellationToken cancellationToken) + { + if (HasOpenTransaction) + { + _context.Database.CurrentTransaction?.CommitAsync(cancellationToken); + } + + return Task.CompletedTask; + } /// /// Get the current connection of the database context /// /// The NpgsqlConnection that is in use - public NpgsqlConnection GetConnection() + public override DbConnection GetConnection() { - return (NpgsqlConnection)_context.Database.GetDbConnection(); + return _context.Database.GetDbConnection(); } /// @@ -37,10 +64,10 @@ public NpgsqlConnection GetConnection() /// /// A cancellation token /// - public Task GetConnectionAsync(CancellationToken cancellationToken = default) + public override Task GetConnectionAsync(CancellationToken cancellationToken = default) { - var tcs = new TaskCompletionSource(); - tcs.SetResult((NpgsqlConnection)_context.Database.GetDbConnection()); + var tcs = new TaskCompletionSource(); + tcs.SetResult(_context.Database.GetDbConnection()); return tcs.Task; } @@ -48,13 +75,13 @@ public Task GetConnectionAsync(CancellationToken cancellationT /// Get the ambient Transaction /// /// The NpgsqlTransaction - public NpgsqlTransaction GetTransaction() + public override DbTransaction GetTransaction() { - return (NpgsqlTransaction)_context.Database.CurrentTransaction?.GetDbTransaction(); + return _context.Database.CurrentTransaction?.GetDbTransaction(); } - public bool HasOpenTransaction { get => _context.Database.CurrentTransaction != null; } + public override bool HasOpenTransaction { get => _context.Database.CurrentTransaction != null; } - public bool IsSharedConnection { get => true; } + public override bool IsSharedConnection { get => true; } } } diff --git a/src/Paramore.Brighter.PostgreSql/IPostgreSqlConnectionProvider.cs b/src/Paramore.Brighter.PostgreSql/IPostgreSqlConnectionProvider.cs deleted file mode 100644 index c0df3cdf5f..0000000000 --- a/src/Paramore.Brighter.PostgreSql/IPostgreSqlConnectionProvider.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; -using Npgsql; - -namespace Paramore.Brighter.PostgreSql -{ - public interface IPostgreSqlConnectionProvider - { - NpgsqlConnection GetConnection(); - - Task GetConnectionAsync(CancellationToken cancellationToken = default); - - NpgsqlTransaction GetTransaction(); - - bool HasOpenTransaction { get; } - - bool IsSharedConnection { get; } - } -} diff --git a/src/Paramore.Brighter.PostgreSql/IPostgreSqlTransactionConnectionProvider.cs b/src/Paramore.Brighter.PostgreSql/IPostgreSqlTransactionConnectionProvider.cs deleted file mode 100644 index 82df857aca..0000000000 --- a/src/Paramore.Brighter.PostgreSql/IPostgreSqlTransactionConnectionProvider.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace Paramore.Brighter.PostgreSql -{ - public interface IPostgreSqlTransactionConnectionProvider : IPostgreSqlConnectionProvider, IAmABoxTransactionConnectionProvider { } -} diff --git a/src/Paramore.Brighter.PostgreSql/Paramore.Brighter.PostgreSql.csproj b/src/Paramore.Brighter.PostgreSql/Paramore.Brighter.PostgreSql.csproj index 9ea0a3fddf..40e4538177 100644 --- a/src/Paramore.Brighter.PostgreSql/Paramore.Brighter.PostgreSql.csproj +++ b/src/Paramore.Brighter.PostgreSql/Paramore.Brighter.PostgreSql.csproj @@ -8,17 +8,15 @@ - - all runtime; build; native; contentfiles; analyzers; buildtransitive + - diff --git a/src/Paramore.Brighter.PostgreSql/PostgreSqlConfiguration.cs b/src/Paramore.Brighter.PostgreSql/PostgreSqlConfiguration.cs deleted file mode 100644 index 649f613e04..0000000000 --- a/src/Paramore.Brighter.PostgreSql/PostgreSqlConfiguration.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Paramore.Brighter.PostgreSql -{ - public abstract class PostgreSqlConfiguration - { - protected PostgreSqlConfiguration(string connectionString) - { - ConnectionString = connectionString; - } - - public string ConnectionString { get; } - } -} diff --git a/src/Paramore.Brighter.PostgreSql/PostgreSqlConnectionProvider.cs b/src/Paramore.Brighter.PostgreSql/PostgreSqlConnectionProvider.cs new file mode 100644 index 0000000000..4425565947 --- /dev/null +++ b/src/Paramore.Brighter.PostgreSql/PostgreSqlConnectionProvider.cs @@ -0,0 +1,88 @@ +using System; +using System.Data.Common; +using System.Threading; +using System.Threading.Tasks; +using Npgsql; + +namespace Paramore.Brighter.PostgreSql +{ + /// + /// A connection provider that uses the connection string to create a connection + /// + public class PostgreSqlConnectionProvider : RelationalDbConnectionProvider + { + private NpgsqlDataSource _dataSource; + private readonly string _connectionString; + + /// + /// Initialise a new instance of PostgreSQl Connection provider from a connection string + /// + /// PostgreSQL Configuration + /// From v7.0 Npgsql uses an Npgsql data source, leave null to have Brighter manage + /// connections; Brighter will not manage type mapping for you in this case so you must register them + /// globally + public PostgreSqlConnectionProvider( + IAmARelationalDatabaseConfiguration configuration, + NpgsqlDataSource dataSource = null) + { + if (string.IsNullOrWhiteSpace(configuration?.ConnectionString)) + throw new ArgumentNullException(nameof(configuration.ConnectionString)); + + _connectionString = configuration.ConnectionString; + _dataSource = dataSource; + } + + /// + /// Close any open Npgsql data source + /// + public override void Close() + { + if (HasDataSource()) _dataSource.Dispose(); + _dataSource = null; + base.Close(); + } + + /// + /// Gets a existing Connection; creates a new one if it does not exist + /// The connection is not opened, you need to open it yourself. + /// + /// A database connection + public override DbConnection GetConnection() + { + if (HasDataSource()) + { + return _dataSource.OpenConnection(); + } + + var connection = new NpgsqlConnection(_connectionString); + if (connection.State != System.Data.ConnectionState.Open) connection.Open(); + return connection; + } + + + /// + /// Gets a existing Connection; creates a new one if it does not exist + /// The connection is not opened, you need to open it yourself. + /// + /// A database connection + public override async Task GetConnectionAsync(CancellationToken cancellationToken = default) + { + var connection = HasDataSource() ? _dataSource.CreateConnection() : new NpgsqlConnection(_connectionString); + if (connection.State != System.Data.ConnectionState.Open) await connection.OpenAsync(cancellationToken); + return connection; + } + + protected override void Dispose(bool disposing) + { + if (HasDataSource()) _dataSource.Dispose(); + _dataSource = null; + base.Dispose(disposing); + } + + private bool HasDataSource() + { + return _dataSource != null; + } + + } +} diff --git a/src/Paramore.Brighter.PostgreSql/PostgreSqlNpgsqlConnectionProvider.cs b/src/Paramore.Brighter.PostgreSql/PostgreSqlNpgsqlConnectionProvider.cs deleted file mode 100644 index 0eb773f027..0000000000 --- a/src/Paramore.Brighter.PostgreSql/PostgreSqlNpgsqlConnectionProvider.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using Npgsql; - -namespace Paramore.Brighter.PostgreSql -{ - public class PostgreSqlNpgsqlConnectionProvider : IPostgreSqlConnectionProvider - { - private readonly string _connectionString; - - public PostgreSqlNpgsqlConnectionProvider(PostgreSqlConfiguration configuration) - { - if (string.IsNullOrWhiteSpace(configuration?.ConnectionString)) - throw new ArgumentNullException(nameof(configuration.ConnectionString)); - - _connectionString = configuration.ConnectionString; - } - - public NpgsqlConnection GetConnection() - { - return new NpgsqlConnection(_connectionString); - } - - public async Task GetConnectionAsync(CancellationToken cancellationToken = default) - { - var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - tcs.SetResult(GetConnection()); - return await tcs.Task; - } - - public NpgsqlTransaction GetTransaction() - { - //This connection factory does not support transactions - return null; - } - - public bool HasOpenTransaction { get => false; } - - public bool IsSharedConnection { get => false; } - } -} diff --git a/src/Paramore.Brighter.PostgreSql/PostgreSqlUnitOfWork.cs b/src/Paramore.Brighter.PostgreSql/PostgreSqlUnitOfWork.cs new file mode 100644 index 0000000000..b0114dd6e9 --- /dev/null +++ b/src/Paramore.Brighter.PostgreSql/PostgreSqlUnitOfWork.cs @@ -0,0 +1,127 @@ +using System; +using System.Data; +using System.Data.Common; +using System.Threading; +using System.Threading.Tasks; +using Npgsql; + +namespace Paramore.Brighter.PostgreSql +{ + /// + /// A connection provider that uses the connection string to create a connection + /// + public class PostgreSqlUnitOfWork : RelationalDbTransactionProvider + { + private NpgsqlDataSource _dataSource; + private readonly string _connectionString; + + /// + /// Initialise a new instance of PostgreSQl Connection provider from a connection string + /// + /// PostgreSQL Configuration + /// From v7.0 Npgsql uses an Npgsql data source, leave null to have Brighter manage + /// connections; Brighter will not manage type mapping for you in this case so you must register them + /// globally + public PostgreSqlUnitOfWork( + IAmARelationalDatabaseConfiguration configuration, + NpgsqlDataSource dataSource = null) + { + if (string.IsNullOrWhiteSpace(configuration?.ConnectionString)) + throw new ArgumentNullException(nameof(configuration.ConnectionString)); + + _connectionString = configuration.ConnectionString; + _dataSource = dataSource; + } + + /// + /// Close any open data source - call base class to close any open connection or transaction + /// + public override void Close() + { + base.Close(); + if (HasDataSource()) _dataSource.Dispose(); + _dataSource = null; + } + + /// + /// Commit the transaction + /// + /// An awaitable Task + public override Task CommitAsync(CancellationToken cancellationToken) + { + if (HasOpenTransaction) + { + ((NpgsqlTransaction)Transaction).CommitAsync(cancellationToken); + Transaction = null; + } + + return Task.CompletedTask; + } + + /// + /// Gets a existing Connection; creates a new one if it does not exist + /// This is a shared connection and you should manage it via this interface + /// + /// A database connection + public override DbConnection GetConnection() + { + if (Connection == null && HasDataSource()) Connection = _dataSource.CreateConnection(); + + if (Connection == null) Connection = new NpgsqlConnection(_connectionString); + if (Connection.State != ConnectionState.Open) Connection.Open(); + return Connection; + } + + /// + /// Gets a existing Connection; creates a new one if it does not exist + /// This is a shared connection and you should manage it via this interface + /// + /// A database connection + public override async Task GetConnectionAsync(CancellationToken cancellationToken = default) + { + if (Connection == null && HasDataSource()) Connection = _dataSource.CreateConnection(); + + if (Connection == null) Connection = new NpgsqlConnection(_connectionString); + if (Connection.State != ConnectionState.Open) await Connection.OpenAsync(cancellationToken); + return Connection; + } + + /// + /// Either returns an existing open transaction or creates and opens a MySql Transaction + /// This is a shared transaction and you should manage it through the unit of work + /// + /// + public override DbTransaction GetTransaction() + { + if (Connection == null) Connection = GetConnection(); + if (!HasOpenTransaction) + Transaction = ((NpgsqlConnection)Connection).BeginTransaction(); + return Transaction; + } + + /// + /// Creates and opens a MySql Transaction + /// This is a shared transaction and you should manage it through the unit of work + /// + /// + public override async Task GetTransactionAsync(CancellationToken cancellationToken = default) + { + if (Connection == null) Connection = await GetConnectionAsync(cancellationToken); + if (!HasOpenTransaction) + Transaction = ((NpgsqlConnection)Connection).BeginTransaction(); + return Transaction; + } + + protected override void Dispose(bool disposing) + { + if (HasDataSource()) _dataSource.Dispose(); + _dataSource = null; + base.Dispose(disposing); + } + + private bool HasDataSource() + { + return _dataSource != null; + } + } +} diff --git a/src/Paramore.Brighter.ServiceActivator.Extensions.DependencyInjection/ServiceActivatorOptions.cs b/src/Paramore.Brighter.ServiceActivator.Extensions.DependencyInjection/ServiceActivatorOptions.cs index 12d8c9c669..2a73ca4c39 100644 --- a/src/Paramore.Brighter.ServiceActivator.Extensions.DependencyInjection/ServiceActivatorOptions.cs +++ b/src/Paramore.Brighter.ServiceActivator.Extensions.DependencyInjection/ServiceActivatorOptions.cs @@ -3,11 +3,41 @@ namespace Paramore.Brighter.ServiceActivator.Extensions.DependencyInjection { + public interface IServiceActivatorOptions + { + /// + /// Used to create a channel, an abstraction over a message processing pipeline + /// + IAmAChannelFactory ChannelFactory { get; set; } + + /// + /// An iterator over the subscriptions that this ServiceActivator has + /// + IEnumerable Subscriptions { get; set; } + + /// + /// Ensures that we use a Command Processor with as scoped lifetime, to allow scoped handlers + /// to take a dependency on it alongside other scoped dependencies such as an EF Core DbContext + /// Otherwise the CommandProcessor is a singleton. + /// + bool UseScoped { get; set; } + } + /// /// Subscriptions used when creating a service activator /// - public class ServiceActivatorOptions : BrighterOptions + public class ServiceActivatorOptions : BrighterOptions, IServiceActivatorOptions { + /// + /// Used to create a channel, an abstraction over a message processing pipeline + /// + public IAmAChannelFactory ChannelFactory { get; set; } + + /// + /// The configuration of our inbox + /// + public InboxConfiguration InboxConfiguration { get; set; } = new InboxConfiguration(); + /// /// An iterator over the subscriptions that this ServiceActivator has /// diff --git a/src/Paramore.Brighter.ServiceActivator.Extensions.DependencyInjection/ServiceCollectionExtensions.cs b/src/Paramore.Brighter.ServiceActivator.Extensions.DependencyInjection/ServiceCollectionExtensions.cs index ab13d73e7e..14dea5fd13 100644 --- a/src/Paramore.Brighter.ServiceActivator.Extensions.DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Paramore.Brighter.ServiceActivator.Extensions.DependencyInjection/ServiceCollectionExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Transactions; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; @@ -25,26 +26,33 @@ public static class ServiceActivatorServiceCollectionExtensions public static IBrighterBuilder AddServiceActivator( this IServiceCollection services, Action configure = null) - { - if (services == null) - throw new ArgumentNullException(nameof(services)); - - var options = new ServiceActivatorOptions(); - configure?.Invoke(options); - services.TryAddSingleton(options); - services.TryAddSingleton(options); + { + if (services == null) + throw new ArgumentNullException(nameof(services)); - services.TryAddSingleton(BuildDispatcher); + var options = new ServiceActivatorOptions(); + configure?.Invoke(options); + services.TryAddSingleton(options); + services.TryAddSingleton(options); + + services.TryAdd(new ServiceDescriptor(typeof(IDispatcher), + (serviceProvider) => (IDispatcher)BuildDispatcher(serviceProvider), + ServiceLifetime.Singleton)); + + services.TryAddSingleton(options.InboxConfiguration); + var inbox = options.InboxConfiguration.Inbox; + if (inbox is IAmAnInboxSync inboxSync) services.TryAddSingleton(inboxSync); + if (inbox is IAmAnInboxAsync inboxAsync) services.TryAddSingleton(inboxAsync); - return ServiceCollectionExtensions.BrighterHandlerBuilder(services, options); - } + return ServiceCollectionExtensions.BrighterHandlerBuilder(services, options); + } - private static Dispatcher BuildDispatcher(IServiceProvider serviceProvider) + private static Dispatcher BuildDispatcher(IServiceProvider serviceProvider) { var loggerFactory = serviceProvider.GetService(); ApplicationLogging.LoggerFactory = loggerFactory; - var options = serviceProvider.GetService(); + var options = serviceProvider.GetService(); Func providerFactory; diff --git a/src/Paramore.Brighter.ServiceActivator/ControlBusReceiverBuilder.cs b/src/Paramore.Brighter.ServiceActivator/ControlBusReceiverBuilder.cs index d3894e618f..aaba2543ce 100644 --- a/src/Paramore.Brighter.ServiceActivator/ControlBusReceiverBuilder.cs +++ b/src/Paramore.Brighter.ServiceActivator/ControlBusReceiverBuilder.cs @@ -24,6 +24,7 @@ THE SOFTWARE. */ using System; using System.Collections.Generic; +using System.Transactions; using Paramore.Brighter.ServiceActivator.Ports; using Paramore.Brighter.ServiceActivator.Ports.Commands; using Paramore.Brighter.ServiceActivator.Ports.Handlers; @@ -150,10 +151,14 @@ public Dispatcher Build(string hostName) var outbox = new SinkOutboxSync(); CommandProcessor commandProcessor = null; + var externalBusConfiguration = new ExternalBusConfiguration(); + externalBusConfiguration.ProducerRegistry = producerRegistry; + externalBusConfiguration.MessageMapperRegistry = outgoingMessageMapperRegistry; + commandProcessor = CommandProcessorBuilder.With() .Handlers(new HandlerConfiguration(subscriberRegistry, new ControlBusHandlerFactorySync(_dispatcher, () => commandProcessor))) .Policies(policyRegistry) - .ExternalBus(new ExternalBusConfiguration(producerRegistry, outgoingMessageMapperRegistry), outbox) + .ExternalBusCreate(externalBusConfiguration, outbox, new CommittableTransactionProvider()) .RequestContextFactory(new InMemoryRequestContextFactory()) .Build(); @@ -183,9 +188,9 @@ public Dispatcher Build(string hostName) /// /// We do not track outgoing control bus messages - so this acts as a sink for such messages /// - private class SinkOutboxSync : IAmAnOutboxSync + private class SinkOutboxSync : IAmAnOutboxSync { - public void Add(Message message, int outBoxTimeout = -1, IAmABoxTransactionConnectionProvider transactionConnectionProvider = null) + public void Add(Message message, int outBoxTimeout = -1, IAmABoxTransactionProvider transactionProvider = null) { //discard message } diff --git a/src/Paramore.Brighter.ServiceActivator/MessagePump.cs b/src/Paramore.Brighter.ServiceActivator/MessagePump.cs index 6933af14a7..a0655f0575 100644 --- a/src/Paramore.Brighter.ServiceActivator/MessagePump.cs +++ b/src/Paramore.Brighter.ServiceActivator/MessagePump.cs @@ -301,7 +301,7 @@ private void RejectMessage(Message message) /// Returns True if the message should be acked, false if the channel has handled it private bool RequeueMessage(Message message) { - message.UpdateHandledCount(); + message.Header.UpdateHandledCount(); if (DiscardRequeuedMessagesEnabled()) { diff --git a/src/Paramore.Brighter.Sqlite.Dapper/SqliteDapperConnectionProvider.cs b/src/Paramore.Brighter.Sqlite.Dapper/SqliteDapperConnectionProvider.cs deleted file mode 100644 index bf4be813aa..0000000000 --- a/src/Paramore.Brighter.Sqlite.Dapper/SqliteDapperConnectionProvider.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Data.Sqlite; -using Paramore.Brighter.Dapper; - -namespace Paramore.Brighter.Sqlite.Dapper -{ - public class SqliteDapperConnectionProvider : ISqliteTransactionConnectionProvider - { - private readonly IUnitOfWork _unitOfWork; - - public SqliteDapperConnectionProvider(IUnitOfWork unitOfWork) - { - _unitOfWork = unitOfWork; - } - - public SqliteConnection GetConnection() - { - return (SqliteConnection)_unitOfWork.Database; - } - - public Task GetConnectionAsync(CancellationToken cancellationToken = default) - { - var tcs = new TaskCompletionSource(); - tcs.SetResult(GetConnection()); - return tcs.Task; - } - - public SqliteTransaction GetTransaction() - { - return (SqliteTransaction)_unitOfWork.BeginOrGetTransaction(); - } - - public bool HasOpenTransaction - { - get - { - return _unitOfWork.HasTransaction(); - } - } - - public bool IsSharedConnection - { - get { return true; } - - } - } -} diff --git a/src/Paramore.Brighter.Sqlite.Dapper/UnitOfWork.cs b/src/Paramore.Brighter.Sqlite.Dapper/UnitOfWork.cs deleted file mode 100644 index 6f1eb27988..0000000000 --- a/src/Paramore.Brighter.Sqlite.Dapper/UnitOfWork.cs +++ /dev/null @@ -1,87 +0,0 @@ -using System; -using System.Data; -using System.Data.Common; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Data.Sqlite; -using Paramore.Brighter.Dapper; - -namespace Paramore.Brighter.Sqlite.Dapper -{ - public class UnitOfWork : IUnitOfWork - { - private readonly SqliteConnection _connection; - private SqliteTransaction _transaction; - - public UnitOfWork(DbConnectionStringProvider dbConnectionStringProvider) - { - _connection = new SqliteConnection(dbConnectionStringProvider.ConnectionString); - } - - public void Commit() - { - if (HasTransaction()) - { - _transaction.Commit(); - _transaction = null; - } - } - - public DbConnection Database - { - get { return _connection; } - } - - public DbTransaction BeginOrGetTransaction() - { - if (!HasTransaction()) - { - if (_connection.State != ConnectionState.Open) - { - _connection.Open(); - } - _transaction = _connection.BeginTransaction(); - } - - return _transaction; - } - - /// - /// Begins a transaction, if one not already started. Closes connection if required - /// - /// - /// - public async Task BeginOrGetTransactionAsync(CancellationToken cancellationToken) - { - if (!HasTransaction()) - { - if (_connection.State != ConnectionState.Open) - { - await _connection.OpenAsync(cancellationToken); - } - _transaction = _connection.BeginTransaction(); - } - - return _transaction; - } - - public bool HasTransaction() - { - return _transaction != null; - } - - public void Dispose() - { - if (HasTransaction()) - { - //will throw if transaction completed, but no way to check transaction state via api - try { _transaction.Rollback(); } catch (Exception) { } - } - - if (_connection is { State: ConnectionState.Open }) - { - _connection.Close(); - } - } - } -} diff --git a/src/Paramore.Brighter.Sqlite.EntityFrameworkCore/SqliteEntityFrameworkConnectionProvider.cs b/src/Paramore.Brighter.Sqlite.EntityFrameworkCore/SqliteEntityFrameworkConnectionProvider.cs index c024d4839a..1d966f51bf 100644 --- a/src/Paramore.Brighter.Sqlite.EntityFrameworkCore/SqliteEntityFrameworkConnectionProvider.cs +++ b/src/Paramore.Brighter.Sqlite.EntityFrameworkCore/SqliteEntityFrameworkConnectionProvider.cs @@ -1,6 +1,7 @@ -using System.Threading; +using System.Data; +using System.Data.Common; +using System.Threading; using System.Threading.Tasks; -using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Storage; @@ -10,7 +11,7 @@ namespace Paramore.Brighter.Sqlite.EntityFrameworkCore /// A connection provider that uses the same connection as EF Core /// /// The Db Context to take the connection from - public class SqliteEntityFrameworkConnectionProvider : ISqliteTransactionConnectionProvider where T: DbContext + public class SqliteEntityFrameworkConnectionProvider : RelationalDbTransactionProvider where T: DbContext { private readonly T _context; @@ -23,15 +24,40 @@ public SqliteEntityFrameworkConnectionProvider(T context) _context = context; } + /// + /// Commit the transaction + /// + public override void Commit() + { + if (HasOpenTransaction) + { + _context.Database.CurrentTransaction?.Commit(); + } + } + + /// + /// Commit the transaction + /// + /// An awaitable Task + public override Task CommitAsync(CancellationToken cancellationToken) + { + if (HasOpenTransaction) + { + _context.Database.CurrentTransaction?.CommitAsync(cancellationToken); + } + + return Task.CompletedTask; + } + /// /// Get the current connection of the DB context /// /// The Sqlite Connection that is in use - public SqliteConnection GetConnection() + public override DbConnection GetConnection() { //This line ensure that the connection has been initialised and that any required interceptors have been run before getting the connection _context.Database.CanConnect(); - return (SqliteConnection) _context.Database.GetDbConnection(); + return _context.Database.GetDbConnection(); } /// @@ -39,23 +65,30 @@ public SqliteConnection GetConnection() /// /// A cancellation token /// - public async Task GetConnectionAsync(CancellationToken cancellationToken = default) + public override async Task GetConnectionAsync(CancellationToken cancellationToken = default) { //This line ensure that the connection has been initialised and that any required interceptors have been run before getting the connection await _context.Database.CanConnectAsync(cancellationToken); - return (SqliteConnection)_context.Database.GetDbConnection(); + return _context.Database.GetDbConnection(); } /// /// Get the ambient EF Core Transaction /// /// The Sqlite Transaction - public SqliteTransaction GetTransaction() + public override DbTransaction GetTransaction() { - return (SqliteTransaction)_context.Database.CurrentTransaction?.GetDbTransaction(); + return _context.Database.CurrentTransaction?.GetDbTransaction(); } - public bool HasOpenTransaction { get => _context.Database.CurrentTransaction != null; } - public bool IsSharedConnection { get => true; } + /// + /// Is there a transaction open? + /// + public override bool HasOpenTransaction { get => _context.Database.CurrentTransaction != null; } + + /// + /// Is there a shared connection? (Do we maintain state of just create anew) + /// + public override bool IsSharedConnection { get => true; } } } diff --git a/src/Paramore.Brighter.Sqlite/ISqliteConnectionProvider.cs b/src/Paramore.Brighter.Sqlite/ISqliteConnectionProvider.cs deleted file mode 100644 index 2027fbdb67..0000000000 --- a/src/Paramore.Brighter.Sqlite/ISqliteConnectionProvider.cs +++ /dev/null @@ -1,65 +0,0 @@ -#region Licence - /* The MIT License (MIT) - Copyright © 2021 Ian Cooper - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the “Software”), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. */ - -#endregion - -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Data.Sqlite; - -namespace Paramore.Brighter.Sqlite -{ - /// - /// Use to get a connection for a Sqlite store, used with the Outbox to ensure that we can have a transaction that spans the entity and the message to be sent - /// - public interface ISqliteConnectionProvider - { - /// - /// Gets the connection we should use for the database - /// - /// A Sqlite connection - SqliteConnection GetConnection(); - - /// - /// Gets the connections we should use for the database - /// - /// Cancels the operation - /// A Sqlite connection - Task GetConnectionAsync(CancellationToken cancellationToken = default); - - /// - /// Is there an ambient transaction? If so return it - /// - /// A Sqlite Transaction - SqliteTransaction GetTransaction(); - - /// - /// Is there an open transaction - /// - bool HasOpenTransaction { get; } - - /// - /// Is this connection created externally? In which case don't close it as you don't own it. - /// - bool IsSharedConnection { get; } - } -} diff --git a/src/Paramore.Brighter.Sqlite/ISqliteTransactionConnectionProvider.cs b/src/Paramore.Brighter.Sqlite/ISqliteTransactionConnectionProvider.cs deleted file mode 100644 index da9947b96a..0000000000 --- a/src/Paramore.Brighter.Sqlite/ISqliteTransactionConnectionProvider.cs +++ /dev/null @@ -1,31 +0,0 @@ -#region Licence - /* The MIT License (MIT) - Copyright © 2021 Ian Cooper - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the “Software”), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. */ - - #endregion - -namespace Paramore.Brighter.Sqlite -{ - public interface ISqliteTransactionConnectionProvider : ISqliteConnectionProvider, IAmABoxTransactionConnectionProvider - { - - } -} diff --git a/src/Paramore.Brighter.Sqlite/SqliteConfiguration.cs b/src/Paramore.Brighter.Sqlite/SqliteConfiguration.cs deleted file mode 100644 index edf0a8afca..0000000000 --- a/src/Paramore.Brighter.Sqlite/SqliteConfiguration.cs +++ /dev/null @@ -1,67 +0,0 @@ -#region Licence - /* The MIT License (MIT) - Copyright © 2021 Ian Cooper - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the “Software”), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. */ - -#endregion - -namespace Paramore.Brighter.Sqlite -{ - public class SqliteConfiguration - { - /// - /// Initializes a new instance of the class. - /// - /// The connection string. - /// Name of the outbox table. - /// Name of the inbox table. - /// Name of the queue store table. - public SqliteConfiguration(string connectionString, string outBoxTableName = null, string inboxTableName = null, string queueStoreTable = null) - { - OutBoxTableName = outBoxTableName; - ConnectionString = connectionString; - InBoxTableName = inboxTableName; - QueueStoreTable = queueStoreTable; - } - - /// - /// Gets the connection string. - /// - /// The connection string. - public string ConnectionString { get; private set; } - - /// - /// Gets the name of the outbox table. - /// - /// The name of the outbox table. - public string OutBoxTableName { get; private set; } - - /// - /// Gets the name of the inbox table. - /// - /// The name of the inbox table. - public string InBoxTableName { get; private set; } - - /// - /// Gets the name of the queue table. - /// - public string QueueStoreTable { get; private set; } - } -} diff --git a/src/Paramore.Brighter.Sqlite/SqliteConnectionProvider.cs b/src/Paramore.Brighter.Sqlite/SqliteConnectionProvider.cs index 618397b317..1763db6b20 100644 --- a/src/Paramore.Brighter.Sqlite/SqliteConnectionProvider.cs +++ b/src/Paramore.Brighter.Sqlite/SqliteConnectionProvider.cs @@ -22,6 +22,9 @@ THE SOFTWARE. */ #endregion +using System; +using System.Data; +using System.Data.Common; using System.Threading; using System.Threading.Tasks; using Microsoft.Data.Sqlite; @@ -29,41 +32,47 @@ THE SOFTWARE. */ namespace Paramore.Brighter.Sqlite { /// - /// A connection provider that uses the connection string to create a connection + /// A connection provider for Sqlite /// - public class SqliteConnectionProvider : ISqliteConnectionProvider + public class SqliteConnectionProvider : RelationalDbConnectionProvider { private readonly string _connectionString; /// - /// Initialise a new instance of Sqlte Connection provider from a connection string + /// Create a connection provider for Sqlite using a connection string for Db access /// - /// Ms Sql Configuration - public SqliteConnectionProvider(SqliteConfiguration configuration) + /// The configuration of the Sqlite database + public SqliteConnectionProvider(IAmARelationalDatabaseConfiguration configuration) { + if (string.IsNullOrWhiteSpace(configuration?.ConnectionString)) + throw new ArgumentNullException(nameof(configuration.ConnectionString)); _connectionString = configuration.ConnectionString; } - public SqliteConnection GetConnection() + /// + /// Gets a existing Connection; creates a new one if it does not exist + /// The connection is not opened, you need to open it yourself. + /// + /// A database connection + public override DbConnection GetConnection() { - return new SqliteConnection(_connectionString); + var connection = new SqliteConnection(_connectionString); + if (connection.State != ConnectionState.Open) + connection.Open(); + return connection; } - public async Task GetConnectionAsync(CancellationToken cancellationToken = default) - { - var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - - tcs.SetResult(GetConnection()); - return await tcs.Task; - } - - public SqliteTransaction GetTransaction() + /// + /// Gets a existing Connection; creates a new one if it does not exist + /// The connection is not opened, you need to open it yourself. + /// + /// A database connection + public override async Task GetConnectionAsync(CancellationToken cancellationToken = default) { - //This Connection Factory does not support Transactions - return null; + var connection = new SqliteConnection(_connectionString); + if (connection.State != ConnectionState.Open) + await connection.OpenAsync(cancellationToken); + return connection; } - - public bool HasOpenTransaction { get => false; } - public bool IsSharedConnection { get => false; } } } diff --git a/src/Paramore.Brighter.Sqlite/SqliteUnitOfWork.cs b/src/Paramore.Brighter.Sqlite/SqliteUnitOfWork.cs new file mode 100644 index 0000000000..36d0a73704 --- /dev/null +++ b/src/Paramore.Brighter.Sqlite/SqliteUnitOfWork.cs @@ -0,0 +1,120 @@ +#region Licence + /* The MIT License (MIT) + Copyright © 2021 Ian Cooper + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the “Software”), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. */ + +#endregion + +using System; +using System.Data; +using System.Data.Common; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Data.Sqlite; + +namespace Paramore.Brighter.Sqlite +{ + /// + /// A connection provider for Sqlite + /// + public class SqliteUnitOfWork : RelationalDbTransactionProvider + { + private readonly string _connectionString; + + /// + /// Create a connection provider for Sqlite using a connection string for Db access + /// + /// The configuration of the Sqlite database + public SqliteUnitOfWork(IAmARelationalDatabaseConfiguration configuration) + { + if (string.IsNullOrWhiteSpace(configuration?.ConnectionString)) + throw new ArgumentNullException(nameof(configuration.ConnectionString)); + _connectionString = configuration.ConnectionString; + } + + /// + /// Commit the transaction + /// + /// An awaitable Task + public override Task CommitAsync(CancellationToken cancellationToken) + { + if (HasOpenTransaction) + { + ((SqliteTransaction)Transaction).CommitAsync(cancellationToken); + Transaction = null; + } + + return Task.CompletedTask; + } + + /// + /// Gets a existing Connection; creates a new one if it does not exist + /// This is a shared connection, so you should use this interface to manage it + /// + /// A database connection + public override DbConnection GetConnection() + { + if (Connection == null) { Connection = new SqliteConnection(_connectionString);} + if (Connection.State != ConnectionState.Open) + Connection.Open(); + return Connection; + } + + /// + /// Gets a existing Connection; creates a new one if it does not exist + /// This is a shared connection, so you should use this interface to manage it + /// + /// A database connection + public override async Task GetConnectionAsync(CancellationToken cancellationToken = default) + { + if(Connection == null) { Connection = new SqliteConnection(_connectionString);} + + if (Connection.State != ConnectionState.Open) + await Connection.OpenAsync(cancellationToken); + return Connection; + } + + /// + /// Creates and opens a Sqlite Transaction + /// This is a shared transaction and you should manage it through the unit of work + /// + /// DbTransaction + public override DbTransaction GetTransaction() + { + if (Connection == null) Connection = GetConnection(); + if (!HasOpenTransaction) + Transaction = Connection.BeginTransaction(); + return Transaction; + } + + /// + /// Creates and opens a Sqlite Transaction + /// This is a shared transaction and you should manage it through the unit of work + /// + /// DbTransaction + public override async Task GetTransactionAsync(CancellationToken cancellationToken = default) + { + if (Connection == null) Connection = await GetConnectionAsync(cancellationToken); + if (!HasOpenTransaction) + Transaction = await Connection.BeginTransactionAsync(cancellationToken); + return Transaction; + } + } +} diff --git a/src/Paramore.Brighter/CommandProcessor.cs b/src/Paramore.Brighter/CommandProcessor.cs index 38be239e67..332bc4e52a 100644 --- a/src/Paramore.Brighter/CommandProcessor.cs +++ b/src/Paramore.Brighter/CommandProcessor.cs @@ -24,13 +24,13 @@ THE SOFTWARE. */ #endregion using System; -using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reflection; using System.Threading; using System.Threading.Tasks; +using System.Transactions; using Microsoft.Extensions.Logging; using Paramore.Brighter.FeatureSwitch; using Paramore.Brighter.Logging; @@ -55,7 +55,6 @@ public class CommandProcessor : IAmACommandProcessor private readonly IAmARequestContextFactory _requestContextFactory; private readonly IPolicyRegistry _policyRegistry; private readonly InboxConfiguration _inboxConfiguration; - private readonly IAmABoxTransactionConnectionProvider _boxTransactionConnectionProvider; private readonly IAmAFeatureSwitchRegistry _featureSwitchRegistry; private readonly IEnumerable _replySubscriptions; private readonly TransformPipelineBuilder _transformPipelineBuilder; @@ -97,13 +96,15 @@ public class CommandProcessor : IAmACommandProcessor /// public const string RETRYPOLICYASYNC = "Paramore.Brighter.CommandProcessor.RetryPolicy.Async"; - //We want to use double lock to let us pass parameters to the constructor from the first instance - private static ExternalBusServices _bus = null; + /// + /// We want to use double lock to let us pass parameters to the constructor from the first instance + /// + private static IAmAnExternalBusService _bus = null; private static readonly object padlock = new object(); /// - /// Initializes a new instance of the class. - /// Use this constructor when no external bus is required and only sync handlers are needed + /// Initializes a new instance of the class + /// NO EXTERNAL BUS: Use this constructor when no external bus is required /// /// The subscriber registry. /// The handler factory. @@ -117,8 +118,7 @@ public CommandProcessor( IAmARequestContextFactory requestContextFactory, IPolicyRegistry policyRegistry, IAmAFeatureSwitchRegistry featureSwitchRegistry = null, - InboxConfiguration inboxConfiguration = null - ) + InboxConfiguration inboxConfiguration = null) { _subscriberRegistry = subscriberRegistry; @@ -139,132 +139,75 @@ public CommandProcessor( /// /// Initializes a new instance of the class. - /// Use this constructor when only posting messages to an external bus is required - /// - /// The request context factory. - /// The policy registry. - /// The mapper registry. - /// The outbox - /// The register of producers via whom we send messages over the external bus - /// How long should we wait to write to the outbox - /// The feature switch config provider. - /// Do we want to insert an inbox handler into pipelines without the attribute. Null (default = no), yes = how to configure - /// The Box Connection Provider to use when Depositing into the outbox. - /// The maximum amount of messages to deposit into the outbox in one transmissions. - /// The factory used to create a transformer pipeline for a message mapper - public CommandProcessor(IAmARequestContextFactory requestContextFactory, - IPolicyRegistry policyRegistry, - IAmAMessageMapperRegistry mapperRegistry, - IAmAnOutbox outBox, - IAmAProducerRegistry producerRegistry, - int outboxTimeout = 300, - IAmAFeatureSwitchRegistry featureSwitchRegistry = null, - InboxConfiguration inboxConfiguration = null, - IAmABoxTransactionConnectionProvider boxTransactionConnectionProvider = null, - int outboxBulkChunkSize = 100, - IAmAMessageTransformerFactory messageTransformerFactory = null) - { - _requestContextFactory = requestContextFactory; - _policyRegistry = policyRegistry; - _featureSwitchRegistry = featureSwitchRegistry; - _inboxConfiguration = inboxConfiguration; - _boxTransactionConnectionProvider = boxTransactionConnectionProvider; - _transformPipelineBuilder = new TransformPipelineBuilder(mapperRegistry, messageTransformerFactory); - - InitExtServiceBus(policyRegistry, outBox, outboxTimeout, producerRegistry, outboxBulkChunkSize); - - ConfigureCallbacks(producerRegistry); - } - - /// - /// Initializes a new instance of the class. - /// Use this constructor when both rpc support is required + /// EXTERNAL BUS AND INTERNAL BUS: Use this constructor when both external bus and command processor support is required + /// OPTIONAL RPC: You can use this if you want to use the command processor as a client to an external bus, but also want to support RPC /// /// The subscriber registry. /// The handler factory. /// The request context factory. /// The policy registry. /// The mapper registry. - /// The outbox - /// The register of producers via whom we send messages over the external bus - /// The Subscriptions for creating the reply queues - /// How long should we wait to write to the outbox + /// The external service bus that we want to send messages over /// The feature switch config provider. - /// If we are expecting a response, then we need a channel to listen on /// Do we want to insert an inbox handler into pipelines without the attribute. Null (default = no), yes = how to configure - /// The Box Connection Provider to use when Depositing into the outbox. - /// The maximum amount of messages to deposit into the outbox in one transmissions. /// The factory used to create a transformer pipeline for a message mapper - public CommandProcessor(IAmASubscriberRegistry subscriberRegistry, + /// The Subscriptions for creating the reply queues + /// If we are expecting a response, then we need a channel to listen on + public CommandProcessor( + IAmASubscriberRegistry subscriberRegistry, IAmAHandlerFactory handlerFactory, IAmARequestContextFactory requestContextFactory, IPolicyRegistry policyRegistry, IAmAMessageMapperRegistry mapperRegistry, - IAmAnOutbox outBox, - IAmAProducerRegistry producerRegistry, - IEnumerable replySubscriptions, - int outboxTimeout = 300, + IAmAnExternalBusService bus, IAmAFeatureSwitchRegistry featureSwitchRegistry = null, - IAmAChannelFactory responseChannelFactory = null, InboxConfiguration inboxConfiguration = null, - IAmABoxTransactionConnectionProvider boxTransactionConnectionProvider = null, - int outboxBulkChunkSize = 100, - IAmAMessageTransformerFactory messageTransformerFactory = null) - : this(subscriberRegistry, handlerFactory, requestContextFactory, policyRegistry) + IAmAMessageTransformerFactory messageTransformerFactory = null, + IEnumerable replySubscriptions = null, + IAmAChannelFactory responseChannelFactory = null + ) + : this(subscriberRegistry, handlerFactory, requestContextFactory, policyRegistry, featureSwitchRegistry, inboxConfiguration) { - _featureSwitchRegistry = featureSwitchRegistry; _responseChannelFactory = responseChannelFactory; - _inboxConfiguration = inboxConfiguration; - _boxTransactionConnectionProvider = boxTransactionConnectionProvider; _replySubscriptions = replySubscriptions; _transformPipelineBuilder = new TransformPipelineBuilder(mapperRegistry, messageTransformerFactory); - InitExtServiceBus(policyRegistry, outBox, outboxTimeout, producerRegistry, outboxBulkChunkSize); - - ConfigureCallbacks(producerRegistry); + InitExtServiceBus(bus); } /// /// Initializes a new instance of the class. - /// Use this constructor when both external bus and command processor support is required + /// EXTERNAL BUS, NO INTERNAL BUS: Use this constructor when only posting messages to an external bus is required /// - /// The subscriber registry. - /// The handler factory. /// The request context factory. /// The policy registry. /// The mapper registry. - /// The outbox. - /// The register of producers via whom we send messages over the external bus - /// How long should we wait to write to the outbox + /// The external service bus that we want to send messages over /// The feature switch config provider. /// Do we want to insert an inbox handler into pipelines without the attribute. Null (default = no), yes = how to configure - /// The Box Connection Provider to use when Depositing into the outbox. - /// The maximum amount of messages to deposit into the outbox in one transmissions. /// The factory used to create a transformer pipeline for a message mapper - public CommandProcessor(IAmASubscriberRegistry subscriberRegistry, - IAmAHandlerFactory handlerFactory, + /// The Subscriptions for creating the reply queues + public CommandProcessor( IAmARequestContextFactory requestContextFactory, IPolicyRegistry policyRegistry, IAmAMessageMapperRegistry mapperRegistry, - IAmAnOutbox outBox, - IAmAProducerRegistry producerRegistry, - int outboxTimeout = 300, + IAmAnExternalBusService bus, IAmAFeatureSwitchRegistry featureSwitchRegistry = null, InboxConfiguration inboxConfiguration = null, - IAmABoxTransactionConnectionProvider boxTransactionConnectionProvider = null, - int outboxBulkChunkSize = 100, - IAmAMessageTransformerFactory messageTransformerFactory = null) - : this(subscriberRegistry, handlerFactory, requestContextFactory, policyRegistry, featureSwitchRegistry) + IAmAMessageTransformerFactory messageTransformerFactory = null, + IEnumerable replySubscriptions = null) { + _requestContextFactory = requestContextFactory; + _policyRegistry = policyRegistry; + _featureSwitchRegistry = featureSwitchRegistry; _inboxConfiguration = inboxConfiguration; - _boxTransactionConnectionProvider = boxTransactionConnectionProvider; _transformPipelineBuilder = new TransformPipelineBuilder(mapperRegistry, messageTransformerFactory); + _replySubscriptions = replySubscriptions; - InitExtServiceBus(policyRegistry, outBox, outboxTimeout, producerRegistry, outboxBulkChunkSize); - - ConfigureCallbacks(producerRegistry); + InitExtServiceBus(bus); } + /// /// Sends the specified command. We expect only one handler. The command is handled synchronously. /// @@ -316,8 +259,7 @@ public void Send(T command) where T : class, IRequest /// Should we use the calling thread's synchronization context when continuing or a default thread synchronization context. Defaults to false /// Allows the sender to cancel the request pipeline. Optional /// awaitable . - public async Task SendAsync(T command, bool continueOnCapturedContext = false, - CancellationToken cancellationToken = default) + public async Task SendAsync(T command, bool continueOnCapturedContext = false, CancellationToken cancellationToken = default) where T : class, IRequest { if (_handlerFactoryAsync == null) @@ -482,7 +424,7 @@ public async Task PublishAsync(T @event, bool continueOnCapturedContext = fal } } } - + /// /// Posts the specified request. The message is placed on a task queue and into a outbox for reposting in the event of failure. /// You will need to configure a service that reads from the task queue to process the message @@ -493,12 +435,12 @@ public async Task PublishAsync(T @event, bool continueOnCapturedContext = fal /// Please note that this call will not participate in any ambient Transactions, if you wish to have the outbox participate in a Transaction please Use Deposit, /// and then after you have committed your transaction use ClearOutbox /// - /// /// The request. + /// The type of request /// - public void Post(T request) where T : class, IRequest + public void Post(TRequest request) where TRequest : class, IRequest { - ClearOutbox(DepositPost(request, null)); + ClearOutbox(DepositPost(request, (IAmABoxTransactionProvider)null)); } /// @@ -511,20 +453,23 @@ public void Post(T request) where T : class, IRequest /// Please note that this call will not participate in any ambient Transactions, if you wish to have the outbox participate in a Transaction please Use DepositAsync, /// and then after you have committed your transaction use ClearOutboxAsync /// - /// /// The request. /// Should we use the calling thread's synchronization context when continuing or a default thread synchronization context. Defaults to false /// Allows the sender to cancel the request pipeline. Optional + /// The type of request /// /// awaitable . - public async Task PostAsync(T request, bool continueOnCapturedContext = false, - CancellationToken cancellationToken = default) - where T : class, IRequest + public async Task PostAsync( + TRequest request, + bool continueOnCapturedContext = false, + CancellationToken cancellationToken = default + ) + where TRequest : class, IRequest { - var messageId = await DepositPostAsync(request, null, continueOnCapturedContext, cancellationToken); + var messageId = await DepositPostAsync(request, (IAmABoxTransactionProvider)null, continueOnCapturedContext, cancellationToken); await ClearOutboxAsync(new Guid[] { messageId }, continueOnCapturedContext, cancellationToken); } - + /// /// Adds a message into the outbox, and returns the id of the saved message. /// Intended for use with the Outbox pattern: http://gistlabs.com/2014/05/the-outbox/ normally you include the @@ -533,49 +478,84 @@ public async Task PostAsync(T request, bool continueOnCapturedContext = false /// Pass deposited Guid to /// /// The request to save to the outbox - /// The type of the request + /// The type of the request /// The Id of the Message that has been deposited. - public Guid DepositPost(T request) where T : class, IRequest + public Guid DepositPost(TRequest request) where TRequest : class, IRequest { - return DepositPost(request, _boxTransactionConnectionProvider); + return DepositPost(request, null); } /// - /// Adds a messages into the outbox, and returns the id of the saved message. + /// Adds a message into the outbox, and returns the id of the saved message. /// Intended for use with the Outbox pattern: http://gistlabs.com/2014/05/the-outbox/ normally you include the /// call to DepositPostBox within the scope of the transaction to write corresponding entity state to your /// database, that you want to signal via the request to downstream consumers /// Pass deposited Guid to /// - /// The requests to save to the outbox - /// The type of the request + /// The request to save to the outbox + /// The transaction provider to use with an outbox + /// The type of the request + /// The type of Db transaction used by the Outbox /// The Id of the Message that has been deposited. - public Guid[] DepositPost(IEnumerable requests) where T : class, IRequest - { - return DepositPost(requests, _boxTransactionConnectionProvider); - } - - private Guid DepositPost(T request, IAmABoxTransactionConnectionProvider connectionProvider) - where T : class, IRequest + public Guid DepositPost( + TRequest request, + IAmABoxTransactionProvider transactionProvider + ) where TRequest : class, IRequest { s_logger.LogInformation("Save request: {RequestType} {Id}", request.GetType(), request.Id); - if (!_bus.HasOutbox()) + var bus = ((ExternalBusServices)_bus); + + if (!bus.HasOutbox()) throw new InvalidOperationException("No outbox defined."); - var message = _transformPipelineBuilder.BuildWrapPipeline().WrapAsync(request).GetAwaiter().GetResult(); + var message = _transformPipelineBuilder.BuildWrapPipeline().WrapAsync(request).GetAwaiter().GetResult(); - AddTelemetryToMessage(message); + AddTelemetryToMessage(message); - _bus.AddToOutbox(request, message, connectionProvider); + bus.AddToOutbox(request, message, transactionProvider); return message.Id; } - - private Guid[] DepositPost(IEnumerable requests, IAmABoxTransactionConnectionProvider connectionProvider) - where T : class, IRequest + + /// + /// Adds a messages into the outbox, and returns the id of the saved message. + /// Intended for use with the Outbox pattern: http://gistlabs.com/2014/05/the-outbox/ normally you include the + /// call to DepositPostBox within the scope of the transaction to write corresponding entity state to your + /// database, that you want to signal via the request to downstream consumers + /// Pass deposited Guid to + /// + /// The requests to save to the outbox + /// The type of the request + /// The Id of the Message that has been deposited. + public Guid[] DepositPost(IEnumerable requests) + where TRequest : class, IRequest { - if (!_bus.HasBulkOutbox()) + return DepositPost(requests, null); + } + + /// + /// Adds a messages into the outbox, and returns the id of the saved message. + /// Intended for use with the Outbox pattern: http://gistlabs.com/2014/05/the-outbox/ normally you include the + /// call to DepositPostBox within the scope of the transaction to write corresponding entity state to your + /// database, that you want to signal via the request to downstream consumers + /// Pass deposited Guid to + /// + /// The requests to save to the outbox + /// The transaction provider to use with an outbox + /// The type of the request + /// The type of transaction used by the Outbox + /// The Id of the Message that has been deposited. + public Guid[] DepositPost( + IEnumerable requests, + IAmABoxTransactionProvider transactionProvider + ) where TRequest : class, IRequest + { + s_logger.LogInformation("Save bulk requests request: {RequestType}", typeof(TRequest)); + + var bus = ((ExternalBusServices)_bus); + + if (!bus.HasBulkOutbox()) throw new InvalidOperationException("No Bulk outbox defined."); var successfullySentMessage = new List(); @@ -586,30 +566,38 @@ private Guid[] DepositPost(IEnumerable requests, IAmABoxTransactionConnect s_logger.LogInformation("Save requests: {RequestType} {AmountOfMessages}", batch.Key, messages.Count()); - _bus.AddToOutbox(messages, connectionProvider); + bus.AddToOutbox(messages, transactionProvider); successfullySentMessage.AddRange(messages.Select(m => m.Id)); } return successfullySentMessage.ToArray(); } - + /// /// Adds a message into the outbox, and returns the id of the saved message. /// Intended for use with the Outbox pattern: http://gistlabs.com/2014/05/the-outbox/ normally you include the /// call to DepositPostBox within the scope of the transaction to write corresponding entity state to your /// database, that you want to signal via the request to downstream consumers - /// Pass deposited Guid to + /// Pass deposited Guid to + /// NOTE: If you get an error about the transaction type not matching CommittableTransaction, then you need to + /// use the specialized version of this method that takes a transaction provider. /// /// The request to save to the outbox /// Should we use the calling thread's synchronization context when continuing or a default thread synchronization context. Defaults to false /// The Cancellation Token. - /// The type of the request + /// The type of the request /// - public async Task DepositPostAsync(T request, bool continueOnCapturedContext = false, - CancellationToken cancellationToken = default) where T : class, IRequest + public async Task DepositPostAsync( + TRequest request, + bool continueOnCapturedContext = false, + CancellationToken cancellationToken = default + ) where TRequest : class, IRequest { - return await DepositPostAsync(request, _boxTransactionConnectionProvider, continueOnCapturedContext, + return await DepositPostAsync( + request, + null, + continueOnCapturedContext, cancellationToken); } @@ -620,46 +608,112 @@ public async Task DepositPostAsync(T request, bool continueOnCapturedCo /// database, that you want to signal via the request to downstream consumers /// Pass deposited Guid to /// - /// The requests to save to the outbox + /// The request to save to the outbox + /// The transaction provider to use with an outbox /// Should we use the calling thread's synchronization context when continuing or a default thread synchronization context. Defaults to false /// The Cancellation Token. - /// The type of the request + /// The type of the request + /// The type of the transaction used by the Outbox /// - public Task DepositPostAsync(IEnumerable requests, bool continueOnCapturedContext = false, - CancellationToken cancellationToken = default) where T : class, IRequest - { - return DepositPostAsync(requests, _boxTransactionConnectionProvider, continueOnCapturedContext, - cancellationToken); - } - - private async Task DepositPostAsync(T request, IAmABoxTransactionConnectionProvider connectionProvider, + public async Task DepositPostAsync( + TRequest request, + IAmABoxTransactionProvider transactionProvider, bool continueOnCapturedContext = false, - CancellationToken cancellationToken = default) where T : class, IRequest + CancellationToken cancellationToken = default + ) where TRequest : class, IRequest { s_logger.LogInformation("Save request: {RequestType} {Id}", request.GetType(), request.Id); + + var bus = ((ExternalBusServices)_bus); - if (!_bus.HasAsyncOutbox()) + if (!bus.HasAsyncOutbox()) throw new InvalidOperationException("No async outbox defined."); - var message = await _transformPipelineBuilder.BuildWrapPipeline().WrapAsync(request, cancellationToken); + var message = await _transformPipelineBuilder.BuildWrapPipeline().WrapAsync(request, cancellationToken); - AddTelemetryToMessage(message); + AddTelemetryToMessage(message); - await _bus.AddToOutboxAsync(request, continueOnCapturedContext, cancellationToken, message, - connectionProvider); + await bus.AddToOutboxAsync(request, message, + transactionProvider, continueOnCapturedContext, cancellationToken); return message.Id; } + + /// + /// Adds a message into the outbox, and returns the id of the saved message. + /// Intended for use with the Outbox pattern: http://gistlabs.com/2014/05/the-outbox/ normally you include the + /// call to DepositPostBox within the scope of the transaction to write corresponding entity state to your + /// database, that you want to signal via the request to downstream consumers + /// Pass deposited Guid to + /// + /// The requests to save to the outbox + /// Should we use the calling thread's synchronization context when continuing or a default thread synchronization context. Defaults to false + /// The Cancellation Token. + /// The type of the request + /// + public async Task DepositPostAsync( + IEnumerable requests, + bool continueOnCapturedContext = false, + CancellationToken cancellationToken = default + ) where TRequest : class, IRequest + { + return await DepositPostAsync( + requests, + null, + continueOnCapturedContext, + cancellationToken); + } + + /// + /// Adds a message into the outbox, and returns the id of the saved message. + /// Intended for use with the Outbox pattern: http://gistlabs.com/2014/05/the-outbox/ normally you include the + /// call to DepositPostBox within the scope of the transaction to write corresponding entity state to your + /// database, that you want to signal via the request to downstream consumers + /// Pass deposited Guid to + /// + /// The requests to save to the outbox + /// The transaction provider used with the Outbox + /// Should we use the calling thread's synchronization context when continuing or a default thread synchronization context. Defaults to false + /// The Cancellation Token. + /// The type of the request + /// The type of transaction used with the Outbox + /// + public async Task DepositPostAsync( + IEnumerable requests, + IAmABoxTransactionProvider transactionProvider, + bool continueOnCapturedContext = false, + CancellationToken cancellationToken = default + ) where TRequest : class, IRequest + { + var bus = ((ExternalBusServices)_bus); + + if (!bus.HasAsyncBulkOutbox()) + throw new InvalidOperationException("No bulk async outbox defined."); + + var successfullySentMessage = new List(); + + foreach (var batch in SplitRequestBatchIntoTypes(requests)) + { + var messages = await MapMessagesAsync(batch.Key, batch.ToArray(), cancellationToken); + s_logger.LogInformation("Save requests: {RequestType} {AmountOfMessages}", batch.Key, messages.Count()); + + await bus.AddToOutboxAsync(messages, transactionProvider, continueOnCapturedContext, cancellationToken); + + successfullySentMessage.AddRange(messages.Select(m => m.Id)); + } + + return successfullySentMessage.ToArray(); + } /// - /// Flushes the message box message given by to the broker. + /// Flushes the messages in the id list from the Outbox. /// Intended for use with the Outbox pattern: http://gistlabs.com/2014/05/the-outbox/ /// - /// The posts to flush - public void ClearOutbox(params Guid[] posts) + /// The message ids to flush + public void ClearOutbox(params Guid[] ids) { - _bus.ClearOutbox(posts); + _bus.ClearOutbox(ids); } /// @@ -679,7 +733,9 @@ public void ClearOutbox(int amountToClear = 100, int minimumAge = 5000, Dictiona /// Flushes the message box message given by to the broker. /// Intended for use with the Outbox pattern: http://gistlabs.com/2014/05/the-outbox/ /// - /// The posts to flush + /// The ids to flush + /// Should the callback run on a new thread? + /// The token to cancel a running asynchronous operation public async Task ClearOutboxAsync( IEnumerable posts, bool continueOnCapturedContext = false, @@ -779,7 +835,7 @@ public TResponse Call(T request, int timeOutInMilliseconds) return response; } //clean up everything at this point, whatever happens } - + /// /// The external service bus is a singleton as it has app lifetime to manage an Outbox. /// This method clears the external service bus, so that the next attempt to use it will create a fresh one @@ -799,6 +855,17 @@ public static void ClearExtServiceBus() } } } + + private void AddTelemetryToMessage(Message message) + { + var activity = Activity.Current ?? + ApplicationTelemetry.ActivitySource.StartActivity(DEPOSITPOST, ActivityKind.Producer); + + if (activity != null) + { + message.Header.AddTelemetryInformation(activity, typeof(T).ToString()); + } + } private void AssertValidSendPipeline(T command, int handlerCount) where T : class, IRequest { @@ -812,114 +879,7 @@ private void AssertValidSendPipeline(T command, int handlerCount) where T : c throw new ArgumentException( $"No command handler was found for the typeof command {typeof(T)} - a command should have exactly one handler."); } - - - private void ConfigureCallbacks(IAmAProducerRegistry producerRegistry) - { - //Only register one, to avoid two callbacks where we support both interfaces on a producer - foreach (var producer in producerRegistry.Producers) - { - if (!_bus.ConfigurePublisherCallbackMaybe(producer)) - _bus.ConfigureAsyncPublisherCallbackMaybe(producer); - } - } - - private (Activity span, bool created) GetSpan(string activityName) - { - bool create = Activity.Current == null; - - if (create) - return (ApplicationTelemetry.ActivitySource.StartActivity(activityName, ActivityKind.Server), create); - else - return (Activity.Current, create); - } - - private void EndSpan(Activity span) - { - if (span?.Status == ActivityStatusCode.Unset) - span.SetStatus(ActivityStatusCode.Ok); - span?.Dispose(); - } - - //Create an instance of the ExternalBusServices if one not already set for this app. Note that we do not support reinitialization here, so once you have - //set a command processor for the app, you can't call init again to set them - although the properties are not read-only so overwriting is possible - //if needed as a "get out of gaol" card. - private static void InitExtServiceBus( - IPolicyRegistry policyRegistry, - IAmAnOutbox outbox, - int outboxTimeout, - IAmAProducerRegistry producerRegistry, - int outboxBulkChunkSize) - { - if (_bus == null) - { - lock (padlock) - { - if (_bus == null) - { - if (producerRegistry == null) - throw new ConfigurationException( - "A producer registry is required to create an external bus"); - - _bus = new ExternalBusServices(); - if (outbox is IAmAnOutboxSync syncOutbox) _bus.OutBox = syncOutbox; - if (outbox is IAmAnOutboxAsync asyncOutbox) _bus.AsyncOutbox = asyncOutbox; - - _bus.OutboxTimeout = outboxTimeout; - _bus.PolicyRegistry = policyRegistry; - _bus.ProducerRegistry = producerRegistry; - _bus.OutboxBulkChunkSize = outboxBulkChunkSize; - } - } - } - } - - private async Task DepositPostAsync(IEnumerable requests, - IAmABoxTransactionConnectionProvider connectionProvider, bool continueOnCapturedContext = false, - CancellationToken cancellationToken = default) where T : class, IRequest - { - if (!_bus.HasAsyncBulkOutbox()) - throw new InvalidOperationException("No bulk async outbox defined."); - - var successfullySentMessage = new List(); - - foreach (var batch in SplitRequestBatchIntoTypes(requests)) - { - var messages = await MapMessagesAsync(batch.Key, batch.ToArray(), cancellationToken); - - s_logger.LogInformation("Save requests: {RequestType} {AmountOfMessages}", batch.Key, messages.Count()); - - await _bus.AddToOutboxAsync(messages, continueOnCapturedContext, cancellationToken, connectionProvider); - - successfullySentMessage.AddRange(messages.Select(m => m.Id)); - } - - return successfullySentMessage.ToArray(); - } - - private IEnumerable> SplitRequestBatchIntoTypes(IEnumerable requests) - { - return requests.GroupBy(r => r.GetType()); - } - - private List MapMessages(Type requestType, IEnumerable requests) - { - return (List)GetType() - .GetMethod(nameof(BulkMapMessages), BindingFlags.Instance | BindingFlags.NonPublic) - .MakeGenericMethod(requestType) - .Invoke(this, new[] { requests }); - } - - private Task> MapMessagesAsync(Type requestType, IEnumerable requests, - CancellationToken cancellationToken) - { - var parameters = new object[] { requests, cancellationToken }; - return (Task>)GetType() - .GetMethod(nameof(BulkMapMessagesAsync), BindingFlags.Instance | BindingFlags.NonPublic) - .MakeGenericMethod(requestType) - .Invoke(this, parameters); - } - + private List BulkMapMessages(IEnumerable requests) where T : class, IRequest { return requests.Select(r => @@ -945,19 +905,38 @@ private async Task> BulkMapMessagesAsync(IEnumerable return messages; } - - private void AddTelemetryToMessage(Message message) + + // Create an instance of the ExternalBusServices if one not already set for this app. Note that we do not support reinitialization here, so once you have + // set a command processor for the app, you can't call init again to set them - although the properties are not read-only so overwriting is possible + // if needed as a "get out of gaol" card. + private static void InitExtServiceBus(IAmAnExternalBusService bus) { - var activity = Activity.Current ?? - ApplicationTelemetry.ActivitySource.StartActivity(DEPOSITPOST, ActivityKind.Producer); - - if (activity != null) + if (_bus == null) { - message.Header.AddTelemetryInformation(activity, typeof(T).ToString()); + lock (padlock) + { + _bus ??= bus; + } } } + + private void EndSpan(Activity span) + { + if (span?.Status == ActivityStatusCode.Unset) + span.SetStatus(ActivityStatusCode.Ok); + span?.Dispose(); + } + private (Activity span, bool created) GetSpan(string activityName) + { + bool create = Activity.Current == null; + if (create) + return (ApplicationTelemetry.ActivitySource.StartActivity(activityName, ActivityKind.Server), create); + else + return (Activity.Current, create); + } + private bool HandlerFactoryIsNotEitherIAmAHandlerFactorySyncOrAsync(IAmAHandlerFactory handlerFactory) { // If we do not have a subscriber registry and we do not have a handler factory @@ -974,5 +953,28 @@ private bool HandlerFactoryIsNotEitherIAmAHandlerFactorySyncOrAsync(IAmAHandlerF return true; } } + + private List MapMessages(Type requestType, IEnumerable requests) + { + return (List)GetType() + .GetMethod(nameof(BulkMapMessages), BindingFlags.Instance | BindingFlags.NonPublic) + .MakeGenericMethod(requestType) + .Invoke(this, new[] { requests }); + } + + private Task> MapMessagesAsync(Type requestType, IEnumerable requests, + CancellationToken cancellationToken) + { + var parameters = new object[] { requests, cancellationToken }; + return (Task>)GetType() + .GetMethod(nameof(BulkMapMessagesAsync), BindingFlags.Instance | BindingFlags.NonPublic) + .MakeGenericMethod(requestType) + .Invoke(this, parameters); + } + + private IEnumerable> SplitRequestBatchIntoTypes(IEnumerable requests) + { + return requests.GroupBy(r => r.GetType()); + } } } diff --git a/src/Paramore.Brighter/CommandProcessorBuilder.cs b/src/Paramore.Brighter/CommandProcessorBuilder.cs index 228fcdba20..d857583590 100644 --- a/src/Paramore.Brighter/CommandProcessorBuilder.cs +++ b/src/Paramore.Brighter/CommandProcessorBuilder.cs @@ -23,6 +23,7 @@ THE SOFTWARE. */ #endregion using System.Collections.Generic; +using System.Transactions; using Paramore.Brighter.FeatureSwitch; using Polly.Registry; @@ -50,7 +51,7 @@ namespace Paramore.Brighter /// /// /// - /// A describing how you want to configure Task Queues for the . We store messages in a + /// A describing how you want to configure Task Queues for the . We store messages in a /// for later replay (in case we need to compensate by trying a message again). We send messages to a Task Queue via a and we want to know how /// to map the ( or ) to a using a using /// an . You can use the default to register the association. You need to @@ -70,8 +71,6 @@ namespace Paramore.Brighter /// public class CommandProcessorBuilder : INeedAHandlers, INeedPolicy, INeedMessaging, INeedARequestContext, IAmACommandProcessorBuilder { - private IAmAnOutbox _outbox; - private IAmAProducerRegistry _producers; private IAmAMessageMapperRegistry _messageMapperRegistry; private IAmAMessageTransformerFactory _transformerFactory; private IAmARequestContextFactory _requestContextFactory; @@ -79,13 +78,11 @@ public class CommandProcessorBuilder : INeedAHandlers, INeedPolicy, INeedMessagi private IAmAHandlerFactory _handlerFactory; private IPolicyRegistry _policyRegistry; private IAmAFeatureSwitchRegistry _featureSwitchRegistry; - private IAmAChannelFactory _responseChannelFactory; - private int _outboxWriteTimeout; - private bool _useExternalBus = false; + private IAmAnExternalBusService _bus = null; private bool _useRequestReplyQueues = false; + private IAmAChannelFactory _responseChannelFactory; private IEnumerable _replySubscriptions; - private IAmABoxTransactionConnectionProvider _overridingBoxTransactionConnectionProvider = null; - private int _outboxBulkChunkSize; + private InboxConfiguration _inboxConfiguration; private CommandProcessorBuilder() { @@ -123,24 +120,24 @@ public INeedAHandlers ConfigureFeatureSwitches(IAmAFeatureSwitchRegistry feature _featureSwitchRegistry = featureSwitchRegistry; return this; } - + /// /// Supplies the specified the policy registry, so we can use policies for Task Queues or in user-defined request handlers such as ExceptionHandler /// that provide quality of service concerns /// - /// The policy registry. + /// The policy registry. /// INeedLogging. /// The policy registry is missing the CommandProcessor.RETRYPOLICY policy which is required /// The policy registry is missing the CommandProcessor.CIRCUITBREAKER policy which is required - public INeedMessaging Policies(IPolicyRegistry thePolicyRegistry) + public INeedMessaging Policies(IPolicyRegistry policyRegistry) { - if (!thePolicyRegistry.ContainsKey(CommandProcessor.RETRYPOLICY)) + if (!policyRegistry.ContainsKey(CommandProcessor.RETRYPOLICY)) throw new ConfigurationException("The policy registry is missing the CommandProcessor.RETRYPOLICY policy which is required"); - if (!thePolicyRegistry.ContainsKey(CommandProcessor.CIRCUITBREAKER)) + if (!policyRegistry.ContainsKey(CommandProcessor.CIRCUITBREAKER)) throw new ConfigurationException("The policy registry is missing the CommandProcessor.CIRCUITBREAKER policy which is required"); - _policyRegistry = thePolicyRegistry; + _policyRegistry = policyRegistry; return this; } @@ -154,6 +151,50 @@ public INeedMessaging DefaultPolicy() return this; } + /// + /// The wants to support or using an external bus. + /// You need to provide a policy to specify how QoS issues, specifically or + /// are handled by adding appropriate when choosing this option. + /// + /// The type of Bus: In-memory, Db, or RPC + /// The service bus that we need to use to send messages externally + /// The registry of mappers or messages to requests needed for outgoing messages + /// A factory for common transforms of messages + /// A factory for channels used to handle RPC responses + /// If we use a request reply queue how do we subscribe to replies + /// + public INeedARequestContext ExternalBus( + ExternalBusType busType, + IAmAnExternalBusService bus, + IAmAMessageMapperRegistry messageMapperRegistry, + IAmAMessageTransformerFactory transformerFactory, + IAmAChannelFactory responseChannelFactory = null, + IEnumerable subscriptions = null + ) + { + _messageMapperRegistry = messageMapperRegistry; + _transformerFactory = transformerFactory; + + switch (busType) + { + case ExternalBusType.None: + break; + case ExternalBusType.FireAndForget: + _bus = bus; + break; + case ExternalBusType.RPC: + _bus = bus; + _useRequestReplyQueues = true; + _replySubscriptions = subscriptions; + _responseChannelFactory = responseChannelFactory; + break; + default: + throw new ConfigurationException("Bus type not supported"); + } + + return this; + } + /// /// The wants to support or using an external bus. /// You need to provide a policy to specify how QoS issues, specifically or @@ -162,18 +203,24 @@ public INeedMessaging DefaultPolicy() /// /// The Task Queues configuration. /// The Outbox. - /// + /// /// INeedARequestContext. - public INeedARequestContext ExternalBus(ExternalBusConfiguration configuration, IAmAnOutbox outbox, IAmABoxTransactionConnectionProvider boxTransactionConnectionProvider = null) + public INeedARequestContext ExternalBusCreate( + ExternalBusConfiguration configuration, + IAmAnOutbox outbox, + IAmABoxTransactionProvider transactionProvider) { - _useExternalBus = true; - _producers = configuration.ProducerRegistry; - _outbox = outbox; - _overridingBoxTransactionConnectionProvider = boxTransactionConnectionProvider; _messageMapperRegistry = configuration.MessageMapperRegistry; - _outboxWriteTimeout = configuration.OutboxWriteTimeout; - _outboxBulkChunkSize = configuration.OutboxBulkChunkSize; + _responseChannelFactory = configuration.ResponseChannelFactory; _transformerFactory = configuration.TransformerFactory; + + _bus = new ExternalBusServices( + configuration.ProducerRegistry, + _policyRegistry, + outbox, + configuration.OutboxBulkChunkSize, + configuration.OutboxTimeout); + return this; } @@ -186,27 +233,6 @@ public INeedARequestContext NoExternalBus() return this; } - /// - /// The wants to support using RPC between client and server - /// - /// - /// The outbox - /// Subscriptions for creating reply queues - /// - public INeedARequestContext ExternalRPC(ExternalBusConfiguration configuration, IAmAnOutbox outbox, IEnumerable subscriptions) - { - _useRequestReplyQueues = true; - _replySubscriptions = subscriptions; - _producers = configuration.ProducerRegistry; - _messageMapperRegistry = configuration.MessageMapperRegistry; - _outboxWriteTimeout = configuration.OutboxWriteTimeout; - _responseChannelFactory = configuration.ResponseChannelFactory; - _outbox = outbox; - _transformerFactory = configuration.TransformerFactory; - - return this; - } - /// - /// - public interface IAmAnOutboxAsync : IAmAnOutbox where T : Message + /// The type of message + /// The type of transaction supported by the Outbox + public interface IAmAnOutboxAsync : IAmAnOutbox where T : Message { /// /// If false we the default thread synchronization context to run any continuation, if true we re-use the original synchronization context. @@ -53,9 +55,13 @@ public interface IAmAnOutboxAsync : IAmAnOutbox where T : Message /// The message. /// The time allowed for the write in milliseconds; on a -1 default /// Allows the sender to cancel the request pipeline. Optional - /// The Connection Provider to use for this call + /// The Connection Provider to use for this call /// . - Task AddAsync(T message, int outBoxTimeout = -1, CancellationToken cancellationToken = default, IAmABoxTransactionConnectionProvider transactionConnectionProvider = null); + Task AddAsync( + T message, + int outBoxTimeout = -1, + CancellationToken cancellationToken = default, + IAmABoxTransactionProvider transactionProvider = null); /// /// Awaitable Get the specified message identifier. diff --git a/src/Paramore.Brighter/IAmAnOutboxSync.cs b/src/Paramore.Brighter/IAmAnOutboxSync.cs index 1407618b84..32251b2444 100644 --- a/src/Paramore.Brighter/IAmAnOutboxSync.cs +++ b/src/Paramore.Brighter/IAmAnOutboxSync.cs @@ -24,6 +24,7 @@ THE SOFTWARE. */ using System; using System.Collections.Generic; +using System.Data.Common; namespace Paramore.Brighter { @@ -33,16 +34,17 @@ namespace Paramore.Brighter /// store the message into an OutBox to allow later replay of those messages in the event of failure. We automatically copy any posted message into the store /// We provide implementations of for various databases. Users using other databases should consider a Pull Request /// - /// - public interface IAmAnOutboxSync : IAmAnOutbox where T : Message + /// The message type + /// The transaction type of the underlying Db + public interface IAmAnOutboxSync : IAmAnOutbox where T : Message { /// /// Adds the specified message. /// /// The message. /// The time allowed for the write in milliseconds; on a -1 default - /// The Connection Provider to use for this call - void Add(T message, int outBoxTimeout = -1, IAmABoxTransactionConnectionProvider transactionConnectionProvider = null); + /// The Connection Provider to use for this call + void Add(T message, int outBoxTimeout = -1, IAmABoxTransactionProvider transactionProvider = null); /// /// Gets the specified message identifier. diff --git a/src/Paramore.Brighter/InMemoryOutbox.cs b/src/Paramore.Brighter/InMemoryOutbox.cs index dc4a0aaf31..a93a1653f4 100644 --- a/src/Paramore.Brighter/InMemoryOutbox.cs +++ b/src/Paramore.Brighter/InMemoryOutbox.cs @@ -29,11 +29,12 @@ THE SOFTWARE. */ #endregion using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using System.Transactions; +using Paramore.Brighter.Extensions; namespace Paramore.Brighter { @@ -81,7 +82,7 @@ public static string ConvertKey(Guid id) /// so you can use multiple instances safely as well /// #pragma warning disable CS0618 - public class InMemoryOutbox : InMemoryBox, IAmABulkOutboxSync, IAmABulkOutboxAsync + public class InMemoryOutbox : InMemoryBox, IAmABulkOutboxSync, IAmABulkOutboxAsync #pragma warning restore CS0618 { /// @@ -98,8 +99,8 @@ public class InMemoryOutbox : InMemoryBox, IAmABulkOutboxSync /// /// - /// This is not used for the In Memory Outbox. - public void Add(Message message, int outBoxTimeout = -1, IAmABoxTransactionConnectionProvider transactionConnectionProvider = null) + /// This is not used for the In Memory Outbox. + public void Add(Message message, int outBoxTimeout = -1, IAmABoxTransactionProvider transactionProvider = null) { ClearExpiredMessages(); EnforceCapacityLimit(); @@ -119,15 +120,19 @@ public void Add(Message message, int outBoxTimeout = -1, IAmABoxTransactionConne /// /// /// - /// This is not used for the In Memory Outbox. - public void Add(IEnumerable messages, int outBoxTimeout = -1, IAmABoxTransactionConnectionProvider transactionConnectionProvider = null) + /// This is not used for the In Memory Outbox. + public void Add( + IEnumerable messages, + int outBoxTimeout = -1, + IAmABoxTransactionProvider transactionProvider = null + ) { ClearExpiredMessages(); EnforceCapacityLimit(); foreach (Message message in messages) { - Add(message, outBoxTimeout, transactionConnectionProvider); + Add(message, outBoxTimeout, transactionProvider); } } @@ -137,9 +142,13 @@ public void Add(IEnumerable messages, int outBoxTimeout = -1, IAmABoxTr /// /// /// - /// This is not used for the In Memory Outbox. + /// This is not used for the In Memory Outbox. /// - public Task AddAsync(Message message, int outBoxTimeout = -1, CancellationToken cancellationToken = default, IAmABoxTransactionConnectionProvider transactionConnectionProvider = null) + public Task AddAsync( + Message message, + int outBoxTimeout = -1, + CancellationToken cancellationToken = default, + IAmABoxTransactionProvider transactionProvider = null) { var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); @@ -161,9 +170,14 @@ public Task AddAsync(Message message, int outBoxTimeout = -1, CancellationToken /// /// /// - /// This is not used for the In Memory Outbox. + /// This is not used for the In Memory Outbox. /// - public Task AddAsync(IEnumerable messages, int outBoxTimeout = -1, CancellationToken cancellationToken = default, IAmABoxTransactionConnectionProvider transactionConnectionProvider = null) + public Task AddAsync( + IEnumerable messages, + int outBoxTimeout = -1, + CancellationToken cancellationToken = default, + IAmABoxTransactionProvider transactionProvider = null + ) { var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); @@ -264,8 +278,11 @@ public Task GetAsync(Guid messageId, int outBoxTimeout = -1, Cancellati return tcs.Task; } - public Task> GetAsync(IEnumerable messageIds, int outBoxTimeout = -1, - CancellationToken cancellationToken = default) + public Task> GetAsync( + IEnumerable messageIds, + int outBoxTimeout = -1, + CancellationToken cancellationToken = default + ) { var tcs = new TaskCompletionSource>(TaskCreationOptions.RunContinuationsAsynchronously); ClearExpiredMessages(); @@ -278,10 +295,17 @@ public Task> GetAsync(IEnumerable messageIds, int out } /// - /// Mark the message as dispatched - /// - /// The message to mark as dispatched - public Task MarkDispatchedAsync(Guid id, DateTime? dispatchedAt = null, Dictionary args = null, CancellationToken cancellationToken = default) + /// Mark the message as dispatched + /// + /// The message to mark as dispatched + /// The time to mark as the dispatch time + /// A cancellation token for the async operation + public Task MarkDispatchedAsync( + Guid id, + DateTime? dispatchedAt = null, + Dictionary args = null, + CancellationToken cancellationToken = default + ) { var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); @@ -292,24 +316,42 @@ public Task MarkDispatchedAsync(Guid id, DateTime? dispatchedAt = null, Dictiona return tcs.Task; } - public Task MarkDispatchedAsync(IEnumerable ids, DateTime? dispatchedAt = null, Dictionary args = null, - CancellationToken cancellationToken = default) + public Task MarkDispatchedAsync( + IEnumerable ids, + DateTime? dispatchedAt = null, + Dictionary args = null, + CancellationToken cancellationToken = default + ) { - throw new NotImplementedException(); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + ids.Each((id) => MarkDispatched(id, dispatchedAt)); + + tcs.SetResult(new object()); + + return tcs.Task; } - public Task> DispatchedMessagesAsync(double millisecondsDispatchedSince, int pageSize = 100, int pageNumber = 1, - int outboxTimeout = -1, Dictionary args = null, CancellationToken cancellationToken = default) + public Task> DispatchedMessagesAsync( + double millisecondsDispatchedSince, + int pageSize = 100, + int pageNumber = 1, + int outboxTimeout = -1, + Dictionary args = null, + CancellationToken cancellationToken = default + ) { return Task.FromResult(DispatchedMessages(millisecondsDispatchedSince, pageSize, pageNumber, outboxTimeout, args)); } /// - /// Mark the message as dispatched - /// - /// The message to mark as dispatched - public void MarkDispatched(Guid id, DateTime? dispatchedAt = null, Dictionary args = null) + /// Mark the message as dispatched + /// + /// The message to mark as dispatched + /// The time that the message was dispatched + /// Allows passing arbitrary arguments for searching for a message - not used + public void MarkDispatched(Guid id, DateTime? dispatchedAt = null, Dictionary args = null) { ClearExpiredMessages(); @@ -319,12 +361,14 @@ public void MarkDispatched(Guid id, DateTime? dispatchedAt = null, Dictionary - /// Messages still outstanding in the Outbox because their timestamp - /// - /// How many seconds since the message was sent do we wait to declare it outstanding - /// Additional parameters required for search, if any - /// Outstanding Messages + /// + /// Messages still outstanding in the Outbox because their timestamp + /// + /// How many seconds since the message was sent do we wait to declare it outstanding + /// The number of messages to return on a page + /// The page number to return + /// Additional parameters required for search, if any + /// Outstanding Messages public IEnumerable OutstandingMessages(double millSecondsSinceSent, int pageSize = 100, int pageNumber = 1, Dictionary args = null) { @@ -337,6 +381,10 @@ public IEnumerable OutstandingMessages(double millSecondsSinceSent, int return outstandingMessages; } + /// + /// Delete the specified messages from the Outbox + /// + /// The messages to delete public void Delete(params Guid[] messageIds) { foreach (Guid messageId in messageIds) @@ -345,7 +393,19 @@ public void Delete(params Guid[] messageIds) } } - public Task> GetAsync(int pageSize = 100, int pageNumber = 1, Dictionary args = null, CancellationToken cancellationToken = default) + /// + /// Get messages from the Outbox + /// + /// The number of messages to return on each page + /// The page to return + /// Additional parameters used to find messages, if any + /// A cancellation token for the ongoing asynchronous process + /// + public Task> GetAsync( + int pageSize = 100, + int pageNumber = 1, + Dictionary args = null, + CancellationToken cancellationToken = default) { var tcs = new TaskCompletionSource>(TaskCreationOptions.RunContinuationsAsynchronously); @@ -354,7 +414,21 @@ public Task> GetAsync(int pageSize = 100, int pageNumber = 1, Dic return tcs.Task; } - public Task> OutstandingMessagesAsync(double millSecondsSinceSent, int pageSize = 100, int pageNumber = 1, Dictionary args = null, CancellationToken cancellationToken = default) + /// + /// A list of outstanding messages + /// + /// The age of the message in milliseconds + /// The number of messages to return on a page + /// The page to return + /// Additional arguments needed to find a message, if any + /// A cancellation token for the ongoing asynchronous operation + /// + public Task> OutstandingMessagesAsync( + double millSecondsSinceSent, + int pageSize = 100, + int pageNumber = 1, + Dictionary args = null, + CancellationToken cancellationToken = default) { var tcs = new TaskCompletionSource>(TaskCreationOptions.RunContinuationsAsynchronously); @@ -363,6 +437,12 @@ public Task> OutstandingMessagesAsync(double millSecondsSin return tcs.Task; } + /// + /// Deletes the messages from the Outbox + /// + /// A cancellation token for the ongoing asynchronous operation + /// The ids of the messages to delete + /// public Task DeleteAsync(CancellationToken cancellationToken, params Guid[] messageIds) { Delete(messageIds); diff --git a/src/Paramore.Brighter/Message.cs b/src/Paramore.Brighter/Message.cs index e09ea0e4e1..f845e18622 100644 --- a/src/Paramore.Brighter/Message.cs +++ b/src/Paramore.Brighter/Message.cs @@ -35,7 +35,13 @@ namespace Paramore.Brighter public class Message : IEquatable { public const string OriginalMessageIdHeaderName = "x-original-message-id"; + /// + /// Tag name for the delivery tag header + /// public const string DeliveryTagHeaderName = "DeliveryTag"; + /// + /// Tag name for the redelivered header + /// public const string RedeliveredHeaderName = "Redelivered"; /// @@ -50,40 +56,18 @@ public class Message : IEquatable public MessageBody Body { get; set; } /// - /// Gets the identifier. + /// Gets the identifier of the message. /// /// The identifier. public Guid Id { get { return Header.Id; } } - + /// - /// Initializes a new instance of the class. - /// - public Message() - { - Header = new MessageHeader(messageId: Guid.Empty, topic: string.Empty, messageType: MessageType.MT_NONE); - Body = new MessageBody(string.Empty); - } - - /// - /// Initializes a new instance of the class. + /// RMQ: An identifier for the message set by the broker. Only valid on the same thread that consumed the message. /// - /// The header. - /// The body. - [JsonConstructor] - public Message(MessageHeader header, MessageBody body) - { - Header = header; - Body = body; - Header.ContentType = string.IsNullOrEmpty(Header.ContentType) ? Body.ContentType: Header.ContentType; - -#pragma warning disable CS0618 - Header.UpdateTelemetryFromHeaders(); -#pragma warning restore CS0618 - } - + [JsonIgnore] public ulong DeliveryTag { get @@ -96,6 +80,16 @@ public ulong DeliveryTag set { Header.Bag[DeliveryTagHeaderName] = value; } } + /// + /// RMQ: Is the message persistent + /// + [JsonIgnore] + public bool Persist { get; set; } + + /// + /// RMQ: Has this message been redelivered + /// + [JsonIgnore] public bool Redelivered { get @@ -110,12 +104,32 @@ public bool Redelivered set { Header.Bag[RedeliveredHeaderName] = value; } } - public bool Persist { get; set; } + /// + /// Initializes a new instance of the class. + /// + public Message() + { + Header = new MessageHeader(messageId: Guid.Empty, topic: string.Empty, messageType: MessageType.MT_NONE); + Body = new MessageBody(string.Empty); + } - public void UpdateHandledCount() + /// + /// Initializes a new instance of the class. + /// + /// The header. + /// The body. + [JsonConstructor] + public Message(MessageHeader header, MessageBody body) { - Header.UpdateHandledCount(); + Header = header; + Body = body; + Header.ContentType = string.IsNullOrEmpty(Header.ContentType) ? Body.ContentType: Header.ContentType; + +#pragma warning disable CS0618 + Header.UpdateTelemetryFromHeaders(); +#pragma warning restore CS0618 } + public bool HandledCountReached(int requeueCount) { return Header.HandledCount >= requeueCount; @@ -179,7 +193,5 @@ public override int GetHashCode() { return !Equals(left, right); } - - } } diff --git a/src/Paramore.Brighter/MessageBody.cs b/src/Paramore.Brighter/MessageBody.cs index 94186fbb5e..e9e4b3a14f 100644 --- a/src/Paramore.Brighter/MessageBody.cs +++ b/src/Paramore.Brighter/MessageBody.cs @@ -95,6 +95,12 @@ public MessageBody(string body, string contentType = APPLICATION_JSON, Character if (characterEncoding == CharacterEncoding.Raw) throw new ArgumentOutOfRangeException("characterEncoding", "Raw encoding is not supported for string constructor"); + if (body == null) + { + Bytes = Array.Empty(); + return; + } + Bytes = CharacterEncoding switch { CharacterEncoding.Base64 => Convert.FromBase64String(body), @@ -113,9 +119,16 @@ public MessageBody(string body, string contentType = APPLICATION_JSON, Character [JsonConstructor] public MessageBody(byte[] bytes, string contentType = APPLICATION_JSON, CharacterEncoding characterEncoding = CharacterEncoding.UTF8) { - Bytes = bytes; ContentType = contentType; CharacterEncoding = characterEncoding; + + if (bytes == null) + { + Bytes = Array.Empty(); + return; + } + + Bytes = bytes; } /// diff --git a/src/Paramore.Brighter/MessageHeader.cs b/src/Paramore.Brighter/MessageHeader.cs index 5acef5b52c..80035148b7 100644 --- a/src/Paramore.Brighter/MessageHeader.cs +++ b/src/Paramore.Brighter/MessageHeader.cs @@ -175,7 +175,7 @@ public MessageHeader( Id = messageId; Topic = topic; MessageType = messageType; - TimeStamp = RoundToSeconds(DateTime.UtcNow); + TimeStamp = DateTime.UtcNow; HandledCount = 0; DelayedMilliseconds = 0; CorrelationId = correlationId ?? Guid.Empty; @@ -207,7 +207,7 @@ public MessageHeader( string partitionKey = "") : this(messageId, topic, messageType, correlationId, replyTo, contentType, partitionKey) { - TimeStamp = RoundToSeconds(timeStamp); + TimeStamp = timeStamp; } /// @@ -268,17 +268,6 @@ public MessageHeader Copy() return newHeader; } - - //AMQP spec says: - // 4.2.5.4 Timestamps - // Time stamps are held in the 64-bit POSIX time_t format with an - // accuracy of one second. By using 64 bits we avoid future wraparound - // issues associated with 31-bit and 32-bit time_t values. - private DateTime RoundToSeconds(DateTime dateTime) - { - return new DateTime(dateTime.Ticks - (dateTime.Ticks % TimeSpan.TicksPerSecond), dateTime.Kind); - } - /// /// Indicates whether the current object is equal to another object of the same type. /// diff --git a/src/Paramore.Brighter/MessageRecovery.cs b/src/Paramore.Brighter/MessageRecovery.cs index 9eca6e8d6c..e3024ebdb2 100644 --- a/src/Paramore.Brighter/MessageRecovery.cs +++ b/src/Paramore.Brighter/MessageRecovery.cs @@ -34,7 +34,16 @@ namespace Paramore.Brighter /// public class MessageRecoverer : IAmAMessageRecoverer { - public void Repost(List messageIds, IAmAnOutboxSync outBox, IAmAMessageProducerSync messageProducerSync) + /// + /// Repost the messages with these ids + /// + /// The list of Ids to repost + /// An outbox that holds the messages that we want to resend + /// A message producer with which to send via a broker + /// The type of the message + /// The type of transaction supported by the outbox + public void Repost(List messageIds, IAmAnOutboxSync outBox, IAmAMessageProducerSync messageProducerSync) + where T : Message { var foundMessages = GetMessagesFromOutBox(outBox, messageIds); foreach (var foundMessage in foundMessages) @@ -48,8 +57,11 @@ public void Repost(List messageIds, IAmAnOutboxSync outBox, IAm /// /// The store to retrieve from /// The messages to retrieve - /// - private static IEnumerable GetMessagesFromOutBox(IAmAnOutboxSync outBox, IReadOnlyCollection messageIds) + /// The type of the message + /// The type of transaction supported by the outbox + /// The selected messages + private static IEnumerable GetMessagesFromOutBox(IAmAnOutboxSync outBox, IReadOnlyCollection messageIds) + where T : Message { IEnumerable foundMessages = messageIds .Select(messageId => outBox.Get(Guid.Parse(messageId))) diff --git a/src/Paramore.Brighter/Monitoring/Handlers/MonitorHandler.cs b/src/Paramore.Brighter/Monitoring/Handlers/MonitorHandler.cs index a851982a61..0c874e255e 100644 --- a/src/Paramore.Brighter/Monitoring/Handlers/MonitorHandler.cs +++ b/src/Paramore.Brighter/Monitoring/Handlers/MonitorHandler.cs @@ -24,6 +24,7 @@ THE SOFTWARE. */ using System; using System.Text.Json; +using System.Transactions; using Paramore.Brighter.Monitoring.Configuration; using Paramore.Brighter.Monitoring.Events; @@ -31,10 +32,11 @@ namespace Paramore.Brighter.Monitoring.Handlers { /// /// Class MonitorHandler. - /// The MonitorHandler raises an event via the Control Bus when we enter, exit, and if any exceptions are thrown, provided that monitoring has been enabled in the configuration. + /// The MonitorHandler raises an event via the Control Bus when we enter, exit, and if any exceptions are thrown, + /// provided that monitoring has been enabled in the configuration. /// - /// - public class MonitorHandler : RequestHandler where T: class, IRequest + /// + public class MonitorHandler : RequestHandler where TRequest: class, IRequest { readonly IAmAControlBusSender _controlBusSender; private readonly bool _isMonitoringEnabled; @@ -69,7 +71,7 @@ public override void InitializeFromAttributeParams(params object[] initializerLi /// /// The command. /// TRequest. - public override T Handle(T command) + public override TRequest Handle(TRequest command) { if (_isMonitoringEnabled) { @@ -84,7 +86,8 @@ public override T Handle(T command) _handlerFullAssemblyName, JsonSerializer.Serialize(command, JsonSerialisationOptions.Options), timeBeforeHandle, - 0)); + 0) + ); base.Handle(command); diff --git a/src/Paramore.Brighter/Monitoring/Handlers/MonitorHandlerAsync.cs b/src/Paramore.Brighter/Monitoring/Handlers/MonitorHandlerAsync.cs index 0fa58bd759..384c0a8a57 100644 --- a/src/Paramore.Brighter/Monitoring/Handlers/MonitorHandlerAsync.cs +++ b/src/Paramore.Brighter/Monitoring/Handlers/MonitorHandlerAsync.cs @@ -27,6 +27,7 @@ THE SOFTWARE. */ using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using System.Transactions; using Paramore.Brighter.Monitoring.Configuration; using Paramore.Brighter.Monitoring.Events; @@ -87,7 +88,7 @@ await _controlBusSender.PostAsync( JsonSerializer.Serialize(command, JsonSerialisationOptions.Options), timeBeforeHandle, 0), - ContinueOnCapturedContext, cancellationToken) + cancellationToken: cancellationToken) .ConfigureAwait(ContinueOnCapturedContext); } @@ -104,8 +105,8 @@ await _controlBusSender.PostAsync( _handlerFullAssemblyName, JsonSerializer.Serialize(command, JsonSerialisationOptions.Options), timeAfterHandle, - (timeAfterHandle - timeBeforeHandle).Milliseconds), - ContinueOnCapturedContext, cancellationToken) + (timeAfterHandle - timeBeforeHandle).Milliseconds), + cancellationToken: cancellationToken) .ConfigureAwait(ContinueOnCapturedContext); } @@ -133,7 +134,7 @@ await _controlBusSender.PostAsync( timeOnException, (timeOnException - timeBeforeHandle).Milliseconds, capturedException.SourceException), - ContinueOnCapturedContext, cancellationToken) + cancellationToken: cancellationToken) .ConfigureAwait(ContinueOnCapturedContext); } diff --git a/src/Paramore.Brighter/OutboxArchiver.cs b/src/Paramore.Brighter/OutboxArchiver.cs index 61f1ea8c71..8d3fdabdbb 100644 --- a/src/Paramore.Brighter/OutboxArchiver.cs +++ b/src/Paramore.Brighter/OutboxArchiver.cs @@ -8,25 +8,25 @@ namespace Paramore.Brighter { - public class OutboxArchiver + public class OutboxArchiver where TMessage: Message { - private const string ARCHIVEOUTBOX = "Archive Outbox"; + private const string ARCHIVE_OUTBOX = "Archive Outbox"; private readonly int _batchSize; - private IAmAnOutboxSync _outboxSync; - private IAmAnOutboxAsync _outboxAsync; - private IAmAnArchiveProvider _archiveProvider; - private readonly ILogger _logger = ApplicationLogging.CreateLogger(); + private readonly IAmAnOutboxSync _outboxSync; + private readonly IAmAnOutboxAsync _outboxAsync; + private readonly IAmAnArchiveProvider _archiveProvider; + private readonly ILogger _logger = ApplicationLogging.CreateLogger>(); private const string SUCCESS_MESSAGE = "Successfully archiver {NumberOfMessageArchived} out of {MessagesToArchive}, batch size : {BatchSize}"; - public OutboxArchiver(IAmAnOutbox outbox,IAmAnArchiveProvider archiveProvider, int batchSize = 100) + public OutboxArchiver(IAmAnOutbox outbox,IAmAnArchiveProvider archiveProvider, int batchSize = 100) { _batchSize = batchSize; - if (outbox is IAmAnOutboxSync syncBox) + if (outbox is IAmAnOutboxSync syncBox) _outboxSync = syncBox; - if (outbox is IAmAnOutboxAsync asyncBox) + if (outbox is IAmAnOutboxAsync asyncBox) _outboxAsync = asyncBox; _archiveProvider = archiveProvider; @@ -38,7 +38,7 @@ public OutboxArchiver(IAmAnOutbox outbox,IAmAnArchiveProvider archivePr /// Minimum age in hours public void Archive(int minimumAge) { - var activity = ApplicationTelemetry.ActivitySource.StartActivity(ARCHIVEOUTBOX, ActivityKind.Server); + var activity = ApplicationTelemetry.ActivitySource.StartActivity(ARCHIVE_OUTBOX, ActivityKind.Server); try { @@ -61,11 +61,11 @@ public void Archive(int minimumAge) } finally { - if(activity?.DisplayName == ARCHIVEOUTBOX) + if(activity?.DisplayName == ARCHIVE_OUTBOX) activity.Dispose(); } } - + /// /// Archive Message from the outbox to the outbox archive provider /// @@ -74,8 +74,8 @@ public void Archive(int minimumAge) /// Send messages to archive provider in parallel public async Task ArchiveAsync(int minimumAge, CancellationToken cancellationToken, bool parallelArchiving = false) { - var activity = ApplicationTelemetry.ActivitySource.StartActivity(ARCHIVEOUTBOX, ActivityKind.Server); - + var activity = ApplicationTelemetry.ActivitySource.StartActivity(ARCHIVE_OUTBOX, ActivityKind.Server); + try { var messages = await _outboxAsync.DispatchedMessagesAsync(minimumAge, _batchSize, @@ -96,9 +96,8 @@ public async Task ArchiveAsync(int minimumAge, CancellationToken cancellationTok } successfullyArchivedMessages = messages.Select(m => m.Id).ToArray(); } - - await _outboxAsync.DeleteAsync(cancellationToken, successfullyArchivedMessages); - _logger.LogInformation(SUCCESS_MESSAGE, messages.Count(), messages.Count(), _batchSize); + + await _outboxAsync.DeleteAsync(cancellationToken, messages.Select(e => e.Id).ToArray()); } catch (Exception e) { @@ -108,7 +107,7 @@ public async Task ArchiveAsync(int minimumAge, CancellationToken cancellationTok } finally { - if(activity?.DisplayName == ARCHIVEOUTBOX) + if(activity?.DisplayName == ARCHIVE_OUTBOX) activity.Dispose(); } } diff --git a/src/Paramore.Brighter/RelationDatabaseOutbox.cs b/src/Paramore.Brighter/RelationDatabaseOutbox.cs index 666878fa04..b5135e7e70 100644 --- a/src/Paramore.Brighter/RelationDatabaseOutbox.cs +++ b/src/Paramore.Brighter/RelationDatabaseOutbox.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Data; +using System.Data.Common; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -7,16 +9,13 @@ namespace Paramore.Brighter { - - public abstract class - RelationDatabaseOutboxSync : IAmAnOutboxSync, - IAmAnOutboxAsync, IAmABulkOutboxAsync + public abstract class RelationDatabaseOutbox : IAmAnOutboxSync, IAmAnOutboxAsync, IAmABulkOutboxAsync { private readonly IRelationDatabaseOutboxQueries _queries; private readonly ILogger _logger; private readonly string _outboxTableName; - protected RelationDatabaseOutboxSync(string outboxTableName, IRelationDatabaseOutboxQueries queries, ILogger logger) + protected RelationDatabaseOutbox(string outboxTableName, IRelationDatabaseOutboxQueries queries, ILogger logger) { _outboxTableName = outboxTableName; _queries = queries; @@ -39,13 +38,15 @@ protected RelationDatabaseOutboxSync(string outboxTableName, IRelationDatabaseOu /// /// The message. /// - /// Connection Provider to use for this call + /// Connection Provider to use for this call /// Task. - public void Add(Message message, int outBoxTimeout = -1, - IAmABoxTransactionConnectionProvider transactionConnectionProvider = null) + public void Add( + Message message, + int outBoxTimeout = -1, + IAmABoxTransactionProvider transactionProvider = null) { var parameters = InitAddDbParameters(message); - WriteToStore(transactionConnectionProvider, connection => InitAddDbCommand(connection, parameters), () => + WriteToStore(transactionProvider, connection => InitAddDbCommand(connection, parameters), () => { _logger.LogWarning( "MsSqlOutbox: A duplicate Message with the MessageId {Id} was inserted into the Outbox, ignoring and continuing", @@ -58,12 +59,15 @@ public void Add(Message message, int outBoxTimeout = -1, /// /// The message. /// - /// Connection Provider to use for this call + /// Connection Provider to use for this call /// Task. - public void Add(IEnumerable messages, int outBoxTimeout = -1, - IAmABoxTransactionConnectionProvider transactionConnectionProvider = null) + public void Add( + IEnumerable messages, + int outBoxTimeout = -1, + IAmABoxTransactionProvider transactionProvider = null + ) { - WriteToStore(transactionConnectionProvider, + WriteToStore(transactionProvider, connection => InitBulkAddDbCommand(messages.ToList(), connection), () => _logger.LogWarning("MsSqlOutbox: At least one message already exists in the outbox")); } @@ -84,14 +88,17 @@ public void Delete(params Guid[] messageIds) /// The message. /// /// Cancellation Token - /// Connection Provider to use for this call + /// Connection Provider to use for this call /// Task<Message>. - public Task AddAsync(Message message, int outBoxTimeout = -1, + public Task AddAsync( + Message message, + int outBoxTimeout = -1, CancellationToken cancellationToken = default, - IAmABoxTransactionConnectionProvider transactionConnectionProvider = null) + IAmABoxTransactionProvider transactionProvider = null + ) { var parameters = InitAddDbParameters(message); - return WriteToStoreAsync(transactionConnectionProvider, + return WriteToStoreAsync(transactionProvider, connection => InitAddDbCommand(connection, parameters), () => { _logger.LogWarning( @@ -107,13 +114,16 @@ public Task AddAsync(Message message, int outBoxTimeout = -1, /// The message. /// The time allowed for the write in milliseconds; on a -1 default /// Allows the sender to cancel the request pipeline. Optional - /// The Connection Provider to use for this call + /// The Connection Provider to use for this call /// . - public Task AddAsync(IEnumerable messages, int outBoxTimeout = -1, + public Task AddAsync( + IEnumerable messages, + int outBoxTimeout = -1, CancellationToken cancellationToken = default, - IAmABoxTransactionConnectionProvider transactionConnectionProvider = null) + IAmABoxTransactionProvider transactionProvider = null + ) { - return WriteToStoreAsync(transactionConnectionProvider, + return WriteToStoreAsync(transactionProvider, connection => InitBulkAddDbCommand(messages.ToList(), connection), () => _logger.LogWarning("MsSqlOutbox: At least one message already exists in the outbox"), cancellationToken); @@ -160,7 +170,9 @@ public Message Get(Guid messageId, int outBoxTimeout = -1) /// The time allowed for the read in milliseconds; on a -2 default /// Allows the sender to cancel the request pipeline. Optional /// . - public Task GetAsync(Guid messageId, int outBoxTimeout = -1, + public Task GetAsync( + Guid messageId, + int outBoxTimeout = -1, CancellationToken cancellationToken = default) { return ReadFromStoreAsync(connection => InitGetMessageCommand(connection, messageId, outBoxTimeout), @@ -174,8 +186,11 @@ public Task GetAsync(Guid messageId, int outBoxTimeout = -1, /// Cancellation Token. /// The Ids of the messages /// - public Task> GetAsync(IEnumerable messageIds, int outBoxTimeout = -1, - CancellationToken cancellationToken = default) + public Task> GetAsync( + IEnumerable messageIds, + int outBoxTimeout = -1, + CancellationToken cancellationToken = default + ) { return ReadFromStoreAsync( connection => InitGetMessagesCommand(connection, messageIds.ToList(), outBoxTimeout), @@ -341,57 +356,82 @@ public Task> DispatchedMessagesAsync(int hoursDispatchedSin #endregion - protected abstract void WriteToStore(IAmABoxTransactionConnectionProvider transactionConnectionProvider, - Func commandFunc, Action loggingAction); - - protected abstract Task WriteToStoreAsync(IAmABoxTransactionConnectionProvider transactionConnectionProvider, - Func commandFunc, Action loggingAction, CancellationToken cancellationToken); - - protected abstract T ReadFromStore(Func commandFunc, - Func resultFunc); - - protected abstract Task ReadFromStoreAsync(Func commandFunc, - Func> resultFunc, CancellationToken cancellationToken); + protected abstract void WriteToStore( + IAmABoxTransactionProvider transactionProvider, + Func commandFunc, + Action loggingAction + ); + + protected abstract Task WriteToStoreAsync( + IAmABoxTransactionProvider transactionProvider, + Func commandFunc, + Action loggingAction, + CancellationToken cancellationToken + ); + + protected abstract T ReadFromStore( + Func commandFunc, + Func resultFunc + ); + + protected abstract Task ReadFromStoreAsync( + Func commandFunc, + Func> resultFunc, + CancellationToken cancellationToken + ); #region Things that Create Commands - private TCommand CreatePagedDispatchedCommand(TConnection connection, double millisecondsDispatchedSince, - int pageSize, int pageNumber) + private DbCommand CreatePagedDispatchedCommand( + DbConnection connection, + double millisecondsDispatchedSince, + int pageSize, + int pageNumber) => CreateCommand(connection, GenerateSqlText(_queries.PagedDispatchedCommand), 0, CreateSqlParameter("PageNumber", pageNumber), CreateSqlParameter("PageSize", pageSize), CreateSqlParameter("OutstandingSince", -1 * millisecondsDispatchedSince)); - private TCommand CreateDispatchedCommand(TConnection connection, int hoursDispatchedSince, + private DbCommand CreateDispatchedCommand(DbConnection connection, int hoursDispatchedSince, int pageSize) => CreateCommand(connection, GenerateSqlText(_queries.DispatchedCommand), 0, CreateSqlParameter("PageSize", pageSize), CreateSqlParameter("DispatchedSince", -1 * hoursDispatchedSince)); - private TCommand CreatePagedReadCommand(TConnection connection, int pageSize, int pageNumber) + private DbCommand CreatePagedReadCommand( + DbConnection connection, + int pageSize, + int pageNumber + ) => CreateCommand(connection, GenerateSqlText(_queries.PagedReadCommand), 0, CreateSqlParameter("PageNumber", pageNumber), CreateSqlParameter("PageSize", pageSize)); - private TCommand CreatePagedOutstandingCommand(TConnection connection, double milliSecondsSinceAdded, - int pageSize, int pageNumber) + private DbCommand CreatePagedOutstandingCommand( + DbConnection connection, + double milliSecondsSinceAdded, + int pageSize, + int pageNumber) => CreateCommand(connection, GenerateSqlText(_queries.PagedOutstandingCommand), 0, CreatePagedOutstandingParameters(milliSecondsSinceAdded, pageSize, pageNumber)); - private TCommand InitAddDbCommand(TConnection connection, TParameter[] parameters) + private DbCommand InitAddDbCommand( + DbConnection connection, + IDbDataParameter[] parameters + ) => CreateCommand(connection, GenerateSqlText(_queries.AddCommand), 0, parameters); - private TCommand InitBulkAddDbCommand(List messages, TConnection connection) + private DbCommand InitBulkAddDbCommand(List messages, DbConnection connection) { var insertClause = GenerateBulkInsert(messages); return CreateCommand(connection, GenerateSqlText(_queries.BulkAddCommand, insertClause.insertClause), 0, insertClause.parameters); } - private TCommand InitMarkDispatchedCommand(TConnection connection, Guid messageId, DateTime? dispatchedAt) + private DbCommand InitMarkDispatchedCommand(DbConnection connection, Guid messageId, DateTime? dispatchedAt) => CreateCommand(connection, GenerateSqlText(_queries.MarkDispatchedCommand), 0, CreateSqlParameter("MessageId", messageId), CreateSqlParameter("DispatchedAt", dispatchedAt?.ToUniversalTime())); - private TCommand InitMarkDispatchedCommand(TConnection connection, IEnumerable messageIds, + private DbCommand InitMarkDispatchedCommand(DbConnection connection, IEnumerable messageIds, DateTime? dispatchedAt) { var inClause = GenerateInClauseAndAddParameters(messageIds.ToList()); @@ -400,11 +440,11 @@ private TCommand InitMarkDispatchedCommand(TConnection connection, IEnumerable CreateCommand(connection, GenerateSqlText(_queries.GetMessageCommand), outBoxTimeout, CreateSqlParameter("MessageId", messageId)); - private TCommand InitGetMessagesCommand(TConnection connection, List messageIds, int outBoxTimeout = -1) + private DbCommand InitGetMessagesCommand(DbConnection connection, List messageIds, int outBoxTimeout = -1) { var inClause = GenerateInClauseAndAddParameters(messageIds); return CreateCommand(connection, GenerateSqlText(_queries.GetMessagesCommand, inClause.inClause), outBoxTimeout, @@ -414,44 +454,44 @@ private TCommand InitGetMessagesCommand(TConnection connection, List messa private string GenerateSqlText(string sqlFormat, params string[] orderedParams) => string.Format(sqlFormat, orderedParams.Prepend(_outboxTableName).ToArray()); - private TCommand InitDeleteDispatchedCommand(TConnection connection, IEnumerable messageIds) + private DbCommand InitDeleteDispatchedCommand(DbConnection connection, IEnumerable messageIds) { var inClause = GenerateInClauseAndAddParameters(messageIds.ToList()); return CreateCommand(connection, GenerateSqlText(_queries.DeleteMessagesCommand, inClause.inClause), 0, inClause.parameters); } - protected abstract TCommand CreateCommand(TConnection connection, string sqlText, int outBoxTimeout, - params TParameter[] parameters); + protected abstract DbCommand CreateCommand(DbConnection connection, string sqlText, int outBoxTimeout, + params IDbDataParameter[] parameters); #endregion #region Parameters - protected abstract TParameter[] CreatePagedOutstandingParameters(double milliSecondsSinceAdded, + protected abstract IDbDataParameter[] CreatePagedOutstandingParameters(double milliSecondsSinceAdded, int pageSize, int pageNumber); #endregion - protected abstract TParameter CreateSqlParameter(string parameterName, object value); - protected abstract TParameter[] InitAddDbParameters(Message message, int? position = null); + protected abstract IDbDataParameter CreateSqlParameter(string parameterName, object value); + protected abstract IDbDataParameter[] InitAddDbParameters(Message message, int? position = null); - protected abstract Message MapFunction(TDataReader dr); + protected abstract Message MapFunction(DbDataReader dr); - protected abstract Task MapFunctionAsync(TDataReader dr, CancellationToken cancellationToken); + protected abstract Task MapFunctionAsync(DbDataReader dr, CancellationToken cancellationToken); - protected abstract IEnumerable MapListFunction(TDataReader dr); + protected abstract IEnumerable MapListFunction(DbDataReader dr); - protected abstract Task> MapListFunctionAsync(TDataReader dr, + protected abstract Task> MapListFunctionAsync(DbDataReader dr, CancellationToken cancellationToken); - private (string inClause, TParameter[] parameters) GenerateInClauseAndAddParameters(List messageIds) + private (string inClause, IDbDataParameter[] parameters) GenerateInClauseAndAddParameters(List messageIds) { var paramNames = messageIds.Select((s, i) => "@p" + i).ToArray(); - var parameters = new TParameter[messageIds.Count]; + var parameters = new IDbDataParameter[messageIds.Count]; for (int i = 0; i < paramNames.Count(); i++) { parameters[i] = CreateSqlParameter(paramNames[i], messageIds[i]); @@ -460,14 +500,14 @@ protected abstract Task> MapListFunctionAsync(TDataReader d return (string.Join(",", paramNames), parameters); } - private (string insertClause, TParameter[] parameters) GenerateBulkInsert(List messages) + private (string insertClause, IDbDataParameter[] parameters) GenerateBulkInsert(List messages) { var messageParams = new List(); - var parameters = new List(); + var parameters = new List(); for (int i = 0; i < messages.Count(); i++) { - messageParams.Add($"(@p{i}_MessageId, @p{i}_MessageType, @p{i}_Topic, @p{i}_Timestamp, @p{i}_CorrelationId, @p{i}_ReplyTo, @p{i}_ContentType, @p{i}_HeaderBag, @p{i}_Body)"); + messageParams.Add($"(@p{i}_MessageId, @p{i}_MessageType, @p{i}_Topic, @p{i}_Timestamp, @p{i}_CorrelationId, @p{i}_ReplyTo, @p{i}_ContentType, @p{i}_PartitionKey, @p{i}_HeaderBag, @p{i}_Body)"); parameters.AddRange(InitAddDbParameters(messages[i], i)); } diff --git a/src/Paramore.Brighter/RelationalDatabaseConfiguration.cs b/src/Paramore.Brighter/RelationalDatabaseConfiguration.cs new file mode 100644 index 0000000000..392dc9347c --- /dev/null +++ b/src/Paramore.Brighter/RelationalDatabaseConfiguration.cs @@ -0,0 +1,59 @@ +namespace Paramore.Brighter +{ + public class RelationalDatabaseConfiguration : IAmARelationalDatabaseConfiguration + { + private const string OUTBOX_TABLE_NAME = "Outbox"; + private const string INBOX_TABLE_NAME = "Inbox"; + + /// + /// Initializes a new instance of the class. + /// + /// The connection string. + /// Name of the outbox table. + /// Name of the inbox table. + /// Name of the queue store table. + /// Is the message payload binary, or a UTF-8 string, default is false or UTF-8 + public RelationalDatabaseConfiguration( + string connectionString, + string outBoxTableName = null, + string inboxTableName = null, + string queueStoreTable = null, + bool binaryMessagePayload = false + ) + { + OutBoxTableName = outBoxTableName ?? OUTBOX_TABLE_NAME; + InBoxTableName = inboxTableName ?? INBOX_TABLE_NAME; + ConnectionString = connectionString; + QueueStoreTable = queueStoreTable; + BinaryMessagePayload = binaryMessagePayload; + } + + /// + /// Is the message payload binary, or a UTF-8 string. Default is false or UTF-8 + /// + public bool BinaryMessagePayload { get; protected set; } + + /// + /// Gets the connection string. + /// + /// The connection string. + public string ConnectionString { get; protected set; } + + /// + /// Gets the name of the inbox table. + /// + /// The name of the inbox table. + public string InBoxTableName { get; private set; } + + /// + /// Gets the name of the outbox table. + /// + /// The name of the outbox table. + public string OutBoxTableName { get; protected set; } + + /// + /// Gets the name of the queue table. + /// + public string QueueStoreTable { get; protected set; } + } +} diff --git a/src/Paramore.Brighter/RelationalDbConnectionProvider.cs b/src/Paramore.Brighter/RelationalDbConnectionProvider.cs new file mode 100644 index 0000000000..6607c3b5f1 --- /dev/null +++ b/src/Paramore.Brighter/RelationalDbConnectionProvider.cs @@ -0,0 +1,119 @@ +using System; +using System.Data; +using System.Data.Common; +using System.Threading; +using System.Threading.Tasks; + +namespace Paramore.Brighter +{ + public abstract class RelationalDbConnectionProvider : IAmARelationalDbConnectionProvider + { + private bool _disposed = false; + + /// + /// debugging + /// + public Guid Instance = Guid.NewGuid(); + + /// + /// Does not retain shared connections or transactions, so nothing to commit + /// + public virtual void Close() { } + + /// + /// Does not support shared transactions, so nothing to commit, manage transactions independently + /// + public void Commit() { } + + /// + /// Does not support shared transactions, so nothing to commit, manage transactions independently + /// + public Task CommitAsync(CancellationToken cancellationToken) { return Task.CompletedTask; } + + /// + /// Gets a existing Connection; creates a new one if it does not exist + /// Opens the connection if it is not opened + /// This is not a shared connection + /// + /// A database connection + public abstract DbConnection GetConnection(); + + /// + /// Gets a existing Connection; creates a new one if it does not exist + /// The connection is not opened, you need to open it yourself. + /// The base class just returns a new or existing connection, but derived types may perform async i/o + /// + /// A database connection + public virtual Task GetConnectionAsync(CancellationToken cancellationToken = default) + { + var tcs = new TaskCompletionSource(); + tcs.SetResult(GetConnection()); + return tcs.Task; + } + + /// + /// Does not support shared transactions, create a transaction of the DbConnection instead + /// + /// A database transaction + public virtual DbTransaction GetTransaction() { return null; } + + /// + /// Does not support shared transactions, create a transaction of the DbConnection instead + /// + /// A database transaction + public virtual Task GetTransactionAsync(CancellationToken cancellationToken = default) + { + return null; + } + + /// + /// Is there a transaction open? + /// On a connection provider we do not manage so our response is always false + /// + public virtual bool HasOpenTransaction { get => false; } + + /// + /// Is there a shared connection? (Do we maintain state of just create anew) + /// On a connection provider we do not have shared connections so our response is always false + /// + public virtual bool IsSharedConnection { get => false; } + + /// + /// Rolls back a transaction + /// On a connection provider we do not manage transactions so our response is always false + /// + public virtual void Rollback() + { + } + + /// + /// Rolls back a transaction + /// On a connection provider we do not manage transactions so our response is always false + /// + public virtual Task RollbackAsync(CancellationToken cancellationToken = default) + { + return Task.CompletedTask; + } + + ~RelationalDbConnectionProvider() => Dispose(false); + + // Public implementation of Dispose pattern callable by consumers. + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + /* No shared transactions, nothing to do */ + } + _disposed = true; + } + } + } +} diff --git a/src/Paramore.Brighter/RelationalDbTransactionProvider.cs b/src/Paramore.Brighter/RelationalDbTransactionProvider.cs new file mode 100644 index 0000000000..0c3a196db4 --- /dev/null +++ b/src/Paramore.Brighter/RelationalDbTransactionProvider.cs @@ -0,0 +1,168 @@ +using System; +using System.Data; +using System.Data.Common; +using System.Threading; +using System.Threading.Tasks; + +namespace Paramore.Brighter +{ + public abstract class RelationalDbTransactionProvider : IAmATransactionConnectionProvider + { + private bool _disposed = false; + protected DbConnection Connection; + protected DbTransaction Transaction; + + /// + /// Close any open connection or transaction + /// + public virtual void Close() + { + if (!HasOpenTransaction) + { + Transaction?.Dispose(); + Transaction = null; + } + + if (!IsSharedConnection) + Connection?.Close(); + } + + /// + /// Commit the transaction + /// + public virtual void Commit() + { + if (HasOpenTransaction) + { + Transaction.Commit(); + Transaction = null; + } + } + + /// + /// Commit the transaction + /// + /// An awaitable Task + public virtual Task CommitAsync(CancellationToken cancellationToken) + { + if (HasOpenTransaction) + { + Transaction.Commit(); + Transaction = null; + } + + return Task.CompletedTask; + } + + /// + /// Gets a existing Connection; creates a new one if it does not exist + /// Opens the connection if it is not opened + /// + /// A database connection + public abstract DbConnection GetConnection(); + + /// + /// Gets a existing Connection; creates a new one if it does not exist + /// The connection is not opened, you need to open it yourself. + /// The base class just returns a new or existing connection, but derived types may perform async i/o + /// + /// A database connection + public virtual Task GetConnectionAsync(CancellationToken cancellationToken = default) + { + var tcs = new TaskCompletionSource(); + tcs.SetResult(GetConnection()); + return tcs.Task; + } + + /// + /// Gets an existing transaction; creates a new one from the connection if it does not exist. + /// You should use the commit transaction using the Commit method. + /// + /// A database transaction + public virtual DbTransaction GetTransaction() + { + Connection ??= GetConnection(); + if (Connection.State != ConnectionState.Open) + Connection.Open(); + if (!HasOpenTransaction) + Transaction = Connection.BeginTransaction(); + return Transaction; + } + + /// + /// Gets an existing transaction; creates a new one from the connection if it does not exist. + /// You are responsible for committing the transaction. + /// + /// A database transaction + public virtual Task GetTransactionAsync(CancellationToken cancellationToken = default) + { + var tcs = new TaskCompletionSource(); + + if(cancellationToken.IsCancellationRequested) + tcs.SetCanceled(); + + tcs.SetResult(GetTransaction()); + return tcs.Task; + } + + /// + /// Is there a transaction open? + /// + public virtual bool HasOpenTransaction { get { return Transaction != null; } } + + /// + /// Is there a shared connection? (Do we maintain state of just create anew) + /// + public virtual bool IsSharedConnection { get => true; } + + /// + /// Rolls back a transaction + /// + public virtual void Rollback() + { + if (HasOpenTransaction) + { + try { Transaction.Rollback(); } catch(Exception) { /*ignore*/ } + Transaction = null; + } + } + + /// + /// Rolls back a transaction + /// + public virtual Task RollbackAsync(CancellationToken cancellationToken = default) + { + if (HasOpenTransaction) + { + try { Transaction.Rollback(); } catch(Exception e) { /*ignore*/ } + Transaction = null; + } + + return Task.CompletedTask; + } + + ~RelationalDbTransactionProvider() => Dispose(false); + + // Public implementation of Dispose pattern callable by consumers. + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + Connection?.Dispose(); + Transaction?.Dispose(); + } + Connection = null; + Transaction = null; + _disposed = true; + } + } + } +} diff --git a/src/Paramore.Brighter/inboxConfiguration.cs b/src/Paramore.Brighter/inboxConfiguration.cs index 3eac967297..137a163d1c 100644 --- a/src/Paramore.Brighter/inboxConfiguration.cs +++ b/src/Paramore.Brighter/inboxConfiguration.cs @@ -21,14 +21,22 @@ public class InboxConfiguration /// What should we do if exists, defaults to OnceOnlyAction.Throw - let the caller handle /// public OnceOnlyAction ActionOnExists { get; } + /// /// Use the inbox to de-duplicate requests - defaults to true /// public bool OnceOnly { get; } + + /// + /// The Inbox to use - defaults to InMemoryInbox + /// + public IAmAnInbox Inbox { get;} + /// /// The scope of the requests to store in the inbox - the default is everything /// public InboxScope Scope { get; } + /// /// If null, the context to pass to the Inbox will be auto-generated from the handler class name. Otherwise /// override with a function that takes a type name - the target handler, and returns a string, the context key @@ -37,11 +45,14 @@ public class InboxConfiguration public Func Context { get; set; } public InboxConfiguration( + IAmAnInbox inbox = null, InboxScope scope = InboxScope.All, bool onceOnly = true, OnceOnlyAction actionOnExists = OnceOnlyAction.Throw, Func context = null) { + Inbox = inbox ?? new InMemoryInbox(); + Scope = scope; Context = context; OnceOnly = onceOnly; diff --git a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/TestDoubles/FakeMessageProducer.cs b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/TestDoubles/FakeMessageProducer.cs index 39d6f2c964..24e3b6c0a2 100644 --- a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/TestDoubles/FakeMessageProducer.cs +++ b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/TestDoubles/FakeMessageProducer.cs @@ -24,6 +24,7 @@ THE SOFTWARE. */ using System; using System.Collections.Generic; +using System.Linq; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; @@ -51,12 +52,13 @@ public Task SendAsync(Message message) } public async IAsyncEnumerable SendAsync(IEnumerable messages, [EnumeratorCancellation] CancellationToken cancellationToken) { - foreach (var msg in messages) + var msgs = messages as Message[] ?? messages.ToArray(); + foreach (var msg in msgs) { yield return new[] { msg.Id }; } MessageWasSent = true; - SentMessages.AddRange(messages); + SentMessages.AddRange(msgs); } public void Send(Message message) diff --git a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/TestDoubles/FakeOutboxSync.cs b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/TestDoubles/FakeOutbox.cs similarity index 80% rename from tests/Paramore.Brighter.Core.Tests/CommandProcessors/TestDoubles/FakeOutboxSync.cs rename to tests/Paramore.Brighter.Core.Tests/CommandProcessors/TestDoubles/FakeOutbox.cs index b6b99baf15..16f09fd50d 100644 --- a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/TestDoubles/FakeOutboxSync.cs +++ b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/TestDoubles/FakeOutbox.cs @@ -28,21 +28,29 @@ THE SOFTWARE. */ using System.Linq; using System.Threading; using System.Threading.Tasks; +using System.Transactions; namespace Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles { - public class FakeOutboxSync : IAmABulkOutboxSync, IAmABulkOutboxAsync +#pragma warning disable CS0618 + public class FakeOutbox : IAmABulkOutboxSync, IAmABulkOutboxAsync +#pragma warning restore CS0618 { private readonly List _posts = new List(); public bool ContinueOnCapturedContext { get; set; } - public void Add(Message message, int outBoxTimeout = -1, IAmABoxTransactionConnectionProvider transactionConnectionProvider = null) + public void Add(Message message, int outBoxTimeout = -1, IAmABoxTransactionProvider transactionProvider = null) { _posts.Add(new OutboxEntry {Message = message, TimeDeposited = DateTime.UtcNow}); } - public Task AddAsync(Message message, int outBoxTimeout = -1, CancellationToken cancellationToken = default, IAmABoxTransactionConnectionProvider transactionConnectionProvider = null) + public Task AddAsync( + Message message, + int outBoxTimeout = -1, + CancellationToken cancellationToken = default, + IAmABoxTransactionProvider transactionProvider = null + ) { if (cancellationToken.IsCancellationRequested) return Task.FromCanceled(cancellationToken); @@ -111,7 +119,11 @@ public Task> GetAsync(IEnumerable messageIds, int out return tcs.Task; } - public Task MarkDispatchedAsync(Guid id, DateTime? dispatchedAt = null, Dictionary args = null, CancellationToken cancellationToken = default) + public Task MarkDispatchedAsync( + Guid id, + DateTime? dispatchedAt = null, + Dictionary args = null, + CancellationToken cancellationToken = default) { var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); @@ -122,8 +134,11 @@ public Task MarkDispatchedAsync(Guid id, DateTime? dispatchedAt = null, Dictiona return tcs.Task; } - public async Task MarkDispatchedAsync(IEnumerable ids, DateTime? dispatchedAt = null, Dictionary args = null, - CancellationToken cancellationToken = default) + public async Task MarkDispatchedAsync( + IEnumerable ids, DateTime? dispatchedAt = null, + Dictionary args = null, + CancellationToken cancellationToken = default + ) { foreach (var id in ids) { @@ -131,15 +146,23 @@ public async Task MarkDispatchedAsync(IEnumerable ids, DateTime? dispatche } } - public Task> DispatchedMessagesAsync(double millisecondsDispatchedSince, int pageSize = 100, int pageNumber = 1, - int outboxTimeout = -1, Dictionary args = null, CancellationToken cancellationToken = default) + public Task> DispatchedMessagesAsync( + double millisecondsDispatchedSince, + int pageSize = 100, + int pageNumber = 1, + int outboxTimeout = -1, Dictionary args = null, + CancellationToken cancellationToken = default) { return Task.FromResult(DispatchedMessages(millisecondsDispatchedSince, pageSize, pageNumber, outboxTimeout, args)); } - public Task> OutstandingMessagesAsync(double millSecondsSinceSent, int pageSize = 100, int pageNumber = 1, - Dictionary args = null, CancellationToken cancellationToken = default) + public Task> OutstandingMessagesAsync( + double millSecondsSinceSent, + int pageSize = 100, + int pageNumber = 1, + Dictionary args = null, + CancellationToken cancellationToken = default) { return Task.FromResult(OutstandingMessages(millSecondsSinceSent, pageSize, pageNumber, args)); } @@ -202,22 +225,26 @@ class OutboxEntry public Message Message { get; set; } } - public void Add(IEnumerable messages, int outBoxTimeout = -1, - IAmABoxTransactionConnectionProvider transactionConnectionProvider = null) + public void Add( + IEnumerable messages, + int outBoxTimeout = -1, + IAmABoxTransactionProvider transactionProvider = null) { foreach (Message message in messages) { - Add(message,outBoxTimeout, transactionConnectionProvider); + Add(message,outBoxTimeout, transactionProvider); } } - public async Task AddAsync(IEnumerable messages, int outBoxTimeout = -1, + public async Task AddAsync( + IEnumerable messages, + int outBoxTimeout = -1, CancellationToken cancellationToken = default, - IAmABoxTransactionConnectionProvider transactionConnectionProvider = null) + IAmABoxTransactionProvider transactionProvider = null) { foreach (var message in messages) { - await AddAsync(message, outBoxTimeout, cancellationToken, transactionConnectionProvider); + await AddAsync(message, outBoxTimeout, cancellationToken, transactionProvider); } } } diff --git a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Building_A_Pipeline_With_Global_Inbox_And_Use_Inbox.cs b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Building_A_Pipeline_With_Global_Inbox_And_Use_Inbox.cs index e19243421e..8fa1ee4926 100644 --- a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Building_A_Pipeline_With_Global_Inbox_And_Use_Inbox.cs +++ b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Building_A_Pipeline_With_Global_Inbox_And_Use_Inbox.cs @@ -51,8 +51,8 @@ public PipelineGlobalInboxWhenUseInboxTests() [Fact] public void When_Building_A_Pipeline_With_Global_Inbox() { - // Settings for UseInbox on MyCommandInboxedHandler - // [UseInbox(step:0, contextKey: typeof(MyCommandInboxedHandler), onceOnly: false)] + // Settings for InboxConfiguration on MyCommandInboxedHandler + // [InboxConfiguration(step:0, contextKey: typeof(MyCommandInboxedHandler), onceOnly: false)] // Settings for InboxConfiguration as above // _inboxConfiguration = new InboxConfiguration(InboxScope.All, context: true, onceOnly: true); // so global will not allow repeated requests ans calls, but local should override this and allow @@ -64,7 +64,7 @@ public void When_Building_A_Pipeline_With_Global_Inbox() var chain = _chainOfResponsibility.First(); var myCommand = new MyCommand(); - //First pass not impacted by UseInbox Handler + //First pass not impacted by InboxConfiguration Handler chain.Handle(myCommand); bool noException = true; diff --git a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Building_A_Pipeline_With_Global_Inbox_And_Use_Inbox_Async.cs b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Building_A_Pipeline_With_Global_Inbox_And_Use_Inbox_Async.cs index b352689eba..bf98859f9a 100644 --- a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Building_A_Pipeline_With_Global_Inbox_And_Use_Inbox_Async.cs +++ b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Building_A_Pipeline_With_Global_Inbox_And_Use_Inbox_Async.cs @@ -53,8 +53,8 @@ public PipelineGlobalInboxWhenUseInboxAsyncTests() [Fact] public async Task When_Building_A_Pipeline_With_Global_Inbox() { - // Settings for UseInbox on MyCommandInboxedHandler - // [UseInbox(step:0, contextKey: typeof(MyCommandInboxedHandler), onceOnly: false)] + // Settings for InboxConfiguration on MyCommandInboxedHandler + // [InboxConfiguration(step:0, contextKey: typeof(MyCommandInboxedHandler), onceOnly: false)] // Settings for InboxConfiguration as above // _inboxConfiguration = new InboxConfiguration(InboxScope.All, context: true, onceOnly: true); // so global will not allow repeated requests ans calls, but local should override this and allow @@ -66,7 +66,7 @@ public async Task When_Building_A_Pipeline_With_Global_Inbox() var chain = _chainOfResponsibility.First(); var myCommand = new MyCommand(); - //First pass not impacted by UseInbox Handler + //First pass not impacted by InboxConfiguration Handler await chain.HandleAsync(myCommand); bool noException = true; diff --git a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Bulk_Clearing_The_PostBox_On_The_Command_Processor_Async.cs b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Bulk_Clearing_The_PostBox_On_The_Command_Processor_Async.cs index 3dfac18845..69e48e8323 100644 --- a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Bulk_Clearing_The_PostBox_On_The_Command_Processor_Async.cs +++ b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Bulk_Clearing_The_PostBox_On_The_Command_Processor_Async.cs @@ -28,6 +28,7 @@ THE SOFTWARE. */ using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using System.Transactions; using FluentAssertions; using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; using Polly; @@ -43,7 +44,7 @@ public class CommandProcessorPostBoxBulkClearAsyncTests : IDisposable private readonly CommandProcessor _commandProcessor; private readonly Message _message; private readonly Message _message2; - private readonly FakeOutboxSync _fakeOutboxSync; + private readonly FakeOutbox _fakeOutbox; private readonly FakeMessageProducerWithPublishConfirmation _fakeMessageProducerWithPublishConfirmation; public CommandProcessorPostBoxBulkClearAsyncTests() @@ -51,7 +52,7 @@ public CommandProcessorPostBoxBulkClearAsyncTests() var myCommand = new MyCommand{ Value = "Hello World"}; var myCommand2 = new MyCommand { Value = "Hello World 2" }; - _fakeOutboxSync = new FakeOutboxSync(); + _fakeOutbox = new FakeOutbox(); _fakeMessageProducerWithPublishConfirmation = new FakeMessageProducerWithPublishConfirmation(); var topic = "MyCommand"; @@ -78,20 +79,29 @@ public CommandProcessorPostBoxBulkClearAsyncTests() .Handle() .CircuitBreakerAsync(1, TimeSpan.FromMilliseconds(1)); + var policyRegistry = new PolicyRegistry {{CommandProcessor.RETRYPOLICYASYNC, retryPolicy}, {CommandProcessor.CIRCUITBREAKERASYNC, circuitBreakerPolicy}}; + var producerRegistry = new ProducerRegistry(new Dictionary + { + { topic, _fakeMessageProducerWithPublishConfirmation }, + { topic2, _fakeMessageProducerWithPublishConfirmation } + }); + + IAmAnExternalBusService bus = new ExternalBusServices(producerRegistry, policyRegistry, _fakeOutbox); + + CommandProcessor.ClearExtServiceBus(); _commandProcessor = new CommandProcessor( - new InMemoryRequestContextFactory(), - new PolicyRegistry { { CommandProcessor.RETRYPOLICYASYNC, retryPolicy }, { CommandProcessor.CIRCUITBREAKERASYNC, circuitBreakerPolicy } }, + new InMemoryRequestContextFactory(), + policyRegistry, messageMapperRegistry, - _fakeOutboxSync, - new ProducerRegistry(new Dictionary { { topic, _fakeMessageProducerWithPublishConfirmation }, { topic2, _fakeMessageProducerWithPublishConfirmation } })); + bus); } [Fact(Skip = "Erratic due to timing")] public async Task When_Clearing_The_PostBox_On_The_Command_Processor_Async() { - await _fakeOutboxSync.AddAsync(_message); - await _fakeOutboxSync.AddAsync(_message2); + await _fakeOutbox.AddAsync(_message); + await _fakeOutbox.AddAsync(_message2); _commandProcessor.ClearAsyncOutbox(2, 1, true); diff --git a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Calling_A_Server_Via_The_Command_Processor.cs b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Calling_A_Server_Via_The_Command_Processor.cs index b5b49b6dcf..a58d904612 100644 --- a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Calling_A_Server_Via_The_Command_Processor.cs +++ b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Calling_A_Server_Via_The_Command_Processor.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Text.Json; +using System.Transactions; using FluentAssertions; using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; using Paramore.Brighter.ServiceActivator.TestHelpers; @@ -69,17 +70,30 @@ public CommandProcessorCallTests() new Subscription() }; + var policyRegistry = new PolicyRegistry + { + { CommandProcessor.RETRYPOLICY, retryPolicy }, + { CommandProcessor.CIRCUITBREAKER, circuitBreakerPolicy } + }; + var producerRegistry = + new ProducerRegistry(new Dictionary + { + { topic, _fakeMessageProducerWithPublishConfirmation }, + }); + + IAmAnExternalBusService bus = new ExternalBusServices(producerRegistry, policyRegistry, new InMemoryOutbox()); + + CommandProcessor.ClearExtServiceBus(); _commandProcessor = new CommandProcessor( subscriberRegistry, handlerFactory, - new InMemoryRequestContextFactory(), - new PolicyRegistry { { CommandProcessor.RETRYPOLICY, retryPolicy }, { CommandProcessor.CIRCUITBREAKER, circuitBreakerPolicy } }, + new InMemoryRequestContextFactory(), + policyRegistry, messageMapperRegistry, - new InMemoryOutbox(), - new ProducerRegistry(new Dictionary {{topic, _fakeMessageProducerWithPublishConfirmation},}), - replySubs, + bus, + replySubscriptions:replySubs, responseChannelFactory: inMemoryChannelFactory); - + PipelineBuilder.ClearPipelineCache(); } diff --git a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Calling_A_Server_Via_The_Command_Processor_With_No_In_Mapper.cs b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Calling_A_Server_Via_The_Command_Processor_With_No_In_Mapper.cs index 0e121f7857..ccec9b94d0 100644 --- a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Calling_A_Server_Via_The_Command_Processor_With_No_In_Mapper.cs +++ b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Calling_A_Server_Via_The_Command_Processor_With_No_In_Mapper.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Transactions; using FluentAssertions; using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; using Paramore.Brighter.Core.Tests.TestHelpers; @@ -44,20 +45,30 @@ public CommandProcessorNoInMapperTests() var replySubscriptions = new List(); + var producerRegistry = new ProducerRegistry(new Dictionary + { + { "MyRequest", new FakeMessageProducerWithPublishConfirmation() }, + }); + + var policyRegistry = new PolicyRegistry + { + { CommandProcessor.RETRYPOLICY, retryPolicy }, + { CommandProcessor.CIRCUITBREAKER, circuitBreakerPolicy } + }; + + IAmAnExternalBusService bus = new ExternalBusServices(producerRegistry, policyRegistry, new InMemoryOutbox()); + + CommandProcessor.ClearExtServiceBus(); _commandProcessor = new CommandProcessor( subscriberRegistry, handlerFactory, - new InMemoryRequestContextFactory(), - new PolicyRegistry - { - {CommandProcessor.RETRYPOLICY, retryPolicy}, - {CommandProcessor.CIRCUITBREAKER, circuitBreakerPolicy} - }, + new InMemoryRequestContextFactory(), + policyRegistry, messageMapperRegistry, - new InMemoryOutbox(), - new ProducerRegistry(new Dictionary {{"MyRequest", new FakeMessageProducerWithPublishConfirmation()},}), - replySubscriptions, - responseChannelFactory: new InMemoryChannelFactory()); + bus, + replySubscriptions:replySubscriptions, + responseChannelFactory: new InMemoryChannelFactory() + ); PipelineBuilder.ClearPipelineCache(); } diff --git a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Calling_A_Server_Via_The_Command_Processor_With_No_Out_Mapper.cs b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Calling_A_Server_Via_The_Command_Processor_With_No_Out_Mapper.cs index 441c63cb25..28468c7316 100644 --- a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Calling_A_Server_Via_The_Command_Processor_With_No_Out_Mapper.cs +++ b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Calling_A_Server_Via_The_Command_Processor_With_No_Out_Mapper.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Transactions; using FluentAssertions; using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; using Paramore.Brighter.Core.Tests.TestHelpers; @@ -47,21 +48,30 @@ public CommandProcessorMissingOutMapperTests() new Subscription() }; + var policyRegistry = new PolicyRegistry + { + { CommandProcessor.RETRYPOLICY, retryPolicy }, + { CommandProcessor.CIRCUITBREAKER, circuitBreakerPolicy } + }; + + var producerRegistry = new ProducerRegistry(new Dictionary + { + { "MyRequest", new FakeMessageProducerWithPublishConfirmation() }, + }); + + IAmAnExternalBusService bus = new ExternalBusServices(producerRegistry, policyRegistry, new InMemoryOutbox()); + + CommandProcessor.ClearExtServiceBus(); _commandProcessor = new CommandProcessor( subscriberRegistry, handlerFactory, - new InMemoryRequestContextFactory(), - new PolicyRegistry - { - {CommandProcessor.RETRYPOLICY, retryPolicy}, - {CommandProcessor.CIRCUITBREAKER, circuitBreakerPolicy} - }, + new InMemoryRequestContextFactory(), + policyRegistry, messageMapperRegistry, - new InMemoryOutbox(), - new ProducerRegistry(new Dictionary {{"MyRequest", new FakeMessageProducerWithPublishConfirmation()},}), - replySubs, + bus, + replySubscriptions:replySubs, responseChannelFactory: new InMemoryChannelFactory()); - + PipelineBuilder.ClearPipelineCache(); } diff --git a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Calling_A_Server_Via_The_Command_Processor_With_No_Timeout.cs b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Calling_A_Server_Via_The_Command_Processor_With_No_Timeout.cs index e12fc8d459..7d20d2bbf7 100644 --- a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Calling_A_Server_Via_The_Command_Processor_With_No_Timeout.cs +++ b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Calling_A_Server_Via_The_Command_Processor_With_No_Timeout.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Transactions; using FluentAssertions; using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; using Paramore.Brighter.Core.Tests.TestHelpers; @@ -49,22 +50,31 @@ public CommandProcessorCallTestsNoTimeout() { new Subscription() }; + + var policyRegistry = new PolicyRegistry() + { + {CommandProcessor.RETRYPOLICY, retryPolicy}, + {CommandProcessor.CIRCUITBREAKER, circuitBreakerPolicy} + }; + + var producerRegistry = new ProducerRegistry(new Dictionary + { + { "MyRequest", new FakeMessageProducerWithPublishConfirmation() } + }); + + IAmAnExternalBusService bus = new ExternalBusServices(producerRegistry, policyRegistry, new InMemoryOutbox()); + CommandProcessor.ClearExtServiceBus(); _commandProcessor = new CommandProcessor( subscriberRegistry, handlerFactory, new InMemoryRequestContextFactory(), - new PolicyRegistry - { - {CommandProcessor.RETRYPOLICY, retryPolicy}, - {CommandProcessor.CIRCUITBREAKER, circuitBreakerPolicy} - }, + policyRegistry, messageMapperRegistry, - new InMemoryOutbox(), - new ProducerRegistry(new Dictionary {{"MyRequest", new FakeMessageProducerWithPublishConfirmation()},}), - replySubs, + bus, + replySubscriptions:replySubs, responseChannelFactory: new InMemoryChannelFactory()); - + PipelineBuilder.ClearPipelineCache(); } diff --git a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Clearing_The_PostBox_On_The_Command_Processor _Async.cs b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Clearing_The_PostBox_On_The_Command_Processor _Async.cs index 1d09f64f0f..0cd5dd4193 100644 --- a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Clearing_The_PostBox_On_The_Command_Processor _Async.cs +++ b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Clearing_The_PostBox_On_The_Command_Processor _Async.cs @@ -27,6 +27,7 @@ THE SOFTWARE. */ using System.Linq; using System.Text.Json; using System.Threading.Tasks; +using System.Transactions; using FluentAssertions; using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; using Polly; @@ -40,14 +41,14 @@ public class CommandProcessorPostBoxClearAsyncTests : IDisposable { private readonly CommandProcessor _commandProcessor; private readonly Message _message; - private readonly FakeOutboxSync _fakeOutboxSync; + private readonly FakeOutbox _fakeOutbox; private readonly FakeMessageProducerWithPublishConfirmation _fakeMessageProducerWithPublishConfirmation; public CommandProcessorPostBoxClearAsyncTests() { var myCommand = new MyCommand{ Value = "Hello World"}; - _fakeOutboxSync = new FakeOutboxSync(); + _fakeOutbox = new FakeOutbox(); _fakeMessageProducerWithPublishConfirmation = new FakeMessageProducerWithPublishConfirmation(); const string topic = "MyCommand"; @@ -67,18 +68,35 @@ public CommandProcessorPostBoxClearAsyncTests() .Handle() .CircuitBreakerAsync(1, TimeSpan.FromMilliseconds(1)); + var producerRegistry = + new ProducerRegistry(new Dictionary + { + { topic, _fakeMessageProducerWithPublishConfirmation }, + }); + + var policyRegistry = new PolicyRegistry + { + { CommandProcessor.RETRYPOLICYASYNC, retryPolicy }, + { CommandProcessor.CIRCUITBREAKERASYNC, circuitBreakerPolicy } + }; + + IAmAnExternalBusService bus = new ExternalBusServices(producerRegistry, policyRegistry, _fakeOutbox); + + CommandProcessor.ClearExtServiceBus(); _commandProcessor = new CommandProcessor( - new InMemoryRequestContextFactory(), - new PolicyRegistry { { CommandProcessor.RETRYPOLICYASYNC, retryPolicy }, { CommandProcessor.CIRCUITBREAKERASYNC, circuitBreakerPolicy } }, + new InMemoryRequestContextFactory(), + policyRegistry, messageMapperRegistry, - _fakeOutboxSync, - new ProducerRegistry(new Dictionary {{topic, _fakeMessageProducerWithPublishConfirmation},})); + bus + ); + + PipelineBuilder.ClearPipelineCache(); } [Fact] public async Task When_Clearing_The_PostBox_On_The_Command_Processor_Async() { - await _fakeOutboxSync.AddAsync(_message); + await _fakeOutbox.AddAsync(_message); await _commandProcessor.ClearOutboxAsync(new []{_message.Id}); diff --git a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Clearing_The_PostBox_On_The_Command_Processor.cs b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Clearing_The_PostBox_On_The_Command_Processor.cs index e1d4d53207..b1c517e85a 100644 --- a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Clearing_The_PostBox_On_The_Command_Processor.cs +++ b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Clearing_The_PostBox_On_The_Command_Processor.cs @@ -26,6 +26,7 @@ THE SOFTWARE. */ using System.Collections.Generic; using System.Linq; using System.Text.Json; +using System.Transactions; using FluentAssertions; using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; using Polly; @@ -39,14 +40,14 @@ public class CommandProcessorPostBoxClearTests : IDisposable { private readonly CommandProcessor _commandProcessor; private readonly Message _message; - private readonly FakeOutboxSync _fakeOutbox; + private readonly FakeOutbox _fakeOutbox; private readonly FakeMessageProducerWithPublishConfirmation _fakeMessageProducerWithPublishConfirmation; public CommandProcessorPostBoxClearTests() { var myCommand = new MyCommand{ Value = "Hello World"}; - _fakeOutbox = new FakeOutboxSync(); + _fakeOutbox = new FakeOutbox(); _fakeMessageProducerWithPublishConfirmation = new FakeMessageProducerWithPublishConfirmation(); var topic = "MyCommand"; @@ -66,12 +67,27 @@ public CommandProcessorPostBoxClearTests() .Handle() .CircuitBreaker(1, TimeSpan.FromMilliseconds(1)); + var producerRegistry = + new ProducerRegistry(new Dictionary + { + { topic, _fakeMessageProducerWithPublishConfirmation }, + }); + + var policyRegistry = new PolicyRegistry + { + { CommandProcessor.RETRYPOLICY, retryPolicy }, + { CommandProcessor.CIRCUITBREAKER, circuitBreakerPolicy } + }; + + IAmAnExternalBusService bus = new ExternalBusServices(producerRegistry, policyRegistry, _fakeOutbox); + + CommandProcessor.ClearExtServiceBus(); _commandProcessor = new CommandProcessor( - new InMemoryRequestContextFactory(), - new PolicyRegistry { { CommandProcessor.RETRYPOLICY, retryPolicy }, { CommandProcessor.CIRCUITBREAKER, circuitBreakerPolicy } }, + new InMemoryRequestContextFactory(), + policyRegistry, messageMapperRegistry, - _fakeOutbox, - new ProducerRegistry(new Dictionary {{topic, _fakeMessageProducerWithPublishConfirmation},})); + bus + ); } [Fact] diff --git a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Depositing_A_Message_In_The_Message_Store.cs b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Depositing_A_Message_In_The_Message_Store.cs index c7ca3377a6..3536df608a 100644 --- a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Depositing_A_Message_In_The_Message_Store.cs +++ b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Depositing_A_Message_In_The_Message_Store.cs @@ -4,6 +4,7 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using System.Transactions; using FluentAssertions; using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; using Polly; @@ -19,14 +20,14 @@ public class CommandProcessorDepositPostTests : IDisposable private readonly CommandProcessor _commandProcessor; private readonly MyCommand _myCommand = new MyCommand(); private readonly Message _message; - private readonly FakeOutboxSync _fakeOutbox; + private readonly FakeOutbox _fakeOutbox; private readonly FakeMessageProducerWithPublishConfirmation _fakeMessageProducerWithPublishConfirmation; public CommandProcessorDepositPostTests() { _myCommand.Value = "Hello World"; - _fakeOutbox = new FakeOutboxSync(); + _fakeOutbox = new FakeOutbox(); _fakeMessageProducerWithPublishConfirmation = new FakeMessageProducerWithPublishConfirmation(); const string topic = "MyCommand"; @@ -46,12 +47,27 @@ public CommandProcessorDepositPostTests() .Handle() .CircuitBreaker(1, TimeSpan.FromMilliseconds(1)); + var producerRegistry = + new ProducerRegistry(new Dictionary + { + { topic, _fakeMessageProducerWithPublishConfirmation }, + }); + + var policyRegistry = new PolicyRegistry + { + { CommandProcessor.RETRYPOLICY, retryPolicy }, + { CommandProcessor.CIRCUITBREAKER, circuitBreakerPolicy } + }; + + IAmAnExternalBusService bus = new ExternalBusServices(producerRegistry, policyRegistry, _fakeOutbox); + + CommandProcessor.ClearExtServiceBus(); _commandProcessor = new CommandProcessor( - new InMemoryRequestContextFactory(), - new PolicyRegistry { { CommandProcessor.RETRYPOLICY, retryPolicy }, { CommandProcessor.CIRCUITBREAKER, circuitBreakerPolicy } }, + new InMemoryRequestContextFactory(), + policyRegistry, messageMapperRegistry, - _fakeOutbox, - new ProducerRegistry(new Dictionary {{topic, _fakeMessageProducerWithPublishConfirmation},})); + bus + ); } diff --git a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Depositing_A_Message_In_The_Message_StoreAsync.cs b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Depositing_A_Message_In_The_Message_StoreAsync.cs index dccd5b02dd..901823462f 100644 --- a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Depositing_A_Message_In_The_Message_StoreAsync.cs +++ b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Depositing_A_Message_In_The_Message_StoreAsync.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Text.Json; using System.Threading.Tasks; +using System.Transactions; using FluentAssertions; using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; using Polly; @@ -19,14 +20,14 @@ public class CommandProcessorDepositPostTestsAsync: IDisposable private readonly CommandProcessor _commandProcessor; private readonly MyCommand _myCommand = new MyCommand(); private readonly Message _message; - private readonly FakeOutboxSync _fakeOutboxSync; + private readonly FakeOutbox _fakeOutbox; private readonly FakeMessageProducerWithPublishConfirmation _fakeMessageProducerWithPublishConfirmation; public CommandProcessorDepositPostTestsAsync() { _myCommand.Value = "Hello World"; - _fakeOutboxSync = new FakeOutboxSync(); + _fakeOutbox = new FakeOutbox(); _fakeMessageProducerWithPublishConfirmation = new FakeMessageProducerWithPublishConfirmation(); var topic = "MyCommand"; @@ -46,16 +47,28 @@ public CommandProcessorDepositPostTestsAsync() .Handle() .CircuitBreakerAsync(1, TimeSpan.FromMilliseconds(1)); - PolicyRegistry policyRegistry = new PolicyRegistry { { CommandProcessor.RETRYPOLICYASYNC, retryPolicy }, { CommandProcessor.CIRCUITBREAKERASYNC, circuitBreakerPolicy } }; + PolicyRegistry policyRegistry = new PolicyRegistry + { + { CommandProcessor.RETRYPOLICYASYNC, retryPolicy }, + { CommandProcessor.CIRCUITBREAKERASYNC, circuitBreakerPolicy } + }; + + var producerRegistry = new ProducerRegistry(new Dictionary + { + { topic, _fakeMessageProducerWithPublishConfirmation }, + }); + + IAmAnExternalBusService bus = new ExternalBusServices(producerRegistry, policyRegistry, _fakeOutbox); + + CommandProcessor.ClearExtServiceBus(); _commandProcessor = new CommandProcessor( - new InMemoryRequestContextFactory(), + new InMemoryRequestContextFactory(), policyRegistry, messageMapperRegistry, - _fakeOutboxSync, - new ProducerRegistry(new Dictionary {{topic, _fakeMessageProducerWithPublishConfirmation},})); + bus + ); } - [Fact] public async Task When_depositing_a_message_in_the_outbox() { @@ -67,7 +80,7 @@ public async Task When_depositing_a_message_in_the_outbox() _fakeMessageProducerWithPublishConfirmation.MessageWasSent.Should().BeFalse(); //message should be in the store - var depositedPost = _fakeOutboxSync + var depositedPost = _fakeOutbox .OutstandingMessages(0) .SingleOrDefault(msg => msg.Id == _message.Id); @@ -80,7 +93,7 @@ public async Task When_depositing_a_message_in_the_outbox() depositedPost.Header.MessageType.Should().Be(_message.Header.MessageType); //message should be marked as outstanding if not sent - var outstandingMessages = await _fakeOutboxSync.OutstandingMessagesAsync(0); + var outstandingMessages = await _fakeOutbox.OutstandingMessagesAsync(0); var outstandingMessage = outstandingMessages.Single(); outstandingMessage.Id.Should().Be(_message.Id); } diff --git a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Depositing_A_Message_In_The_Message_StoreAsync_Bulk.cs b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Depositing_A_Message_In_The_Message_StoreAsync_Bulk.cs index 7cd8575a17..2afa8af506 100644 --- a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Depositing_A_Message_In_The_Message_StoreAsync_Bulk.cs +++ b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Depositing_A_Message_In_The_Message_StoreAsync_Bulk.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Text.Json; using System.Threading.Tasks; +using System.Transactions; using FluentAssertions; using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; using Paramore.Brighter.Core.Tests.MessageDispatch.TestDoubles; @@ -24,7 +25,7 @@ public class CommandProcessorBulkDepositPostTestsAsync: IDisposable private readonly Message _message; private readonly Message _message2; private readonly Message _message3; - private readonly FakeOutboxSync _fakeOutboxSync; + private readonly FakeOutbox _fakeOutbox; private readonly FakeMessageProducerWithPublishConfirmation _fakeMessageProducerWithPublishConfirmation; public CommandProcessorBulkDepositPostTestsAsync() @@ -32,7 +33,7 @@ public CommandProcessorBulkDepositPostTestsAsync() _myCommand.Value = "Hello World"; _myCommand2.Value = "Update World"; - _fakeOutboxSync = new FakeOutboxSync(); + _fakeOutbox = new FakeOutbox(); _fakeMessageProducerWithPublishConfirmation = new FakeMessageProducerWithPublishConfirmation(); var topic = "MyCommand"; @@ -72,13 +73,27 @@ public CommandProcessorBulkDepositPostTestsAsync() .Handle() .CircuitBreakerAsync(1, TimeSpan.FromMilliseconds(1)); - PolicyRegistry policyRegistry = new PolicyRegistry { { CommandProcessor.RETRYPOLICYASYNC, retryPolicy }, { CommandProcessor.CIRCUITBREAKERASYNC, circuitBreakerPolicy } }; + PolicyRegistry policyRegistry = new PolicyRegistry + { + { CommandProcessor.RETRYPOLICYASYNC, retryPolicy }, + { CommandProcessor.CIRCUITBREAKERASYNC, circuitBreakerPolicy } + }; + + var producerRegistry = + new ProducerRegistry(new Dictionary + { + { topic, _fakeMessageProducerWithPublishConfirmation }, + }); + + IAmAnExternalBusService bus = new ExternalBusServices(producerRegistry, policyRegistry, _fakeOutbox); + + CommandProcessor.ClearExtServiceBus(); _commandProcessor = new CommandProcessor( - new InMemoryRequestContextFactory(), + new InMemoryRequestContextFactory(), policyRegistry, messageMapperRegistry, - _fakeOutboxSync, - new ProducerRegistry(new Dictionary {{topic, _fakeMessageProducerWithPublishConfirmation},})); + bus + ); } @@ -87,24 +102,24 @@ public async Task When_depositing_a_message_in_the_outbox() { //act var requests = new List {_myCommand, _myCommand2, _myEvent } ; - var postedMessageIds = await _commandProcessor.DepositPostAsync(requests); + await _commandProcessor.DepositPostAsync(requests); //assert //message should not be posted _fakeMessageProducerWithPublishConfirmation.MessageWasSent.Should().BeFalse(); //message should be in the store - var depositedPost = _fakeOutboxSync + var depositedPost = _fakeOutbox .OutstandingMessages(0) .SingleOrDefault(msg => msg.Id == _message.Id); //message should be in the store - var depositedPost2 = _fakeOutboxSync + var depositedPost2 = _fakeOutbox .OutstandingMessages(0) .SingleOrDefault(msg => msg.Id == _message2.Id); //message should be in the store - var depositedPost3 = _fakeOutboxSync + var depositedPost3 = _fakeOutbox .OutstandingMessages(0) .SingleOrDefault(msg => msg.Id == _message3.Id); diff --git a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Depositing_A_Message_In_The_Message_Store_Bulk.cs b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Depositing_A_Message_In_The_Message_Store_Bulk.cs index 49a7e11df0..f65e0ad837 100644 --- a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Depositing_A_Message_In_The_Message_Store_Bulk.cs +++ b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Depositing_A_Message_In_The_Message_Store_Bulk.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Text.Json; +using System.Transactions; using FluentAssertions; using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; using Paramore.Brighter.Core.Tests.MessageDispatch.TestDoubles; @@ -22,14 +23,14 @@ public class CommandProcessorBulkDepositPostTests : IDisposable private readonly Message _message; private readonly Message _message2; private readonly Message _message3; - private readonly FakeOutboxSync _fakeOutbox; + private readonly FakeOutbox _fakeOutbox; private readonly FakeMessageProducerWithPublishConfirmation _fakeMessageProducerWithPublishConfirmation; public CommandProcessorBulkDepositPostTests() { _myCommand.Value = "Hello World"; - _fakeOutbox = new FakeOutboxSync(); + _fakeOutbox = new FakeOutbox(); _fakeMessageProducerWithPublishConfirmation = new FakeMessageProducerWithPublishConfirmation(); const string topic = "MyCommand"; @@ -68,13 +69,26 @@ public CommandProcessorBulkDepositPostTests() var circuitBreakerPolicy = Policy .Handle() .CircuitBreaker(1, TimeSpan.FromMilliseconds(1)); + + var producerRegistry = new ProducerRegistry(new Dictionary + { + { topic, _fakeMessageProducerWithPublishConfirmation }, + }); + + var policyRegistry = new PolicyRegistry + { + { CommandProcessor.RETRYPOLICY, retryPolicy }, + { CommandProcessor.CIRCUITBREAKER, circuitBreakerPolicy } + }; + + IAmAnExternalBusService bus = new ExternalBusServices(producerRegistry, policyRegistry, _fakeOutbox); + CommandProcessor.ClearExtServiceBus(); _commandProcessor = new CommandProcessor( new InMemoryRequestContextFactory(), - new PolicyRegistry { { CommandProcessor.RETRYPOLICY, retryPolicy }, { CommandProcessor.CIRCUITBREAKER, circuitBreakerPolicy } }, + policyRegistry, messageMapperRegistry, - _fakeOutbox, - new ProducerRegistry(new Dictionary {{topic, _fakeMessageProducerWithPublishConfirmation},})); + bus); } diff --git a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Implicit_Clearing_The_PostBox_On_The_Command_Processor.cs b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Implicit_Clearing_The_PostBox_On_The_Command_Processor.cs index 3e604701fb..759f68eb8f 100644 --- a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Implicit_Clearing_The_PostBox_On_The_Command_Processor.cs +++ b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Implicit_Clearing_The_PostBox_On_The_Command_Processor.cs @@ -27,6 +27,7 @@ THE SOFTWARE. */ using System.Linq; using System.Text.Json; using System.Threading; +using System.Transactions; using FluentAssertions; using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; using Polly; @@ -41,14 +42,14 @@ public class CommandProcessorPostBoxImplicitClearTests : IDisposable private readonly CommandProcessor _commandProcessor; private readonly Message _message; private readonly Message _message2; - private readonly FakeOutboxSync _fakeOutbox; + private readonly FakeOutbox _fakeOutbox; private readonly FakeMessageProducer _fakeMessageProducer; public CommandProcessorPostBoxImplicitClearTests() { var myCommand = new MyCommand{ Value = "Hello World"}; - _fakeOutbox = new FakeOutboxSync(); + _fakeOutbox = new FakeOutbox(); _fakeMessageProducer = new FakeMessageProducer(); var topic = "MyCommand"; @@ -73,12 +74,26 @@ public CommandProcessorPostBoxImplicitClearTests() .Handle() .CircuitBreaker(1, TimeSpan.FromMilliseconds(1)); + var policyRegistry = new PolicyRegistry + { + { CommandProcessor.RETRYPOLICY, retryPolicy }, + { CommandProcessor.CIRCUITBREAKER, circuitBreakerPolicy } + }; + + var producerRegistry = new ProducerRegistry(new Dictionary + { + { topic, _fakeMessageProducer }, + }); + + IAmAnExternalBusService bus = new ExternalBusServices(producerRegistry, policyRegistry, _fakeOutbox); + + CommandProcessor.ClearExtServiceBus(); _commandProcessor = new CommandProcessor( - new InMemoryRequestContextFactory(), - new PolicyRegistry { { CommandProcessor.RETRYPOLICY, retryPolicy }, { CommandProcessor.CIRCUITBREAKER, circuitBreakerPolicy } }, + new InMemoryRequestContextFactory(), + policyRegistry, messageMapperRegistry, - _fakeOutbox, - new ProducerRegistry(new Dictionary {{topic, _fakeMessageProducer},})); + bus + ); } [Fact] diff --git a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Implicit_Clearing_The_PostBox_On_The_Command_Processor_Async.cs b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Implicit_Clearing_The_PostBox_On_The_Command_Processor_Async.cs index 327e759e90..5eca6bab12 100644 --- a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Implicit_Clearing_The_PostBox_On_The_Command_Processor_Async.cs +++ b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Implicit_Clearing_The_PostBox_On_The_Command_Processor_Async.cs @@ -28,6 +28,7 @@ THE SOFTWARE. */ using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using System.Transactions; using FluentAssertions; using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; using Polly; @@ -42,14 +43,14 @@ public class CommandProcessorPostBoxImplicitClearAsyncTests : IDisposable private readonly CommandProcessor _commandProcessor; private readonly Message _message; private readonly Message _message2; - private readonly FakeOutboxSync _fakeOutboxSync; + private readonly FakeOutbox _fakeOutbox; private readonly FakeMessageProducer _fakeMessageProducer; public CommandProcessorPostBoxImplicitClearAsyncTests() { var myCommand = new MyCommand{ Value = "Hello World"}; - _fakeOutboxSync = new FakeOutboxSync(); + _fakeOutbox = new FakeOutbox(); _fakeMessageProducer = new FakeMessageProducer(); const string topic = "MyCommand"; @@ -74,19 +75,32 @@ public CommandProcessorPostBoxImplicitClearAsyncTests() .Handle() .CircuitBreakerAsync(1, TimeSpan.FromMilliseconds(1)); + var producerRegistry = new ProducerRegistry(new Dictionary + { + { topic, _fakeMessageProducer }, + }); + + var policyRegistry = new PolicyRegistry + { + { CommandProcessor.RETRYPOLICYASYNC, retryPolicy }, + { CommandProcessor.CIRCUITBREAKERASYNC, circuitBreakerPolicy } + }; + + IAmAnExternalBusService bus = new ExternalBusServices(producerRegistry, policyRegistry, _fakeOutbox); + + CommandProcessor.ClearExtServiceBus(); _commandProcessor = new CommandProcessor( new InMemoryRequestContextFactory(), - new PolicyRegistry { { CommandProcessor.RETRYPOLICYASYNC, retryPolicy }, { CommandProcessor.CIRCUITBREAKERASYNC, circuitBreakerPolicy } }, + policyRegistry, messageMapperRegistry, - _fakeOutboxSync, - new ProducerRegistry(new Dictionary {{topic, _fakeMessageProducer},})); + bus); } [Fact] public async Task When_Implicit_Clearing_The_PostBox_On_The_Command_Processor_Async() { - await _fakeOutboxSync.AddAsync(_message); - await _fakeOutboxSync.AddAsync(_message2); + await _fakeOutbox.AddAsync(_message); + await _fakeOutbox.AddAsync(_message2); _commandProcessor.ClearAsyncOutbox(1,1); diff --git a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Inserting_A_Default_Inbox_Into_The_Publish_Pipeline.cs b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Inserting_A_Default_Inbox_Into_The_Publish_Pipeline.cs index 68b65ca9c0..48d871c524 100644 --- a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Inserting_A_Default_Inbox_Into_The_Publish_Pipeline.cs +++ b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Inserting_A_Default_Inbox_Into_The_Publish_Pipeline.cs @@ -44,6 +44,7 @@ public CommandProcessorBuildDefaultInboxPublishTests() .CircuitBreaker(1, TimeSpan.FromMilliseconds(1)); var inboxConfiguration = new InboxConfiguration( + _inbox, InboxScope.All, //grab all the events onceOnly: true, //only allow once actionOnExists: OnceOnlyAction.Throw //throw on duplicates (we should be the only entry after) diff --git a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Inserting_A_Default_Inbox_Into_The_Publish_Pipeline_Async.cs b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Inserting_A_Default_Inbox_Into_The_Publish_Pipeline_Async.cs index 4b9f70a1e5..4aad345f25 100644 --- a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Inserting_A_Default_Inbox_Into_The_Publish_Pipeline_Async.cs +++ b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Inserting_A_Default_Inbox_Into_The_Publish_Pipeline_Async.cs @@ -21,65 +21,64 @@ public class CommandProcessorBuildDefaultInboxPublishAsyncTests : IDisposable public CommandProcessorBuildDefaultInboxPublishAsyncTests() { - var handler = new MyEventHandlerAsync(new Dictionary()); - - var subscriberRegistry = new SubscriberRegistry(); - //This handler has no Inbox attribute - subscriberRegistry.RegisterAsync(); + var handler = new MyEventHandlerAsync(new Dictionary()); - var container = new ServiceCollection(); - container.AddSingleton(handler); - container.AddSingleton(_inbox); - container.AddTransient>(); - container.AddSingleton(new BrighterOptions {HandlerLifetime = ServiceLifetime.Transient}); + var subscriberRegistry = new SubscriberRegistry(); + //This handler has no Inbox attribute + subscriberRegistry.RegisterAsync(); - var handlerFactory = new ServiceProviderHandlerFactory(container.BuildServiceProvider()); + var container = new ServiceCollection(); + container.AddSingleton(handler); + container.AddSingleton(_inbox); + container.AddTransient>(); + container.AddSingleton( + new BrighterOptions { HandlerLifetime = ServiceLifetime.Transient }); + var handlerFactory = new ServiceProviderHandlerFactory(container.BuildServiceProvider()); var retryPolicy = Policy .Handle() .RetryAsync(); - var circuitBreakerPolicy = Policy + var circuitBreakerPolicy = Policy .Handle() .CircuitBreakerAsync(1, TimeSpan.FromMilliseconds(1)); - var inboxConfiguration = new InboxConfiguration( + var inboxConfiguration = new InboxConfiguration( + _inbox, InboxScope.All, //grab all the events onceOnly: true, //only allow once actionOnExists: OnceOnlyAction.Throw //throw on duplicates (we should be the only entry after) ); - _commandProcessor = new CommandProcessor( - subscriberRegistry, - handlerFactory, + _commandProcessor = new CommandProcessor( + subscriberRegistry, + handlerFactory, new InMemoryRequestContextFactory(), new PolicyRegistry { - { CommandProcessor.RETRYPOLICYASYNC, retryPolicy }, + { CommandProcessor.RETRYPOLICYASYNC, retryPolicy }, { CommandProcessor.CIRCUITBREAKERASYNC, circuitBreakerPolicy } }, inboxConfiguration: inboxConfiguration - ); - + ); } - - + [Fact] public async Task WhenInsertingADefaultInboxIntoTheSendPipeline() { //act var @event = new MyEvent(); await _commandProcessor.SendAsync(@event); - + //assert we are in, and auto-context added us under our name var boxed = await _inbox.ExistsAsync(@event.Id, typeof(MyEventHandlerAsync).FullName, 100); boxed.Should().BeTrue(); } - + public void Dispose() { CommandProcessor.ClearExtServiceBus(); } - } + } } diff --git a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Inserting_A_Default_Inbox_Into_The_Send_Pipeline.cs b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Inserting_A_Default_Inbox_Into_The_Send_Pipeline.cs index a5dca288c6..ef6e728611 100644 --- a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Inserting_A_Default_Inbox_Into_The_Send_Pipeline.cs +++ b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Inserting_A_Default_Inbox_Into_The_Send_Pipeline.cs @@ -29,7 +29,6 @@ public CommandProcessorBuildDefaultInboxSendTests() container.AddTransient>(); container.AddSingleton(new BrighterOptions {HandlerLifetime = ServiceLifetime.Transient}); - _provider = container.BuildServiceProvider(); var handlerFactory = new ServiceProviderHandlerFactory(_provider); @@ -42,6 +41,7 @@ public CommandProcessorBuildDefaultInboxSendTests() .CircuitBreaker(1, TimeSpan.FromMilliseconds(1)); var inboxConfiguration = new InboxConfiguration( + new InMemoryInbox(), InboxScope.All, //grab all the events onceOnly: true, //only allow once actionOnExists: OnceOnlyAction.Throw //throw on duplicates (we should be the only entry after) diff --git a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Inserting_A_Default_Inbox_Into_The_Send_Pipeline_Async.cs b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Inserting_A_Default_Inbox_Into_The_Send_Pipeline_Async.cs index e95bb28cf9..b8ad00c006 100644 --- a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Inserting_A_Default_Inbox_Into_The_Send_Pipeline_Async.cs +++ b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Inserting_A_Default_Inbox_Into_The_Send_Pipeline_Async.cs @@ -44,6 +44,7 @@ public CommandProcessorBuildDefaultInboxSendAsyncTests() .CircuitBreakerAsync(1, TimeSpan.FromMilliseconds(1)); var inboxConfiguration = new InboxConfiguration( + _inbox, InboxScope.All, //grab all the events onceOnly: true, //only allow once actionOnExists: OnceOnlyAction.Throw //throw on duplicates (we should be the only entry after) diff --git a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Posting_A_Message_And_There_Is_No_Message_Mapper_Registry.cs b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Posting_A_Message_And_There_Is_No_Message_Mapper_Registry.cs index 75a4c161be..5f60709b1c 100644 --- a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Posting_A_Message_And_There_Is_No_Message_Mapper_Registry.cs +++ b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Posting_A_Message_And_There_Is_No_Message_Mapper_Registry.cs @@ -25,6 +25,7 @@ THE SOFTWARE. */ using System; using System.Collections.Generic; using System.Text.Json; +using System.Transactions; using FluentAssertions; using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; using Paramore.Brighter.Core.Tests.TestHelpers; @@ -40,7 +41,7 @@ public class CommandProcessorNoMessageMapperTests : IDisposable private readonly CommandProcessor _commandProcessor; private readonly MyCommand _myCommand = new MyCommand(); private Message _message; - private readonly FakeOutboxSync _fakeOutbox; + private readonly FakeOutbox _fakeOutbox; private readonly FakeMessageProducerWithPublishConfirmation _fakeMessageProducerWithPublishConfirmation; private Exception _exception; @@ -48,7 +49,7 @@ public CommandProcessorNoMessageMapperTests() { _myCommand.Value = "Hello World"; - _fakeOutbox = new FakeOutboxSync(); + _fakeOutbox = new FakeOutbox(); _fakeMessageProducerWithPublishConfirmation = new FakeMessageProducerWithPublishConfirmation(); const string topic = "MyCommand"; @@ -67,12 +68,26 @@ public CommandProcessorNoMessageMapperTests() .Handle() .CircuitBreaker(1, TimeSpan.FromMilliseconds(1)); + var producerRegistry = new ProducerRegistry(new Dictionary + { + { topic, _fakeMessageProducerWithPublishConfirmation }, + }); + + var policyRegistry = new PolicyRegistry + { + { CommandProcessor.RETRYPOLICY, retryPolicy }, + { CommandProcessor.CIRCUITBREAKER, circuitBreakerPolicy } + }; + + IAmAnExternalBusService bus = new ExternalBusServices(producerRegistry, policyRegistry, _fakeOutbox); + + CommandProcessor.ClearExtServiceBus(); _commandProcessor = new CommandProcessor( - new InMemoryRequestContextFactory(), - new PolicyRegistry { { CommandProcessor.RETRYPOLICY, retryPolicy }, { CommandProcessor.CIRCUITBREAKER, circuitBreakerPolicy } }, + new InMemoryRequestContextFactory(), + policyRegistry, messageMapperRegistry, - _fakeOutbox, - new ProducerRegistry(new Dictionary {{topic, _fakeMessageProducerWithPublishConfirmation},})); + bus + ); } public void When_Posting_A_Message_And_There_Is_No_Message_Mapper_Registry() diff --git a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Posting_A_Message_And_There_Is_No_Message_Mapper_Registry_Async.cs b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Posting_A_Message_And_There_Is_No_Message_Mapper_Registry_Async.cs index 40d88ef3dc..6876b34b8d 100644 --- a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Posting_A_Message_And_There_Is_No_Message_Mapper_Registry_Async.cs +++ b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Posting_A_Message_And_There_Is_No_Message_Mapper_Registry_Async.cs @@ -26,6 +26,7 @@ THE SOFTWARE. */ using System.Collections.Generic; using System.Text.Json; using System.Threading.Tasks; +using System.Transactions; using FluentAssertions; using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; using Paramore.Brighter.Core.Tests.TestHelpers; @@ -41,7 +42,7 @@ public class CommandProcessorNoMessageMapperAsyncTests : IDisposable private readonly CommandProcessor _commandProcessor; private readonly MyCommand _myCommand = new MyCommand(); private Message _message; - private readonly FakeOutboxSync _fakeOutboxSync; + private readonly FakeOutbox _fakeOutbox; private readonly FakeMessageProducerWithPublishConfirmation _fakeMessageProducerWithPublishConfirmation; private Exception _exception; @@ -49,7 +50,7 @@ public CommandProcessorNoMessageMapperAsyncTests() { _myCommand.Value = "Hello World"; - _fakeOutboxSync = new FakeOutboxSync(); + _fakeOutbox = new FakeOutbox(); _fakeMessageProducerWithPublishConfirmation = new FakeMessageProducerWithPublishConfirmation(); const string topic = "MyCommand"; @@ -62,18 +63,21 @@ public CommandProcessorNoMessageMapperAsyncTests() var retryPolicy = Policy .Handle() - .Retry(); + .RetryAsync(); var circuitBreakerPolicy = Policy .Handle() - .CircuitBreaker(1, TimeSpan.FromMilliseconds(1)); + .CircuitBreakerAsync(1, TimeSpan.FromMilliseconds(1)); + + var policyRegistry = new PolicyRegistry { { CommandProcessor.RETRYPOLICYASYNC, retryPolicy }, { CommandProcessor.CIRCUITBREAKERASYNC, circuitBreakerPolicy } }; + var producerRegistry = new ProducerRegistry(new Dictionary {{topic, _fakeMessageProducerWithPublishConfirmation},}); + IAmAnExternalBusService bus = new ExternalBusServices(producerRegistry, policyRegistry, _fakeOutbox); _commandProcessor = new CommandProcessor( new InMemoryRequestContextFactory(), - new PolicyRegistry { { CommandProcessor.RETRYPOLICY, retryPolicy }, { CommandProcessor.CIRCUITBREAKER, circuitBreakerPolicy } }, + policyRegistry, messageMapperRegistry, - _fakeOutboxSync, - new ProducerRegistry(new Dictionary {{topic, _fakeMessageProducerWithPublishConfirmation},})); + bus); } [Fact] diff --git a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Posting_A_Message_And_There_Is_No_Message_Producer.cs b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Posting_A_Message_And_There_Is_No_Message_Producer.cs index 0fce6eaaba..17e6f465bd 100644 --- a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Posting_A_Message_And_There_Is_No_Message_Producer.cs +++ b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Posting_A_Message_And_There_Is_No_Message_Producer.cs @@ -23,7 +23,9 @@ THE SOFTWARE. */ #endregion using System; +using System.Collections.Generic; using System.Text.Json; +using System.Transactions; using FluentAssertions; using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; using Paramore.Brighter.Core.Tests.TestHelpers; @@ -40,7 +42,7 @@ public class CommandProcessorPostMissingMessageProducerTests : IDisposable { private readonly MyCommand _myCommand = new MyCommand(); private Message _message; - private readonly FakeOutboxSync _fakeOutboxSync; + private readonly FakeOutbox _fakeOutbox; private Exception _exception; private readonly MessageMapperRegistry _messageMapperRegistry; private readonly RetryPolicy _retryPolicy; @@ -50,7 +52,7 @@ public CommandProcessorPostMissingMessageProducerTests() { _myCommand.Value = "Hello World"; - _fakeOutboxSync = new FakeOutboxSync(); + _fakeOutbox = new FakeOutbox(); _message = new Message( new MessageHeader(_myCommand.Id, "MyCommand", MessageType.MT_COMMAND), @@ -67,19 +69,14 @@ public CommandProcessorPostMissingMessageProducerTests() _circuitBreakerPolicy = Policy .Handle() .CircuitBreaker(1, TimeSpan.FromMilliseconds(1)); - - } [Fact] public void When_Creating_A_Command_Processor_Without_Producer_Registry() { - _exception = Catch.Exception(() => new CommandProcessor( - new InMemoryRequestContextFactory(), - new PolicyRegistry { { CommandProcessor.RETRYPOLICY, _retryPolicy }, { CommandProcessor.CIRCUITBREAKER, _circuitBreakerPolicy } }, - _messageMapperRegistry, - _fakeOutboxSync, - null)); + var policyRegistry = new PolicyRegistry { { CommandProcessor.RETRYPOLICY, _retryPolicy }, { CommandProcessor.CIRCUITBREAKER, _circuitBreakerPolicy } }; + + _exception = Catch.Exception(() => new ExternalBusServices(null, policyRegistry, _fakeOutbox)); _exception.Should().BeOfType(); } diff --git a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Posting_A_Message_And_There_Is_No_Message_Store.cs b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Posting_A_Message_And_There_Is_No_Message_Store.cs deleted file mode 100644 index 9f62659ba3..0000000000 --- a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Posting_A_Message_And_There_Is_No_Message_Store.cs +++ /dev/null @@ -1,83 +0,0 @@ -#region -/* The MIT License (MIT) -Copyright © 2015 Ian Cooper - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the “Software”), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. */ - -#endregion - -using System; -using System.Collections.Generic; -using FluentAssertions; -using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; -using Paramore.Brighter.Core.Tests.TestHelpers; -using Polly; -using Polly.Registry; -using Xunit; - -namespace Paramore.Brighter.Core.Tests.CommandProcessors -{ - [Collection("CommandProcessor")] - public class CommandProcessorNoOutboxTests : IDisposable - { - private readonly CommandProcessor _commandProcessor; - private readonly MyCommand _myCommand = new MyCommand(); - private readonly FakeMessageProducerWithPublishConfirmation _fakeMessageProducerWithPublishConfirmation; - private Exception _exception; - - public CommandProcessorNoOutboxTests() - { - _myCommand.Value = "Hello World"; - - _fakeMessageProducerWithPublishConfirmation = new FakeMessageProducerWithPublishConfirmation(); - - var messageMapperRegistry = new MessageMapperRegistry(new SimpleMessageMapperFactory((_) => new MyCommandMessageMapper())); - messageMapperRegistry.Register(); - - var retryPolicy = Policy - .Handle() - .Retry(); - - var circuitBreakerPolicy = Policy - .Handle() - .CircuitBreaker(1, TimeSpan.FromMilliseconds(1)); - - _commandProcessor = new CommandProcessor( - new InMemoryRequestContextFactory(), - new PolicyRegistry { { CommandProcessor.RETRYPOLICY, retryPolicy }, { CommandProcessor.CIRCUITBREAKER, circuitBreakerPolicy } }, - messageMapperRegistry, - null, - new ProducerRegistry(new Dictionary {{"MyCommand", _fakeMessageProducerWithPublishConfirmation},})); - } - - [Fact] - public void When_Posting_A_Message_And_There_Is_No_Outbox() - { - _exception = Catch.Exception(() => _commandProcessor.Post(_myCommand)); - - //_should_throw_an_exception - _exception.Should().BeOfType(); - } - - public void Dispose() - { - CommandProcessor.ClearExtServiceBus(); - } - } -} diff --git a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Posting_A_Message_And_There_Is_No_Message_Store_Async.cs b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Posting_A_Message_And_There_Is_No_Message_Store_Async.cs deleted file mode 100644 index c25385c1b4..0000000000 --- a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Posting_A_Message_And_There_Is_No_Message_Store_Async.cs +++ /dev/null @@ -1,84 +0,0 @@ -#region Licence -/* The MIT License (MIT) -Copyright © 2015 Ian Cooper - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the “Software”), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. */ - -#endregion - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using FluentAssertions; -using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; -using Paramore.Brighter.Core.Tests.TestHelpers; -using Polly; -using Polly.Registry; -using Xunit; - -namespace Paramore.Brighter.Core.Tests.CommandProcessors -{ - [Collection("CommandProcessor")] - public class CommandProcessorNoOutboxAsyncTests : IDisposable - { - private readonly CommandProcessor _commandProcessor; - private readonly MyCommand _myCommand = new MyCommand(); - private readonly FakeMessageProducerWithPublishConfirmation _fakeMessageProducerWithPublishConfirmation; - private Exception _exception; - - public CommandProcessorNoOutboxAsyncTests() - { - _myCommand.Value = "Hello World"; - - _fakeMessageProducerWithPublishConfirmation = new FakeMessageProducerWithPublishConfirmation(); - - var messageMapperRegistry = new MessageMapperRegistry(new SimpleMessageMapperFactory((_) => new MyCommandMessageMapper())); - messageMapperRegistry.Register(); - - var retryPolicy = Policy - .Handle() - .Retry(); - - var circuitBreakerPolicy = Policy - .Handle() - .CircuitBreaker(1, TimeSpan.FromMilliseconds(1)); - - _commandProcessor = new CommandProcessor( - new InMemoryRequestContextFactory(), - new PolicyRegistry { { CommandProcessor.RETRYPOLICY, retryPolicy }, { CommandProcessor.CIRCUITBREAKER, circuitBreakerPolicy } }, - messageMapperRegistry, - null, - new ProducerRegistry(new Dictionary {{"MyCommand", _fakeMessageProducerWithPublishConfirmation},})); - } - - [Fact] - public async Task When_Posting_A_Message_And_There_Is_No_Outbox_Async() - { - _exception = await Catch.ExceptionAsync(async () => await _commandProcessor.PostAsync(_myCommand)); - - //_should_throw_an_exception - _exception.Should().BeOfType(); - } - - public void Dispose() - { - CommandProcessor.ClearExtServiceBus(); - } - } -} diff --git a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Posting_A_Message_To_The_Command_Processor.cs b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Posting_A_Message_To_The_Command_Processor.cs index 693a811771..e76a7d4081 100644 --- a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Posting_A_Message_To_The_Command_Processor.cs +++ b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Posting_A_Message_To_The_Command_Processor.cs @@ -26,6 +26,7 @@ THE SOFTWARE. */ using System.Collections.Generic; using System.Linq; using System.Text.Json; +using System.Transactions; using FluentAssertions; using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; using Polly; @@ -40,14 +41,14 @@ public class CommandProcessorPostCommandTests : IDisposable private readonly CommandProcessor _commandProcessor; private readonly MyCommand _myCommand = new MyCommand(); private readonly Message _message; - private readonly FakeOutboxSync _fakeOutbox; + private readonly FakeOutbox _fakeOutbox; private readonly FakeMessageProducerWithPublishConfirmation _fakeMessageProducerWithPublishConfirmation; public CommandProcessorPostCommandTests() { _myCommand.Value = "Hello World"; - _fakeOutbox = new FakeOutboxSync(); + _fakeOutbox = new FakeOutbox(); _fakeMessageProducerWithPublishConfirmation = new FakeMessageProducerWithPublishConfirmation(); const string topic = "MyCommand"; @@ -66,14 +67,18 @@ public CommandProcessorPostCommandTests() var circuitBreakerPolicy = Policy .Handle() .CircuitBreaker(1, TimeSpan.FromMilliseconds(1)); + + var policyRegistry = new PolicyRegistry { { CommandProcessor.RETRYPOLICY, retryPolicy }, { CommandProcessor.CIRCUITBREAKER, circuitBreakerPolicy } }; + var producerRegistry = new ProducerRegistry(new Dictionary {{topic, _fakeMessageProducerWithPublishConfirmation},}); + IAmAnExternalBusService bus = new ExternalBusServices(producerRegistry, policyRegistry, _fakeOutbox); + CommandProcessor.ClearExtServiceBus(); _commandProcessor = new CommandProcessor( new InMemoryRequestContextFactory(), - new PolicyRegistry { { CommandProcessor.RETRYPOLICY, retryPolicy }, { CommandProcessor.CIRCUITBREAKER, circuitBreakerPolicy } }, + policyRegistry, messageMapperRegistry, - _fakeOutbox, - new ProducerRegistry(new Dictionary {{topic, _fakeMessageProducerWithPublishConfirmation},})); - } + bus); + } [Fact] public void When_Posting_A_Message_To_The_Command_Processor() diff --git a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Posting_A_Message_To_The_Command_Processor_Async.cs b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Posting_A_Message_To_The_Command_Processor_Async.cs index 00342bcdde..997ed30a2a 100644 --- a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Posting_A_Message_To_The_Command_Processor_Async.cs +++ b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Posting_A_Message_To_The_Command_Processor_Async.cs @@ -27,6 +27,7 @@ THE SOFTWARE. */ using System.Linq; using System.Text.Json; using System.Threading.Tasks; +using System.Transactions; using FluentAssertions; using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; using Polly; @@ -41,14 +42,14 @@ public class CommandProcessorPostCommandAsyncTests : IDisposable private readonly CommandProcessor _commandProcessor; private readonly MyCommand _myCommand = new MyCommand(); private Message _message; - private readonly FakeOutboxSync _fakeOutboxSync; + private readonly FakeOutbox _fakeOutbox; private readonly FakeMessageProducerWithPublishConfirmation _fakeMessageProducerWithPublishConfirmation; public CommandProcessorPostCommandAsyncTests() { _myCommand.Value = "Hello World"; - _fakeOutboxSync = new FakeOutboxSync(); + _fakeOutbox = new FakeOutbox(); _fakeMessageProducerWithPublishConfirmation = new FakeMessageProducerWithPublishConfirmation(); const string topic = "MyCommand"; @@ -68,12 +69,16 @@ public CommandProcessorPostCommandAsyncTests() .Handle() .CircuitBreakerAsync(1, TimeSpan.FromMilliseconds(1)); + var policyRegistry = new PolicyRegistry { { CommandProcessor.RETRYPOLICYASYNC, retryPolicy }, { CommandProcessor.CIRCUITBREAKERASYNC, circuitBreakerPolicy } }; + var producerRegistry = new ProducerRegistry(new Dictionary {{topic, _fakeMessageProducerWithPublishConfirmation},}); + IAmAnExternalBusService bus = new ExternalBusServices(producerRegistry, policyRegistry, _fakeOutbox); + + CommandProcessor.ClearExtServiceBus(); _commandProcessor = new CommandProcessor( new InMemoryRequestContextFactory(), - new PolicyRegistry { { CommandProcessor.RETRYPOLICYASYNC, retryPolicy }, { CommandProcessor.CIRCUITBREAKERASYNC, circuitBreakerPolicy } }, + policyRegistry, messageMapperRegistry, - _fakeOutboxSync, - new ProducerRegistry(new Dictionary {{topic, _fakeMessageProducerWithPublishConfirmation},})); + bus); } [Fact] @@ -82,7 +87,7 @@ public async Task When_Posting_A_Message_To_The_Command_Processor_Async() await _commandProcessor.PostAsync(_myCommand); //_should_store_the_message_in_the_sent_command_message_repository - _fakeOutboxSync + _fakeOutbox .Get() .SingleOrDefault(msg => msg.Id == _message.Id) .Should().NotBeNull(); diff --git a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Posting_Fails_Limit_Total_Writes_To_OutBox_In_Window.cs b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Posting_Fails_Limit_Total_Writes_To_OutBox_In_Window.cs index faf31cf187..c26278cf46 100644 --- a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Posting_Fails_Limit_Total_Writes_To_OutBox_In_Window.cs +++ b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Posting_Fails_Limit_Total_Writes_To_OutBox_In_Window.cs @@ -26,6 +26,7 @@ THE SOFTWARE. */ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using System.Transactions; using FluentAssertions; using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; using Xunit; @@ -48,14 +49,22 @@ public PostFailureLimitCommandTests() new MessageMapperRegistry(new SimpleMessageMapperFactory((_) => new MyCommandMessageMapper())); messageMapperRegistry.Register(); + var busConfiguration = new ExternalBusConfiguration { + ProducerRegistry = new ProducerRegistry(new Dictionary + { + { "MyCommand", _fakeMessageProducer }, + }), + MessageMapperRegistry = messageMapperRegistry, + Outbox = _outbox + }; + _commandProcessor = CommandProcessorBuilder.With() .Handlers(new HandlerConfiguration(new SubscriberRegistry(), new EmptyHandlerFactorySync())) .DefaultPolicy() - .ExternalBus(new ExternalBusConfiguration( - new ProducerRegistry(new Dictionary {{"MyCommand", _fakeMessageProducer},}), - messageMapperRegistry), - _outbox - ) + .ExternalBusCreate( + busConfiguration, + _outbox, + new CommittableTransactionProvider()) .RequestContextFactory(new InMemoryRequestContextFactory()) .Build(); } diff --git a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Posting_Via_A_Control_Bus_Sender.cs b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Posting_Via_A_Control_Bus_Sender.cs index 743eee67e0..bba1e3e26e 100644 --- a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Posting_Via_A_Control_Bus_Sender.cs +++ b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Posting_Via_A_Control_Bus_Sender.cs @@ -26,6 +26,7 @@ THE SOFTWARE. */ using System.Collections.Generic; using System.Linq; using System.Text.Json; +using System.Transactions; using FluentAssertions; using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; using Polly; @@ -41,14 +42,14 @@ public class ControlBusSenderPostMessageTests : IDisposable private readonly ControlBusSender _controlBusSender; private readonly MyCommand _myCommand = new MyCommand(); private readonly Message _message; - private readonly FakeOutboxSync _fakeOutbox; + private readonly FakeOutbox _fakeOutbox; private readonly FakeMessageProducer _fakeMessageProducer; public ControlBusSenderPostMessageTests() { _myCommand.Value = "Hello World"; - _fakeOutbox = new FakeOutboxSync(); + _fakeOutbox = new FakeOutbox(); _fakeMessageProducer = new FakeMessageProducer(); const string topic = "MyCommand"; @@ -68,12 +69,16 @@ public ControlBusSenderPostMessageTests() .Handle() .CircuitBreaker(1, TimeSpan.FromMilliseconds(1)); + var policyRegistry = new PolicyRegistry { { CommandProcessor.RETRYPOLICY, retryPolicy }, { CommandProcessor.CIRCUITBREAKER, circuitBreakerPolicy } }; + var producerRegistry = new ProducerRegistry(new Dictionary {{topic, _fakeMessageProducer},}); + IAmAnExternalBusService bus = new ExternalBusServices(producerRegistry, policyRegistry, _fakeOutbox); + + CommandProcessor.ClearExtServiceBus(); _commandProcessor = new CommandProcessor( new InMemoryRequestContextFactory(), - new PolicyRegistry { { CommandProcessor.RETRYPOLICY, retryPolicy }, { CommandProcessor.CIRCUITBREAKER, circuitBreakerPolicy } }, + policyRegistry, messageMapperRegistry, - _fakeOutbox, - new ProducerRegistry(new Dictionary {{topic, _fakeMessageProducer},})); + bus); _controlBusSender = new ControlBusSender(_commandProcessor); } diff --git a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Posting_Via_A_Control_Bus_Sender_Async.cs b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Posting_Via_A_Control_Bus_Sender_Async.cs index 304e07f402..91a049877c 100644 --- a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Posting_Via_A_Control_Bus_Sender_Async.cs +++ b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Posting_Via_A_Control_Bus_Sender_Async.cs @@ -27,6 +27,7 @@ THE SOFTWARE. */ using System.Linq; using System.Text.Json; using System.Threading.Tasks; +using System.Transactions; using FluentAssertions; using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; using Polly; @@ -39,11 +40,10 @@ namespace Paramore.Brighter.Core.Tests.CommandProcessors [Collection("CommandProcessor")] public class ControlBusSenderPostMessageAsyncTests : IDisposable { - private readonly CommandProcessor _commandProcessor; private readonly ControlBusSender _controlBusSender; - private readonly MyCommand _myCommand = new MyCommand(); + private readonly MyCommand _myCommand = new(); private readonly Message _message; - private readonly IAmAnOutboxSync _outbox; + private readonly IAmAnOutboxSync _outbox; private readonly FakeMessageProducerWithPublishConfirmation _fakeMessageProducerWithPublishConfirmation; public ControlBusSenderPostMessageAsyncTests() @@ -70,14 +70,19 @@ public ControlBusSenderPostMessageAsyncTests() .Handle() .CircuitBreakerAsync(1, TimeSpan.FromMilliseconds(1)); - _commandProcessor = new CommandProcessor( + var producerRegistry = new ProducerRegistry(new Dictionary {{topic, _fakeMessageProducerWithPublishConfirmation},}); + var policyRegistry = new PolicyRegistry { { CommandProcessor.RETRYPOLICYASYNC, retryPolicy }, { CommandProcessor.CIRCUITBREAKERASYNC, circuitBreakerPolicy } }; + IAmAnExternalBusService bus = new ExternalBusServices(producerRegistry, policyRegistry, _outbox); + + CommandProcessor.ClearExtServiceBus(); + CommandProcessor commandProcessor = new CommandProcessor( new InMemoryRequestContextFactory(), - new PolicyRegistry { { CommandProcessor.RETRYPOLICYASYNC, retryPolicy }, { CommandProcessor.CIRCUITBREAKERASYNC, circuitBreakerPolicy } }, + policyRegistry, messageMapperRegistry, - _outbox, - new ProducerRegistry(new Dictionary {{topic, _fakeMessageProducerWithPublishConfirmation},})); + bus + ); - _controlBusSender = new ControlBusSender(_commandProcessor); + _controlBusSender = new ControlBusSender(commandProcessor); } [Fact(Skip = "Requires publisher confirmation")] diff --git a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Posting_With_A_Default_Policy.cs b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Posting_With_A_Default_Policy.cs index 64b7f68bb0..13014ca0d2 100644 --- a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Posting_With_A_Default_Policy.cs +++ b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Posting_With_A_Default_Policy.cs @@ -26,6 +26,7 @@ THE SOFTWARE. */ using System.Collections.Generic; using System.Linq; using System.Text.Json; +using System.Transactions; using FluentAssertions; using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; using Xunit; @@ -38,14 +39,14 @@ public class PostCommandTests : IDisposable private readonly CommandProcessor _commandProcessor; private readonly MyCommand _myCommand = new MyCommand(); private readonly Message _message; - private readonly FakeOutboxSync _fakeOutboxSync; + private readonly FakeOutbox _fakeOutbox; private readonly FakeMessageProducerWithPublishConfirmation _fakeMessageProducerWithPublishConfirmation; public PostCommandTests() { _myCommand.Value = "Hello World"; - _fakeOutboxSync = new FakeOutboxSync(); + _fakeOutbox = new FakeOutbox(); _fakeMessageProducerWithPublishConfirmation = new FakeMessageProducerWithPublishConfirmation(); const string topic = "MyCommand"; @@ -58,13 +59,21 @@ public PostCommandTests() new MessageMapperRegistry(new SimpleMessageMapperFactory((_) => new MyCommandMessageMapper())); messageMapperRegistry.Register(); + var busConfiguration = new ExternalBusConfiguration { + ProducerRegistry = new ProducerRegistry(new Dictionary + { + { topic, _fakeMessageProducerWithPublishConfirmation }, + }), + MessageMapperRegistry = messageMapperRegistry + }; + _commandProcessor = CommandProcessorBuilder.With() .Handlers(new HandlerConfiguration(new SubscriberRegistry(), new EmptyHandlerFactorySync())) .DefaultPolicy() - .ExternalBus(new ExternalBusConfiguration( - new ProducerRegistry(new Dictionary {{topic, _fakeMessageProducerWithPublishConfirmation},}), - messageMapperRegistry), - _fakeOutboxSync) + .ExternalBusCreate( + busConfiguration, + _fakeOutbox, + new CommittableTransactionProvider()) .RequestContextFactory(new InMemoryRequestContextFactory()) .Build(); } @@ -75,14 +84,14 @@ public void When_Posting_With_A_Default_Policy() _commandProcessor.Post(_myCommand); //should store the message in the sent outbox - _fakeOutboxSync + _fakeOutbox .Get() .SingleOrDefault(msg => msg.Id == _message.Id) .Should().NotBeNull(); //should send a message via the messaging gateway _fakeMessageProducerWithPublishConfirmation.MessageWasSent.Should().BeTrue(); // should convert the command into a message - _fakeOutboxSync.Get().First().Should().Be(_message); + _fakeOutbox.Get().First().Should().Be(_message); } public void Dispose() diff --git a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Posting_With_An_In_Memory_Message_Store.cs b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Posting_With_An_In_Memory_Message_Store.cs index 374aa9f26c..f581311bf7 100644 --- a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Posting_With_An_In_Memory_Message_Store.cs +++ b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Posting_With_An_In_Memory_Message_Store.cs @@ -25,6 +25,7 @@ THE SOFTWARE. */ using System; using System.Collections.Generic; using System.Text.Json; +using System.Transactions; using FluentAssertions; using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; using Polly; @@ -62,13 +63,17 @@ public CommandProcessorWithInMemoryOutboxTests() var circuitBreakerPolicy = Policy .Handle() .CircuitBreaker(1, TimeSpan.FromMilliseconds(1)); + + var policyRegistry = new PolicyRegistry { { CommandProcessor.RETRYPOLICY, retryPolicy }, { CommandProcessor.CIRCUITBREAKER, circuitBreakerPolicy } }; + var producerRegistry = new ProducerRegistry(new Dictionary {{topic, _fakeMessageProducerWithPublishConfirmation},}); + IAmAnExternalBusService bus = new ExternalBusServices(producerRegistry, policyRegistry, _outbox); + CommandProcessor.ClearExtServiceBus(); _commandProcessor = new CommandProcessor( new InMemoryRequestContextFactory(), - new PolicyRegistry { { CommandProcessor.RETRYPOLICY, retryPolicy }, { CommandProcessor.CIRCUITBREAKER, circuitBreakerPolicy } }, + policyRegistry, messageMapperRegistry, - _outbox, - new ProducerRegistry(new Dictionary {{topic, _fakeMessageProducerWithPublishConfirmation},})); + bus); } [Fact] diff --git a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Posting_With_An_In_Memory_Message_Store_Async.cs b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Posting_With_An_In_Memory_Message_Store_Async.cs index 807b91958e..91a562c6b3 100644 --- a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Posting_With_An_In_Memory_Message_Store_Async.cs +++ b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Posting_With_An_In_Memory_Message_Store_Async.cs @@ -26,6 +26,7 @@ THE SOFTWARE. */ using System.Collections.Generic; using System.Text.Json; using System.Threading.Tasks; +using System.Transactions; using FluentAssertions; using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; using Polly; @@ -67,12 +68,16 @@ public CommandProcessorWithInMemoryOutboxAscyncTests() .Handle() .CircuitBreakerAsync(1, TimeSpan.FromMilliseconds(1)); + var policyRegistry = new PolicyRegistry { { CommandProcessor.RETRYPOLICYASYNC, retryPolicy }, { CommandProcessor.CIRCUITBREAKERASYNC, circuitBreakerPolicy } }; + var producerRegistry = new ProducerRegistry(new Dictionary {{topic, _fakeMessageProducerWithPublishConfirmation},}); + IAmAnExternalBusService bus = new ExternalBusServices(producerRegistry, policyRegistry, _outbox); + + CommandProcessor.ClearExtServiceBus(); _commandProcessor = new CommandProcessor( new InMemoryRequestContextFactory(), - new PolicyRegistry { { CommandProcessor.RETRYPOLICYASYNC, retryPolicy }, { CommandProcessor.CIRCUITBREAKERASYNC, circuitBreakerPolicy } }, + policyRegistry, messageMapperRegistry, - _outbox, - new ProducerRegistry(new Dictionary {{topic, _fakeMessageProducerWithPublishConfirmation},})); + bus); } diff --git a/tests/Paramore.Brighter.Core.Tests/ControlBus/When_creating_a_control_bus_sender.cs b/tests/Paramore.Brighter.Core.Tests/ControlBus/When_creating_a_control_bus_sender.cs index d01d090d60..f4abb3268d 100644 --- a/tests/Paramore.Brighter.Core.Tests/ControlBus/When_creating_a_control_bus_sender.cs +++ b/tests/Paramore.Brighter.Core.Tests/ControlBus/When_creating_a_control_bus_sender.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Transactions; using FakeItEasy; using FluentAssertions; using Xunit; @@ -7,28 +8,28 @@ namespace Paramore.Brighter.Core.Tests.ControlBus { public class ControlBusSenderFactoryTests { - private IAmAControlBusSender s_sender; - private readonly IAmAControlBusSenderFactory s_senderFactory; - private readonly IAmAnOutboxSync _fakeOutboxSync; - private readonly IAmAMessageProducerSync s_fakeGateway; + private IAmAControlBusSender _sender; + private readonly IAmAControlBusSenderFactory _senderFactory; + private readonly IAmAnOutboxSync _fakeOutbox; + private readonly IAmAMessageProducerSync _fakeGateway; public ControlBusSenderFactoryTests() { - _fakeOutboxSync = A.Fake>(); - s_fakeGateway = A.Fake(); + _fakeOutbox = A.Fake>(); + _fakeGateway = A.Fake(); - s_senderFactory = new ControlBusSenderFactory(); + _senderFactory = new ControlBusSenderFactory(); } [Fact] public void When_creating_a_control_bus_sender() { - s_sender = s_senderFactory.Create( - _fakeOutboxSync, - new ProducerRegistry(new Dictionary {{"MyTopic", s_fakeGateway},})); + _sender = _senderFactory.Create( + _fakeOutbox, + new ProducerRegistry(new Dictionary {{"MyTopic", _fakeGateway},})); //_should_create_a_control_bus_sender - s_sender.Should().NotBeNull(); + _sender.Should().NotBeNull(); } } } diff --git a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/TestDoubles/SpyCommandProcessor.cs b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/TestDoubles/SpyCommandProcessor.cs index edeae3a976..f9508209d0 100644 --- a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/TestDoubles/SpyCommandProcessor.cs +++ b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/TestDoubles/SpyCommandProcessor.cs @@ -1,4 +1,5 @@ #region Licence + /* The MIT License (MIT) Copyright © 2014 Ian Cooper @@ -53,7 +54,7 @@ public class ClearParams public Dictionary Args; } - internal class SpyCommandProcessor : Paramore.Brighter.IAmACommandProcessor + internal class SpyCommandProcessor : IAmACommandProcessor { private readonly Queue _requests = new Queue(); private readonly Dictionary _postBox = new Dictionary(); @@ -67,7 +68,8 @@ public virtual void Send(T command) where T : class, IRequest Commands.Add(CommandType.Send); } - public virtual async Task SendAsync(T command, bool continueOnCapturedContext = false, CancellationToken cancellationToken = default) where T : class, IRequest + public virtual async Task SendAsync(TRequest command, bool continueOnCapturedContext = false, + CancellationToken cancellationToken = default) where TRequest : class, IRequest { _requests.Enqueue(command); Commands.Add(CommandType.SendAsync); @@ -76,13 +78,14 @@ public virtual async Task SendAsync(T command, bool continueOnCapturedContext await completionSource.Task; } - public virtual void Publish(T @event) where T : class, IRequest + public virtual void Publish(TRequest @event) where TRequest : class, IRequest { _requests.Enqueue(@event); Commands.Add(CommandType.Publish); } - public virtual async Task PublishAsync(T @event, bool continueOnCapturedContext = false, CancellationToken cancellationToken = default) where T : class, IRequest + public virtual async Task PublishAsync(TRequest @event, bool continueOnCapturedContext = false, + CancellationToken cancellationToken = default) where TRequest : class, IRequest { _requests.Enqueue(@event); Commands.Add(CommandType.PublishAsync); @@ -92,20 +95,20 @@ public virtual async Task PublishAsync(T @event, bool continueOnCapturedConte await completionSource.Task; } - public virtual void Post(T request) where T : class, IRequest + public virtual void Post(TRequest request) where TRequest : class, IRequest { _requests.Enqueue(request); Commands.Add(CommandType.Post); } - /// - /// Posts the specified request with async/await support. - /// - /// - /// The request. - /// Should we use the calling thread's synchronization context when continuing or a default thread synchronization context. Defaults to false - /// awaitable . - public virtual async Task PostAsync(T request, bool continueOnCapturedContext = false, CancellationToken cancellationToken = default) where T : class, IRequest + public virtual void Post(TRequest request, + IAmABoxTransactionProvider provider) where TRequest : class, IRequest + { + Post(request); + } + + public virtual async Task PostAsync(TRequest request, bool continueOnCapturedContext = false, + CancellationToken cancellationToken = default) where TRequest : class, IRequest { _requests.Enqueue(request); Commands.Add(CommandType.PostAsync); @@ -115,16 +118,30 @@ public virtual async Task PostAsync(T request, bool continueOnCapturedContext await completionSource.Task; } - public Guid DepositPost(T request) where T : class, IRequest + public virtual async Task PostAsync(TRequest request, + IAmABoxTransactionProvider provider, bool continueOnCapturedContext = false, + CancellationToken cancellationToken = default) where TRequest : class, IRequest + { + await PostAsync(request, cancellationToken: cancellationToken); + } + + public Guid DepositPost(TRequest request) where TRequest : class, IRequest { _postBox.Add(request.Id, request); return request.Id; } - public Guid[] DepositPost(IEnumerable request) where T : class, IRequest + public Guid DepositPost(TRequest request, + IAmABoxTransactionProvider provider) where TRequest : class, IRequest + { + return DepositPost(request); + } + + + public Guid[] DepositPost(IEnumerable request) where TRequest : class, IRequest { var ids = new List(); - foreach (T r in request) + foreach (TRequest r in request) { ids.Add(DepositPost(r)); } @@ -132,8 +149,27 @@ public Guid[] DepositPost(IEnumerable request) where T : class, IRequest return ids.ToArray(); } - public async Task DepositPostAsync(T request, bool continueOnCapturedContext = false, - CancellationToken cancellationToken = default) where T : class, IRequest + public Guid[] DepositPost( + IEnumerable request, IAmABoxTransactionProvider provider) + where TRequest : class, IRequest + { + return DepositPost(request); + } + + public async Task DepositPostAsync(TRequest request, bool continueOnCapturedContext = false, + CancellationToken cancellationToken = default) where TRequest : class, IRequest + { + _postBox.Add(request.Id, request); + + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + tcs.SetResult(request.Id); + return await tcs.Task; + } + + public async Task DepositPostAsync(TRequest request, + IAmABoxTransactionProvider provider, + bool continueOnCapturedContext = false, CancellationToken cancellationToken = default) + where TRequest : class, IRequest { _postBox.Add(request.Id, request); @@ -142,11 +178,12 @@ public async Task DepositPostAsync(T request, bool continueOnCapturedCo return await tcs.Task; } - public async Task DepositPostAsync(IEnumerable requests, bool continueOnCapturedContext = false, - CancellationToken cancellationToken = default) where T : class, IRequest + public async Task DepositPostAsync(IEnumerable requests, + bool continueOnCapturedContext = false, + CancellationToken cancellationToken = default) where TRequest : class, IRequest { var ids = new List(); - foreach (T r in requests) + foreach (TRequest r in requests) { ids.Add(await DepositPostAsync(r, cancellationToken: cancellationToken)); } @@ -154,6 +191,14 @@ public async Task DepositPostAsync(IEnumerable requests, bool cont return ids.ToArray(); } + public async Task DepositPostAsync(IEnumerable requests, + IAmABoxTransactionProvider provider, + bool continueOnCapturedContext = false, + CancellationToken cancellationToken = default) where TRequest : class, IRequest + { + return await DepositPostAsync(requests, cancellationToken: cancellationToken); + } + public void ClearOutbox(params Guid[] posts) { foreach (var messageId in posts) @@ -168,7 +213,10 @@ public void ClearOutbox(params Guid[] posts) public void ClearOutbox(int amountToClear = 100, int minimumAge = 5000, Dictionary args = null) { Commands.Add(CommandType.Clear); - ClearParamsList.Add(new ClearParams { AmountToClear = amountToClear, MinimumAge = minimumAge, Args = args }); + ClearParamsList.Add(new ClearParams + { + AmountToClear = amountToClear, MinimumAge = minimumAge, Args = args + }); } public async Task ClearOutboxAsync(IEnumerable posts, bool continueOnCapturedContext = false, @@ -181,10 +229,14 @@ public async Task ClearOutboxAsync(IEnumerable posts, bool continueOnCaptu await completionSource.Task; } - public void ClearAsyncOutbox(int amountToClear = 100, int minimumAge = 5000, bool useBulk = false, Dictionary args = null) + public void ClearAsyncOutbox(int amountToClear = 100, int minimumAge = 5000, bool useBulk = false, + Dictionary args = null) { Commands.Add(CommandType.Clear); - ClearParamsList.Add(new ClearParams { AmountToClear = amountToClear, MinimumAge = minimumAge, Args = args }); + ClearParamsList.Add(new ClearParams + { + AmountToClear = amountToClear, MinimumAge = minimumAge, Args = args + }); } public Task BulkClearOutboxAsync(IEnumerable posts, bool continueOnCapturedContext = false, @@ -193,16 +245,17 @@ public Task BulkClearOutboxAsync(IEnumerable posts, bool continueOnCapture return ClearOutboxAsync(posts, continueOnCapturedContext, cancellationToken); } - public TResponse Call(T request, int timeOutInMilliseconds) where T : class, ICall where TResponse : class, IResponse + public TResponse Call(T request, int timeOutInMilliseconds) + where T : class, ICall where TResponse : class, IResponse { _requests.Enqueue(request); Commands.Add(CommandType.Call); - return default (TResponse); + return default(TResponse); } public virtual T Observe() where T : class, IRequest { - return (T) _requests.Dequeue(); + return (T)_requests.Dequeue(); } public bool ContainsCommand(CommandType commandType) @@ -234,27 +287,31 @@ public override void Publish(T @event) base.Publish(@event); PublishCount++; - var exceptions = new List {new DeferMessageAction()}; + var exceptions = new List { new DeferMessageAction() }; - throw new AggregateException("Failed to publish to one more handlers successfully, see inner exceptions for details", exceptions); + throw new AggregateException( + "Failed to publish to one more handlers successfully, see inner exceptions for details", exceptions); } - public override async Task SendAsync(T command, bool continueOnCapturedContext = false, CancellationToken cancellationToken = default) + + public override async Task SendAsync(T command, bool continueOnCapturedContext = false, + CancellationToken cancellationToken = default) { await base.SendAsync(command, continueOnCapturedContext, cancellationToken); SendCount++; throw new DeferMessageAction(); } - public override async Task PublishAsync(T @event, bool continueOnCapturedContext = false, CancellationToken cancellationToken = default) + public override async Task PublishAsync(T @event, bool continueOnCapturedContext = false, + CancellationToken cancellationToken = default) { await base.PublishAsync(@event, continueOnCapturedContext, cancellationToken); PublishCount++; var exceptions = new List { new DeferMessageAction() }; - throw new AggregateException("Failed to publish to one more handlers successfully, see inner exceptions for details", exceptions); + throw new AggregateException( + "Failed to publish to one more handlers successfully, see inner exceptions for details", exceptions); } - } internal class SpyExceptionCommandProcessor : SpyCommandProcessor diff --git a/tests/Paramore.Brighter.Core.Tests/Observability/When_Implicitly_Clearing_The_Outbox_A_Span_Is_Exported.cs b/tests/Paramore.Brighter.Core.Tests/Observability/When_Implicitly_Clearing_The_Outbox_A_Span_Is_Exported.cs index 041539e1f9..711a9895df 100644 --- a/tests/Paramore.Brighter.Core.Tests/Observability/When_Implicitly_Clearing_The_Outbox_A_Span_Is_Exported.cs +++ b/tests/Paramore.Brighter.Core.Tests/Observability/When_Implicitly_Clearing_The_Outbox_A_Span_Is_Exported.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.Linq; using System.Threading.Tasks; +using System.Transactions; using Microsoft.Extensions.DependencyInjection; using OpenTelemetry; using OpenTelemetry.Trace; @@ -17,14 +18,13 @@ namespace Paramore.Brighter.Core.Tests.Observability; public class ImplicitClearingObservabilityTests : IDisposable { private readonly CommandProcessor _commandProcessor; - private readonly IAmAnOutboxSync _outbox; private readonly MyEvent _event; private readonly TracerProvider _traceProvider; private readonly List _exportedActivities; public ImplicitClearingObservabilityTests() { - _outbox = new InMemoryOutbox(); + IAmAnOutboxSync outbox = new InMemoryOutbox(); _event = new MyEvent("TestEvent"); var registry = new SubscriberRegistry(); @@ -57,8 +57,17 @@ public ImplicitClearingObservabilityTests() }); producerRegistry.GetDefaultProducer().MaxOutStandingMessages = -1; - _commandProcessor = new CommandProcessor(registry, handlerFactory, new InMemoryRequestContextFactory(), - policyRegistry, messageMapperRegistry,_outbox,producerRegistry); + IAmAnExternalBusService bus = new ExternalBusServices(producerRegistry, policyRegistry, outbox); + + CommandProcessor.ClearExtServiceBus(); + + _commandProcessor = new CommandProcessor( + registry, + handlerFactory, + new InMemoryRequestContextFactory(), + policyRegistry, + messageMapperRegistry, + bus); } [Fact] diff --git a/tests/Paramore.Brighter.Core.Tests/Observability/When_Implicitly_Clearing_The_Outbox_async_A_Span_Is_Exported.cs b/tests/Paramore.Brighter.Core.Tests/Observability/When_Implicitly_Clearing_The_Outbox_async_A_Span_Is_Exported.cs index e3aceb9189..15ea9d7596 100644 --- a/tests/Paramore.Brighter.Core.Tests/Observability/When_Implicitly_Clearing_The_Outbox_async_A_Span_Is_Exported.cs +++ b/tests/Paramore.Brighter.Core.Tests/Observability/When_Implicitly_Clearing_The_Outbox_async_A_Span_Is_Exported.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.Linq; using System.Threading.Tasks; +using System.Transactions; using Microsoft.Extensions.DependencyInjection; using OpenTelemetry; using OpenTelemetry.Exporter; @@ -18,14 +19,13 @@ namespace Paramore.Brighter.Core.Tests.Observability; public class ImplicitClearingAsyncObservabilityTests : IDisposable { private readonly CommandProcessor _commandProcessor; - private readonly IAmAnOutboxSync _outbox; private readonly MyEvent _event; private readonly TracerProvider _traceProvider; private readonly List _exportedActivities; public ImplicitClearingAsyncObservabilityTests() { - _outbox = new InMemoryOutbox(); + IAmAnOutboxSync outbox = new InMemoryOutbox(); _event = new MyEvent("TestEvent"); var registry = new SubscriberRegistry(); @@ -49,22 +49,29 @@ public ImplicitClearingAsyncObservabilityTests() var retryPolicy = Policy .Handle() - .Retry(); + .RetryAsync(); var circuitBreakerPolicy = Policy .Handle() .CircuitBreakerAsync(1, TimeSpan.FromMilliseconds(1)); - var policyRegistry = new PolicyRegistry {{CommandProcessor.RETRYPOLICY, retryPolicy}, {CommandProcessor.RETRYPOLICYASYNC, circuitBreakerPolicy}}; + var policyRegistry = new PolicyRegistry {{CommandProcessor.RETRYPOLICYASYNC, retryPolicy}, {CommandProcessor.CIRCUITBREAKERASYNC, circuitBreakerPolicy}}; var producerRegistry = new ProducerRegistry(new Dictionary { {MyEvent.Topic, new FakeMessageProducer()} }); producerRegistry.GetDefaultProducer().MaxOutStandingMessages = -1; + IAmAnExternalBusService bus = new ExternalBusServices(producerRegistry, policyRegistry, outbox); + CommandProcessor.ClearExtServiceBus(); - _commandProcessor = new CommandProcessor(registry, handlerFactory, new InMemoryRequestContextFactory(), - policyRegistry, messageMapperRegistry,_outbox,producerRegistry); + _commandProcessor = new CommandProcessor( + registry, + handlerFactory, + new InMemoryRequestContextFactory(), + policyRegistry, + messageMapperRegistry, + bus); } [Fact] diff --git a/tests/Paramore.Brighter.DynamoDB.Tests/Outbox/When_there_is_a_transaction_between_outbox_and_entity.cs b/tests/Paramore.Brighter.DynamoDB.Tests/Outbox/When_there_is_a_transaction_between_outbox_and_entity.cs index 5a675058d9..d157563ed8 100644 --- a/tests/Paramore.Brighter.DynamoDB.Tests/Outbox/When_there_is_a_transaction_between_outbox_and_entity.cs +++ b/tests/Paramore.Brighter.DynamoDB.Tests/Outbox/When_there_is_a_transaction_between_outbox_and_entity.cs @@ -9,16 +9,19 @@ using Paramore.Brighter.DynamoDB.Tests.TestDoubles; using Paramore.Brighter.Outbox.DynamoDB; using Xunit; +using Xunit.Abstractions; namespace Paramore.Brighter.DynamoDB.Tests.Outbox; public class DynamoDbOutboxTransactionTests : DynamoDBOutboxBaseTest { + private readonly ITestOutputHelper _testOutputHelper; private readonly DynamoDbOutbox _dynamoDbOutbox; private readonly string _entityTableName; - public DynamoDbOutboxTransactionTests() + public DynamoDbOutboxTransactionTests(ITestOutputHelper testOutputHelper) { + _testOutputHelper = testOutputHelper; var tableRequestFactory = new DynamoDbTableFactory(); //act @@ -69,15 +72,16 @@ public async void When_There_Is_A_Transaction_Between_Outbox_And_Entity() TransactWriteItemsResponse response = null; try { - var transaction = uow.BeginOrGetTransaction(); + var transaction = await uow.GetTransactionAsync(); transaction.TransactItems.Add(new TransactWriteItem { Put = new Put { TableName = _entityTableName, Item = attributes, } }); transaction.TransactItems.Add(new TransactWriteItem { Put = new Put { TableName = OutboxTableName, Item = messageAttributes}}); - response = await uow.CommitAsync(); + await uow.CommitAsync(); + response = uow.LastResponse; } catch (Exception e) { - Console.WriteLine(e); + _testOutputHelper.WriteLine(e.ToString()); throw; } diff --git a/tests/Paramore.Brighter.DynamoDB.Tests/Outbox/When_writing_a_utf8_message_to_the_outbox.cs b/tests/Paramore.Brighter.DynamoDB.Tests/Outbox/When_writing_a_utf8_message_to_the_outbox.cs index 59a17ec9fc..a2404b0f32 100644 --- a/tests/Paramore.Brighter.DynamoDB.Tests/Outbox/When_writing_a_utf8_message_to_the_outbox.cs +++ b/tests/Paramore.Brighter.DynamoDB.Tests/Outbox/When_writing_a_utf8_message_to_the_outbox.cs @@ -89,7 +89,8 @@ public void When_writing_a_utf8_message_to_the_dynamo_db_outbox() //should read the header from the outbox _storedMessage.Header.Topic.Should().Be(_messageEarliest.Header.Topic); _storedMessage.Header.MessageType.Should().Be(_messageEarliest.Header.MessageType); - _storedMessage.Header.TimeStamp.Should().Be(_messageEarliest.Header.TimeStamp); + _storedMessage.Header.TimeStamp.ToString("yyyy-MM-ddTHH:mm:ss.fZ") + .Should().Be(_messageEarliest.Header.TimeStamp.ToString("yyyy-MM-ddTHH:mm:ss.fZ")); _storedMessage.Header.HandledCount.Should().Be(0); // -- should be zero when read from outbox _storedMessage.Header.DelayedMilliseconds.Should().Be(0); // -- should be zero when read from outbox _storedMessage.Header.CorrelationId.Should().Be(_messageEarliest.Header.CorrelationId); diff --git a/tests/Paramore.Brighter.DynamoDB.Tests/Outbox/When_writing_a_utf8_message_to_the_outbox_async.cs b/tests/Paramore.Brighter.DynamoDB.Tests/Outbox/When_writing_a_utf8_message_to_the_outbox_async.cs index 5650190b78..54509bc5c3 100644 --- a/tests/Paramore.Brighter.DynamoDB.Tests/Outbox/When_writing_a_utf8_message_to_the_outbox_async.cs +++ b/tests/Paramore.Brighter.DynamoDB.Tests/Outbox/When_writing_a_utf8_message_to_the_outbox_async.cs @@ -92,14 +92,14 @@ public async Task When_writing_a_utf8_message_to_the_dynamo_db_outbox() //should read the header from the outbox _storedMessage.Header.Topic.Should().Be(_messageEarliest.Header.Topic); _storedMessage.Header.MessageType.Should().Be(_messageEarliest.Header.MessageType); - _storedMessage.Header.TimeStamp.Should().Be(_messageEarliest.Header.TimeStamp); + _storedMessage.Header.TimeStamp.ToString("yyyy-MM-ddTHH:mm:ss.fffZ").Should() + .Be(_messageEarliest.Header.TimeStamp.ToString("yyyy-MM-ddTHH:mm:ss.fffZ")); _storedMessage.Header.HandledCount.Should().Be(0); // -- should be zero when read from outbox _storedMessage.Header.DelayedMilliseconds.Should().Be(0); // -- should be zero when read from outbox _storedMessage.Header.CorrelationId.Should().Be(_messageEarliest.Header.CorrelationId); _storedMessage.Header.ReplyTo.Should().Be(_messageEarliest.Header.ReplyTo); _storedMessage.Header.ContentType.Should().Be(_messageEarliest.Header.ContentType); - //Bag serialization _storedMessage.Header.Bag.ContainsKey(_key1).Should().BeTrue(); _storedMessage.Header.Bag[_key1].Should().Be(_value1); diff --git a/tests/Paramore.Brighter.Extensions.Tests/TestDifferentSetups.cs b/tests/Paramore.Brighter.Extensions.Tests/TestDifferentSetups.cs index 79aa81c260..89bd7ec271 100644 --- a/tests/Paramore.Brighter.Extensions.Tests/TestDifferentSetups.cs +++ b/tests/Paramore.Brighter.Extensions.Tests/TestDifferentSetups.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using System.Transactions; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Paramore.Brighter; @@ -29,17 +30,19 @@ public void BasicSetup() } [Fact] - public void WithProducerRegistry() + public void WithExternalBus() { var serviceCollection = new ServiceCollection(); - var producer = new ProducerRegistry(new Dictionary { { "MyTopic", new FakeProducerSync() }, }); + var producerRegistry = new ProducerRegistry(new Dictionary { { "MyTopic", new FakeProducer() }, }); serviceCollection.AddSingleton(); serviceCollection .AddBrighter() - .UseInMemoryOutbox() - .UseExternalBus(producer, false) + .UseExternalBus((config) => + { + config.ProducerRegistry = producerRegistry; + }) .AutoFromAssemblies(); var serviceProvider = serviceCollection.BuildServiceProvider(); @@ -98,8 +101,10 @@ public void WithScopedLifetime() } - internal class FakeProducerSync : IAmAMessageProducerSync, IAmAMessageProducerAsync + internal class FakeProducer : IAmAMessageProducerSync, IAmAMessageProducerAsync { + public List SentMessages { get; } = new List(); + public int MaxOutStandingMessages { get; set; } = -1; public int MaxOutStandingCheckIntervalMilliSeconds { get; set; } = 0; @@ -107,22 +112,26 @@ internal class FakeProducerSync : IAmAMessageProducerSync, IAmAMessageProducerAs public void Dispose() { - throw new NotImplementedException(); + SentMessages.Clear(); } public Task SendAsync(Message message) { - throw new NotImplementedException(); + var tcs = new TaskCompletionSource(); + Send(message); + tcs.SetResult(); + return tcs.Task; } public void Send(Message message) { - throw new NotImplementedException(); + SentMessages.Add(message); } public void SendWithDelay(Message message, int delayMilliseconds = 0) { - throw new NotImplementedException(); + Task.Delay(TimeSpan.FromMilliseconds(delayMilliseconds)).Wait(); + Send(message); } } } diff --git a/tests/Paramore.Brighter.Extensions.Tests/TestTransform.cs b/tests/Paramore.Brighter.Extensions.Tests/TestTransform.cs index 057aebb715..5696956e18 100644 --- a/tests/Paramore.Brighter.Extensions.Tests/TestTransform.cs +++ b/tests/Paramore.Brighter.Extensions.Tests/TestTransform.cs @@ -1,32 +1,40 @@ -using System.Threading; +using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; namespace Paramore.Brighter.Extensions.Tests; public class TestTransform : IAmAMessageTransformAsync { + public List WrapInitializerList { get; set; } = new List(); + public List UnwrapInitializerList { get; set; } = new List(); + public void Dispose() { - throw new System.NotImplementedException(); + WrapInitializerList.Clear(); } public void InitializeWrapFromAttributeParams(params object[] initializerList) { - throw new System.NotImplementedException(); + WrapInitializerList.AddRange(initializerList); } public void InitializeUnwrapFromAttributeParams(params object[] initializerList) { - throw new System.NotImplementedException(); + UnwrapInitializerList.AddRange(initializerList); } public async Task WrapAsync(Message message, CancellationToken cancellationToken) { - throw new System.NotImplementedException(); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + tcs.SetResult(message); + return tcs.Task.Result; } public async Task UnwrapAsync(Message message, CancellationToken cancellationToken) { - throw new System.NotImplementedException(); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + tcs.SetResult(message); + return tcs.Task.Result; } } diff --git a/tests/Paramore.Brighter.InMemory.Tests/TestDoubles/FakeCommandProcessor.cs b/tests/Paramore.Brighter.InMemory.Tests/TestDoubles/FakeCommandProcessor.cs index b7d5a82f85..df8c049749 100644 --- a/tests/Paramore.Brighter.InMemory.Tests/TestDoubles/FakeCommandProcessor.cs +++ b/tests/Paramore.Brighter.InMemory.Tests/TestDoubles/FakeCommandProcessor.cs @@ -59,6 +59,12 @@ public void Post(T request) where T : class, IRequest { ClearOutbox(DepositPost(request)); } + + public void Post(T request, IAmABoxTransactionProvider provider) where T : class, IRequest + { + Post(request); + } + public Task PostAsync(T request, bool continueOnCapturedContext = false, CancellationToken cancellationToken = default) where T : class, IRequest { @@ -71,12 +77,22 @@ public Task PostAsync(T request, bool continueOnCapturedContext = false, Canc return tcs.Task; } + + public Task PostAsync(T request, IAmABoxTransactionProvider provider, bool continueOnCapturedContext = false, CancellationToken cancellationToken = default) where T : class, IRequest + { + return PostAsync(request, continueOnCapturedContext, cancellationToken); + } public Guid DepositPost(T request) where T : class, IRequest { Deposited.Enqueue(new DepositedMessage(request)); return request.Id; } + + public Guid DepositPost(T request, IAmABoxTransactionProvider provider) where T : class, IRequest + { + return DepositPost(request); + } public Guid[] DepositPost(IEnumerable request) where T : class, IRequest { @@ -88,6 +104,11 @@ public Guid[] DepositPost(IEnumerable request) where T : class, IRequest return ids.ToArray(); } + + public Guid[] DepositPost(IEnumerable request, IAmABoxTransactionProvider provider) where T : class, IRequest + { + return DepositPost(request); + } public Task DepositPostAsync(T request, bool continueOnCapturedContext = false, CancellationToken cancellationToken = default) where T : class, IRequest { @@ -103,6 +124,11 @@ public Task DepositPostAsync(T request, bool continueOnCapturedContext return tcs.Task; } + + public Task DepositPostAsync(T request, IAmABoxTransactionProvider provider, bool continueOnCapturedContext = false, CancellationToken cancellationToken = default) where T : class, IRequest + { + return DepositPostAsync(request, continueOnCapturedContext, cancellationToken); + } public async Task DepositPostAsync(IEnumerable requests, bool continueOnCapturedContext = false, CancellationToken cancellationToken = default) where T : class, IRequest @@ -115,6 +141,12 @@ public async Task DepositPostAsync(IEnumerable requests, bool cont return ids.ToArray(); } + + public async Task DepositPostAsync(IEnumerable requests, IAmABoxTransactionProvider provider, bool continueOnCapturedContext = false, + CancellationToken cancellationToken = default) where T : class, IRequest + { + return await DepositPostAsync(requests, continueOnCapturedContext, cancellationToken); + } public void ClearOutbox(params Guid[] posts) { diff --git a/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_posting_a_message.cs b/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_posting_a_message.cs index 36055e26d5..9572d24943 100644 --- a/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_posting_a_message.cs +++ b/tests/Paramore.Brighter.Kafka.Tests/MessagingGateway/When_posting_a_message.cs @@ -24,6 +24,7 @@ THE SOFTWARE. */ #endregion using System; +using System.Collections.Generic; using System.Text.Json; using System.Threading.Tasks; using Confluent.Kafka; @@ -95,7 +96,17 @@ public void When_posting_a_message() var body = JsonSerializer.Serialize(command, JsonSerialisationOptions.Options); var message = new Message( - new MessageHeader(Guid.NewGuid(), _topic, MessageType.MT_COMMAND) { PartitionKey = _partitionKey }, + new MessageHeader(Guid.NewGuid(), _topic, MessageType.MT_COMMAND) + { + PartitionKey = _partitionKey, + ContentType = "application/json", + Bag = new Dictionary{{"Test Header", "Test Value"},}, + ReplyTo = "com.brightercommand.replyto", + CorrelationId = Guid.NewGuid(), + DelayedMilliseconds = 10, + HandledCount = 2, + TimeStamp = DateTime.UtcNow + }, new MessageBody(body)); ((IAmAMessageProducerSync)_producerRegistry.LookupBy(_topic)).Send(message); @@ -108,6 +119,8 @@ public void When_posting_a_message() receivedMessage.Header.PartitionKey.Should().Be(_partitionKey); receivedMessage.Body.Bytes.Should().Equal(message.Body.Bytes); receivedMessage.Body.Value.Should().Be(message.Body.Value); + receivedMessage.Header.TimeStamp.ToString("yyyy-MM-ddTHH:mm:ss.fffZ") + .Should().Be(message.Header.TimeStamp.ToString("yyyy-MM-ddTHH:mm:ss.fffZ")); receivedCommand.Id.Should().Be(command.Id); receivedCommand.Value.Should().Be(command.Value); } diff --git a/tests/Paramore.Brighter.MSSQL.Tests/MsSqlTestHelper.cs b/tests/Paramore.Brighter.MSSQL.Tests/MsSqlTestHelper.cs index b7d4691e6a..a461fb503b 100644 --- a/tests/Paramore.Brighter.MSSQL.Tests/MsSqlTestHelper.cs +++ b/tests/Paramore.Brighter.MSSQL.Tests/MsSqlTestHelper.cs @@ -9,12 +9,13 @@ namespace Paramore.Brighter.MSSQL.Tests { public class MsSqlTestHelper { + private readonly bool _binaryMessagePayload; private string _tableName; private SqlSettings _sqlSettings; - private IMsSqlConnectionProvider _connectionProvider; - private IMsSqlConnectionProvider _masterConnectionProvider; + private IAmARelationalDbConnectionProvider _connectionProvider; + private IAmARelationalDbConnectionProvider _masterConnectionProvider; - private const string _queueDDL = @"CREATE TABLE [dbo].[{0}]( + private const string _textQueueDDL = @"CREATE TABLE [dbo].[{0}]( [Id][bigint] IDENTITY(1, 1) NOT NULL, [Topic] [nvarchar](255) NOT NULL, [MessageType] [nvarchar](1024) NOT NULL, @@ -24,9 +25,31 @@ [Payload] [nvarchar](max)NOT NULL, [Id] ASC )WITH(PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON[PRIMARY] ) ON[PRIMARY] TEXTIMAGE_ON[PRIMARY]"; + + private const string _binaryQueueDDL = @"CREATE TABLE [dbo].[{0}]( + [Id][bigint] IDENTITY(1, 1) NOT NULL, + [Topic] [nvarchar](255) NOT NULL, + [MessageType] [nvarchar](1024) NOT NULL, + [Payload] [varbinary](max)NOT NULL, + CONSTRAINT[PK_QueueData_{1}] PRIMARY KEY CLUSTERED + ( + [Id] ASC + )WITH(PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON[PRIMARY] + ) ON[PRIMARY] TEXTIMAGE_ON[PRIMARY]"; + + public RelationalDatabaseConfiguration InboxConfiguration => + new(_sqlSettings.TestsBrighterConnectionString, inboxTableName: _tableName); - public MsSqlTestHelper() + public RelationalDatabaseConfiguration OutboxConfiguration => + new(_sqlSettings.TestsBrighterConnectionString, outBoxTableName: _tableName, binaryMessagePayload: _binaryMessagePayload); + + public RelationalDatabaseConfiguration QueueConfiguration => + new(_sqlSettings.TestsBrighterConnectionString, queueStoreTable: _tableName); + + + public MsSqlTestHelper(bool binaryMessagePayload = false) { + _binaryMessagePayload = binaryMessagePayload; var builder = new ConfigurationBuilder().AddEnvironmentVariables(); var configuration = builder.Build(); @@ -34,16 +57,17 @@ public MsSqlTestHelper() configuration.GetSection("Sql").Bind(_sqlSettings); _tableName = $"test_{Guid.NewGuid()}"; - - _connectionProvider = new MsSqlSqlAuthConnectionProvider(new MsSqlConfiguration(_sqlSettings.TestsBrighterConnectionString)); - _masterConnectionProvider = new MsSqlSqlAuthConnectionProvider(new MsSqlConfiguration(_sqlSettings.TestsMasterConnectionString)); + + _connectionProvider = + new MsSqlConnectionProvider(new RelationalDatabaseConfiguration(_sqlSettings.TestsBrighterConnectionString)); + _masterConnectionProvider = + new MsSqlConnectionProvider(new RelationalDatabaseConfiguration(_sqlSettings.TestsMasterConnectionString)); } - public void CreateDatabase() - { + public void CreateDatabase() + { using (var connection = _masterConnectionProvider.GetConnection()) { - connection.Open(); using (var command = connection.CreateCommand()) { command.CommandText = @" @@ -74,31 +98,26 @@ public void SetupQueueDb() CreateQueueTable(); } - public MsSqlConfiguration InboxConfiguration => new MsSqlConfiguration(_sqlSettings.TestsBrighterConnectionString, inboxTableName: _tableName); - - public MsSqlConfiguration OutboxConfiguration => new MsSqlConfiguration(_sqlSettings.TestsBrighterConnectionString, outBoxTableName: _tableName); - - public MsSqlConfiguration QueueConfiguration => new MsSqlConfiguration(_sqlSettings.TestsBrighterConnectionString, queueStoreTable: _tableName); - private void CreateQueueTable() { _tableName = $"queue_{_tableName}"; using var connection = _connectionProvider.GetConnection(); - var createTableSql = string.Format(_queueDDL, _tableName, Guid.NewGuid().ToString()); + var ddl = _binaryMessagePayload ? _binaryQueueDDL : _textQueueDDL; + var createTableSql = string.Format(ddl, _tableName, Guid.NewGuid().ToString()); - connection.Open(); using (var command = connection.CreateCommand()) { command.CommandText = createTableSql; command.ExecuteNonQuery(); } + connection.Close(); } + public void CleanUpDb() { using (var connection = _connectionProvider.GetConnection()) { - connection.Open(); using (var command = connection.CreateCommand()) { command.CommandText = $@" @@ -116,9 +135,8 @@ public void CreateOutboxTable() using (var connection = _connectionProvider.GetConnection()) { _tableName = $"[message_{_tableName}]"; - var createTableSql = SqlOutboxBuilder.GetDDL(_tableName); + var createTableSql = SqlOutboxBuilder.GetDDL(_tableName, _binaryMessagePayload); - connection.Open(); using (var command = connection.CreateCommand()) { command.CommandText = createTableSql; @@ -134,7 +152,6 @@ public void CreateInboxTable() _tableName = $"[command_{_tableName}]"; var createTableSql = SqlInboxBuilder.GetDDL(_tableName); - connection.Open(); using (var command = connection.CreateCommand()) { command.CommandText = createTableSql; @@ -147,9 +164,9 @@ public void CreateInboxTable() internal class SqlSettings { public string TestsBrighterConnectionString { get; set; } = - "Server=127.0.0.1,11433;Database=BrighterTests;User Id=sa;Password=Password1!;Application Name=BrighterTests;Connect Timeout=60;Encrypt=false"; + "Server=127.0.0.1,11433;Database=BrighterTests;User Id=sa;Password=Password123!;Application Name=BrighterTests;Connect Timeout=60;Encrypt=false"; public string TestsMasterConnectionString { get; set; } = - "Server=127.0.0.1,11433;Database=master;User Id=sa;Password=Password1!;Application Name=BrighterTests;Connect Timeout=60;Encrypt=false"; + "Server=127.0.0.1,11433;Database=master;User Id=sa;Password=Password123!;Application Name=BrighterTests;Connect Timeout=60;Encrypt=false"; } } diff --git a/tests/Paramore.Brighter.MSSQL.Tests/Outbox/When_there_are_multiple_messages_and_some_are_recievied_and_Dispatched_bulk_Async.cs b/tests/Paramore.Brighter.MSSQL.Tests/Outbox/When_there_are_multiple_messages_and_some_are_recievied_and_Dispatched_bulk_Async.cs index 4911d70fb9..b1010e6550 100644 --- a/tests/Paramore.Brighter.MSSQL.Tests/Outbox/When_there_are_multiple_messages_and_some_are_recievied_and_Dispatched_bulk_Async.cs +++ b/tests/Paramore.Brighter.MSSQL.Tests/Outbox/When_there_are_multiple_messages_and_some_are_recievied_and_Dispatched_bulk_Async.cs @@ -12,8 +12,8 @@ namespace Paramore.Brighter.MSSQL.Tests.Outbox public class MsSqlOutboxBulkGetAsyncTests : IDisposable { private readonly MsSqlTestHelper _msSqlTestHelper; - private readonly string _Topic1 = "test_topic"; - private readonly string _Topic2 = "test_topic3"; + private readonly string _topic1 = "test_topic"; + private readonly string _topic2 = "test_topic3"; private IEnumerable _messages; private readonly Message _message1; private readonly Message _message2; @@ -27,13 +27,13 @@ public MsSqlOutboxBulkGetAsyncTests() _msSqlTestHelper.SetupMessageDb(); _sqlOutbox = new MsSqlOutbox(_msSqlTestHelper.OutboxConfiguration); - _message = new Message(new MessageHeader(Guid.NewGuid(), _Topic1, MessageType.MT_COMMAND), + _message = new Message(new MessageHeader(Guid.NewGuid(), _topic1, MessageType.MT_COMMAND), new MessageBody("message body")); - _message1 = new Message(new MessageHeader(Guid.NewGuid(), _Topic2, MessageType.MT_EVENT), + _message1 = new Message(new MessageHeader(Guid.NewGuid(), _topic2, MessageType.MT_EVENT), new MessageBody("message body2")); - _message2 = new Message(new MessageHeader(Guid.NewGuid(), _Topic1, MessageType.MT_COMMAND), + _message2 = new Message(new MessageHeader(Guid.NewGuid(), _topic1, MessageType.MT_COMMAND), new MessageBody("message body3")); - _message3 = new Message(new MessageHeader(Guid.NewGuid(), _Topic2, MessageType.MT_EVENT), + _message3 = new Message(new MessageHeader(Guid.NewGuid(), _topic2, MessageType.MT_EVENT), new MessageBody("message body4")); } diff --git a/tests/Paramore.Brighter.MSSQL.Tests/Outbox/When_there_are_multiple_messages_in_the_message_store_and_a_range_is_fetched.cs b/tests/Paramore.Brighter.MSSQL.Tests/Outbox/When_there_are_multiple_messages_in_the_message_store_and_a_range_is_fetched.cs index f16dd55a13..d6139325fe 100644 --- a/tests/Paramore.Brighter.MSSQL.Tests/Outbox/When_there_are_multiple_messages_in_the_message_store_and_a_range_is_fetched.cs +++ b/tests/Paramore.Brighter.MSSQL.Tests/Outbox/When_there_are_multiple_messages_in_the_message_store_and_a_range_is_fetched.cs @@ -37,10 +37,14 @@ namespace Paramore.Brighter.MSSQL.Tests.Outbox public class MsSqlOutboxRangeRequestTests : IDisposable { private readonly MsSqlTestHelper _msSqlTestHelper; - private readonly string _TopicFirstMessage = "test_topic"; - private readonly string _TopicLastMessage = "test_topic3"; + private readonly string _testTopicOne = "test_topic"; + private string _testTopicTwo = "test_topic2"; + private readonly string _testTopicThree = "test_topic3"; private IEnumerable _messages; private readonly MsSqlOutbox _sqlOutbox; + private readonly Message _messageOne; + private readonly Message _messageTwo; + private readonly Message _messageThree; public MsSqlOutboxRangeRequestTests() { @@ -48,14 +52,14 @@ public MsSqlOutboxRangeRequestTests() _msSqlTestHelper.SetupMessageDb(); _sqlOutbox = new MsSqlOutbox(_msSqlTestHelper.OutboxConfiguration); - var messageEarliest = new Message(new MessageHeader(Guid.NewGuid(), _TopicFirstMessage, MessageType.MT_DOCUMENT), new MessageBody("message body")); - var message1 = new Message(new MessageHeader(Guid.NewGuid(), "test_topic2", MessageType.MT_DOCUMENT), new MessageBody("message body2")); - var message2 = new Message(new MessageHeader(Guid.NewGuid(), _TopicLastMessage, MessageType.MT_DOCUMENT), new MessageBody("message body3")); - _sqlOutbox.Add(messageEarliest); + _messageOne = new Message(new MessageHeader(Guid.NewGuid(), _testTopicOne, MessageType.MT_DOCUMENT), new MessageBody("message body")); + _messageTwo = new Message(new MessageHeader(Guid.NewGuid(), _testTopicTwo, MessageType.MT_DOCUMENT), new MessageBody("message body2")); + _messageThree = new Message(new MessageHeader(Guid.NewGuid(), _testTopicThree, MessageType.MT_DOCUMENT), new MessageBody("message body3")); + _sqlOutbox.Add(_messageOne); Task.Delay(100); - _sqlOutbox.Add(message1); + _sqlOutbox.Add(_messageTwo); Task.Delay(100); - _sqlOutbox.Add(message2); + _sqlOutbox.Add(_messageThree); } [Fact] @@ -66,7 +70,7 @@ public void When_There_Are_Multiple_Messages_In_The_Outbox_And_A_Range_Is_Fetche //should fetch 1 message _messages.Should().HaveCount(1); //should fetch expected message - _messages.First().Header.Topic.Should().Be(_TopicLastMessage); + _messages.First().Header.Topic.Should().Be(_messageThree.Header.Topic); //should not fetch null messages _messages.Should().NotBeNull(); } diff --git a/tests/Paramore.Brighter.MSSQL.Tests/Outbox/When_there_are_multiple_messages_in_the_message_store_and_a_range_is_fetched_async.cs b/tests/Paramore.Brighter.MSSQL.Tests/Outbox/When_there_are_multiple_messages_in_the_message_store_and_a_range_is_fetched_async.cs index 9b5548b41a..5556b2d0ca 100644 --- a/tests/Paramore.Brighter.MSSQL.Tests/Outbox/When_there_are_multiple_messages_in_the_message_store_and_a_range_is_fetched_async.cs +++ b/tests/Paramore.Brighter.MSSQL.Tests/Outbox/When_there_are_multiple_messages_in_the_message_store_and_a_range_is_fetched_async.cs @@ -37,12 +37,13 @@ namespace Paramore.Brighter.MSSQL.Tests.Outbox public class MsSqlOutboxRangeRequestAsyncTests : IDisposable { private readonly MsSqlTestHelper _msSqlTestHelper; - private readonly string _TopicFirstMessage = "test_topic"; - private readonly string _TopicLastMessage = "test_topic3"; + private readonly string _testTopicOne = "test_topic"; + private string _testTopicTwo = "test_topic2"; + private readonly string _testTopicThree = "test_topic3"; private IEnumerable _messages; - private readonly Message _message1; - private readonly Message _message2; - private readonly Message _messageEarliest; + private readonly Message _messageTwo; + private readonly Message _messageThree; + private readonly Message _messageOne; private readonly MsSqlOutbox _sqlOutbox; public MsSqlOutboxRangeRequestAsyncTests() @@ -51,26 +52,26 @@ public MsSqlOutboxRangeRequestAsyncTests() _msSqlTestHelper.SetupMessageDb(); _sqlOutbox = new MsSqlOutbox(_msSqlTestHelper.OutboxConfiguration); - _messageEarliest = new Message(new MessageHeader(Guid.NewGuid(), _TopicFirstMessage, MessageType.MT_DOCUMENT), new MessageBody("message body")); - _message1 = new Message(new MessageHeader(Guid.NewGuid(), "test_topic2", MessageType.MT_DOCUMENT), new MessageBody("message body2")); - _message2 = new Message(new MessageHeader(Guid.NewGuid(), _TopicLastMessage, MessageType.MT_DOCUMENT), new MessageBody("message body3")); + _messageOne = new Message(new MessageHeader(Guid.NewGuid(), _testTopicOne, MessageType.MT_DOCUMENT), new MessageBody("message body")); + _messageTwo = new Message(new MessageHeader(Guid.NewGuid(), _testTopicTwo, MessageType.MT_DOCUMENT), new MessageBody("message body2")); + _messageThree = new Message(new MessageHeader(Guid.NewGuid(), _testTopicThree, MessageType.MT_DOCUMENT), new MessageBody("message body3")); } [Fact] public async Task When_There_Are_Multiple_Messages_In_The_Outbox_And_A_Range_Is_Fetched_Async() { - await _sqlOutbox.AddAsync(_messageEarliest); + await _sqlOutbox.AddAsync(_messageOne); await Task.Delay(100); - await _sqlOutbox.AddAsync(_message1); + await _sqlOutbox.AddAsync(_messageTwo); await Task.Delay(100); - await _sqlOutbox.AddAsync(_message2); + await _sqlOutbox.AddAsync(_messageThree); _messages = await _sqlOutbox.GetAsync(1, 3); //should fetch 1 message _messages.Should().HaveCount(1); //should fetch expected message - _messages.First().Header.Topic.Should().Be(_TopicLastMessage); + _messages.First().Header.Topic.Should().Be(_messageThree.Header.Topic); //should not fetch null messages _messages.Should().NotBeNull(); } diff --git a/tests/Paramore.Brighter.MSSQL.Tests/Outbox/When_writing_a_message_to_a_binary_body_message_store.cs b/tests/Paramore.Brighter.MSSQL.Tests/Outbox/When_writing_a_message_to_a_binary_body_message_store.cs new file mode 100644 index 0000000000..d2d97dc0fa --- /dev/null +++ b/tests/Paramore.Brighter.MSSQL.Tests/Outbox/When_writing_a_message_to_a_binary_body_message_store.cs @@ -0,0 +1,107 @@ +using System; +using FluentAssertions; +using Paramore.Brighter.Outbox.MsSql; +using Xunit; + +namespace Paramore.Brighter.MSSQL.Tests.Outbox +{ + [Trait("Category", "MSSQL")] + public class SqlBinaryPayloadOutboxWritingMessageTests + { + private readonly string _key1 = "name1"; + private readonly string _key2 = "name2"; + private readonly string _key3 = "name3"; + private readonly string _key4 = "name4"; + private readonly string _key5 = "name5"; + private Message _message; + private readonly MsSqlOutbox _sqlOutbox; + private Message _storedMessage; + private readonly string _value1 = "value1"; + private readonly string _value2 = "value2"; + private readonly int _value3 = 123; + private readonly Guid _value4 = Guid.NewGuid(); + private readonly DateTime _value5 = DateTime.UtcNow; + private readonly MsSqlTestHelper _msSqlTestHelper; + private readonly MessageHeader _messageHeader; + + public SqlBinaryPayloadOutboxWritingMessageTests() + { + _msSqlTestHelper = new MsSqlTestHelper(binaryMessagePayload: true); + _msSqlTestHelper.SetupMessageDb(); + + _sqlOutbox = new MsSqlOutbox(_msSqlTestHelper.OutboxConfiguration); + _messageHeader = new MessageHeader( + messageId: Guid.NewGuid(), + topic: "test_topic", + messageType: MessageType.MT_DOCUMENT, + timeStamp: DateTime.UtcNow.AddDays(-1), + handledCount: 5, + delayedMilliseconds: 5, + correlationId: Guid.NewGuid(), + replyTo: "ReplyAddress", + contentType: "application/octet-stream", + partitionKey: "123456789"); + _messageHeader.Bag.Add(_key1, _value1); + _messageHeader.Bag.Add(_key2, _value2); + _messageHeader.Bag.Add(_key3, _value3); + _messageHeader.Bag.Add(_key4, _value4); + _messageHeader.Bag.Add(_key5, _value5); + } + + [Fact] + public void When_Writing_A_Message_To_The_MSSQL_Outbox() + { + _message = new Message(_messageHeader, + new MessageBody(new byte[] { 1, 2, 3, 4, 5 }, "application/octet-stream", CharacterEncoding.Raw)); + _sqlOutbox.Add(_message); + + AssertMessage(); + } + + [Fact] + public void When_Writing_A_Message_With_a_Null_To_The_MSSQL_Outbox() + { + _message = new Message(_messageHeader, new MessageBody((byte[])null)); + _sqlOutbox.Add(_message); + + AssertMessage(); + } + + private void AssertMessage() + { + _storedMessage = _sqlOutbox.Get(_message.Id); + + //should read the message from the sql outbox + _storedMessage.Body.Bytes.Should().Equal(_message.Body.Bytes); + //should read the header from the sql outbox + _storedMessage.Header.Topic.Should().Be(_message.Header.Topic); + _storedMessage.Header.MessageType.Should().Be(_message.Header.MessageType); + _storedMessage.Header.TimeStamp.ToString("yyyy-MM-ddTHH:mm:ss.fZ") + .Should().Be(_message.Header.TimeStamp.ToString("yyyy-MM-ddTHH:mm:ss.fZ")); + _storedMessage.Header.HandledCount.Should().Be(0); // -- should be zero when read from outbox + _storedMessage.Header.DelayedMilliseconds.Should().Be(0); // -- should be zero when read from outbox + _storedMessage.Header.CorrelationId.Should().Be(_message.Header.CorrelationId); + _storedMessage.Header.ReplyTo.Should().Be(_message.Header.ReplyTo); + _storedMessage.Header.ContentType.Should().Be(_message.Header.ContentType); + _storedMessage.Header.PartitionKey.Should().Be(_message.Header.PartitionKey); + + + //Bag serialization + _storedMessage.Header.Bag.ContainsKey(_key1).Should().BeTrue(); + _storedMessage.Header.Bag[_key1].Should().Be(_value1); + _storedMessage.Header.Bag.ContainsKey(_key2).Should().BeTrue(); + _storedMessage.Header.Bag[_key2].Should().Be(_value2); + _storedMessage.Header.Bag.ContainsKey(_key3).Should().BeTrue(); + _storedMessage.Header.Bag[_key3].Should().Be(_value3); + _storedMessage.Header.Bag.ContainsKey(_key4).Should().BeTrue(); + _storedMessage.Header.Bag[_key4].Should().Be(_value4); + _storedMessage.Header.Bag.ContainsKey(_key5).Should().BeTrue(); + _storedMessage.Header.Bag[_key5].Should().Be(_value5); + } + + public void Dispose() + { + _msSqlTestHelper.CleanUpDb(); + } + } +} diff --git a/tests/Paramore.Brighter.MSSQL.Tests/Outbox/When_writing_a_message_to_the_message_store.cs b/tests/Paramore.Brighter.MSSQL.Tests/Outbox/When_writing_a_message_to_the_message_store.cs index 04f1a23eb8..041ab94dbc 100644 --- a/tests/Paramore.Brighter.MSSQL.Tests/Outbox/When_writing_a_message_to_the_message_store.cs +++ b/tests/Paramore.Brighter.MSSQL.Tests/Outbox/When_writing_a_message_to_the_message_store.cs @@ -64,7 +64,8 @@ public SqlOutboxWritingMessageTests() delayedMilliseconds:5, correlationId: Guid.NewGuid(), replyTo: "ReplyAddress", - contentType: "text/plain"); + contentType: "text/plain", + partitionKey: Guid.NewGuid().ToString()); _messageHeader.Bag.Add(_key1, _value1); _messageHeader.Bag.Add(_key2, _value2); _messageHeader.Bag.Add(_key3, _value3); @@ -84,7 +85,7 @@ public void When_Writing_A_Message_To_The_MSSQL_Outbox() [Fact] public void When_Writing_A_Message_With_a_Null_To_The_MSSQL_Outbox() { - _message = new Message(_messageHeader, null); + _message = new Message(_messageHeader, new MessageBody((byte[])null)); _sqlOutbox.Add(_message); AssertMessage(); @@ -95,19 +96,18 @@ private void AssertMessage() _storedMessage = _sqlOutbox.Get(_message.Id); //should read the message from the sql outbox - if (!string.IsNullOrEmpty(_storedMessage.Body.Value)) - _storedMessage.Body.Value.Should().Be(_message.Body.Value); - else - Assert.Null(_message.Body); + _storedMessage.Body.Value.Should().Be(_message.Body.Value); //should read the header from the sql outbox _storedMessage.Header.Topic.Should().Be(_message.Header.Topic); _storedMessage.Header.MessageType.Should().Be(_message.Header.MessageType); - _storedMessage.Header.TimeStamp.Should().Be(_message.Header.TimeStamp); + _storedMessage.Header.TimeStamp.ToString("yyyy-MM-ddTHH:mm:ss.fZ") + .Should().Be(_message.Header.TimeStamp.ToString("yyyy-MM-ddTHH:mm:ss.fZ")); _storedMessage.Header.HandledCount.Should().Be(0); // -- should be zero when read from outbox _storedMessage.Header.DelayedMilliseconds.Should().Be(0); // -- should be zero when read from outbox _storedMessage.Header.CorrelationId.Should().Be(_message.Header.CorrelationId); _storedMessage.Header.ReplyTo.Should().Be(_message.Header.ReplyTo); _storedMessage.Header.ContentType.Should().Be(_message.Header.ContentType); + _storedMessage.Header.PartitionKey.Should().Be(_message.Header.PartitionKey); //Bag serialization diff --git a/tests/Paramore.Brighter.MSSQL.Tests/Outbox/When_writing_a_message_to_the_message_store_async.cs b/tests/Paramore.Brighter.MSSQL.Tests/Outbox/When_writing_a_message_to_the_message_store_async.cs index 0fd5f372fc..52fee42f9d 100644 --- a/tests/Paramore.Brighter.MSSQL.Tests/Outbox/When_writing_a_message_to_the_message_store_async.cs +++ b/tests/Paramore.Brighter.MSSQL.Tests/Outbox/When_writing_a_message_to_the_message_store_async.cs @@ -85,7 +85,8 @@ public async Task When_Writing_A_Message_To_The_Outbox_Async() //should read the header from the sql outbox _storedMessage.Header.Topic.Should().Be(_message.Header.Topic); _storedMessage.Header.MessageType.Should().Be(_message.Header.MessageType); - _storedMessage.Header.TimeStamp.Should().Be(_message.Header.TimeStamp); + _storedMessage.Header.TimeStamp.ToString("yyyy-MM-ddTHH:mm:ss.fZ") + .Should().Be(_message.Header.TimeStamp.ToString("yyyy-MM-ddTHH:mm:ss.fZ")); _storedMessage.Header.HandledCount.Should().Be(0); // -- should be zero when read from outbox _storedMessage.Header.DelayedMilliseconds.Should().Be(0); // -- should be zero when read from outbox _storedMessage.Header.CorrelationId.Should().Be(_message.Header.CorrelationId); diff --git a/tests/Paramore.Brighter.MSSQL.Tests/Outbox/When_writing_messages_to_the_message_store_async.cs b/tests/Paramore.Brighter.MSSQL.Tests/Outbox/When_writing_messages_to_the_message_store_async.cs index 895c8898e2..44bc70ce22 100644 --- a/tests/Paramore.Brighter.MSSQL.Tests/Outbox/When_writing_messages_to_the_message_store_async.cs +++ b/tests/Paramore.Brighter.MSSQL.Tests/Outbox/When_writing_messages_to_the_message_store_async.cs @@ -37,9 +37,9 @@ namespace Paramore.Brighter.MSSQL.Tests.Outbox public class SqlOutboxWritingMessagesAsyncTests : IDisposable { private readonly MsSqlTestHelper _msSqlTestHelper; - private Message _message2; - private Message _messageEarliest; - private Message _messageLatest; + private Message _messageTwo; + private Message _messageOne; + private Message _messageThree; private IList _retrievedMessages; private readonly MsSqlOutbox _sqlOutbox; @@ -58,10 +58,10 @@ public async Task When_Writing_Messages_To_The_Outbox_Async() _retrievedMessages = await _sqlOutbox.GetAsync(); - //should read first message last from the outbox - _retrievedMessages.Last().Id.Should().Be(_messageEarliest.Id); - //should read last message first from the outbox - _retrievedMessages.First().Id.Should().Be(_messageLatest.Id); + //should read last message last from the outbox + _retrievedMessages.Last().Id.Should().Be(_messageThree.Id); + //should read first message first from the outbox + _retrievedMessages.First().Id.Should().Be(_messageOne.Id); //should read the messages from the outbox _retrievedMessages.Should().HaveCount(3); } @@ -69,33 +69,33 @@ public async Task When_Writing_Messages_To_The_Outbox_Async() [Fact] public async Task When_Bulk_Writing_Messages_To_The_Outbox_Async() { - _messageEarliest = new Message(new MessageHeader(Guid.NewGuid(), "Test", MessageType.MT_COMMAND, DateTime.UtcNow.AddHours(-3)), new MessageBody("Body")); - _message2 = new Message(new MessageHeader(Guid.NewGuid(), "Test2", MessageType.MT_COMMAND, DateTime.UtcNow.AddHours(-2)), new MessageBody("Body2")); - _messageLatest = new Message(new MessageHeader(Guid.NewGuid(), "Test3", MessageType.MT_COMMAND, DateTime.UtcNow.AddHours(-1)), new MessageBody("Body3")); + _messageOne = new Message(new MessageHeader(Guid.NewGuid(), "Test", MessageType.MT_COMMAND, DateTime.UtcNow.AddHours(-3)), new MessageBody("Body")); + _messageTwo = new Message(new MessageHeader(Guid.NewGuid(), "Test2", MessageType.MT_COMMAND, DateTime.UtcNow.AddHours(-2)), new MessageBody("Body2")); + _messageThree = new Message(new MessageHeader(Guid.NewGuid(), "Test3", MessageType.MT_COMMAND, DateTime.UtcNow.AddHours(-1)), new MessageBody("Body3")); - var messages = new List { _messageEarliest, _message2, _messageLatest }; + var messages = new List { _messageOne, _messageTwo, _messageThree }; await _sqlOutbox.AddAsync(messages); _retrievedMessages = await _sqlOutbox.GetAsync(); - //should read first message last from the outbox - _retrievedMessages.Last().Id.Should().Be(_messageEarliest.Id); - //should read last message first from the outbox - _retrievedMessages.First().Id.Should().Be(_messageLatest.Id); + //should read last message last from the outbox + _retrievedMessages.Last().Id.Should().Be(_messageThree.Id); + //should read first message first from the outbox + _retrievedMessages.First().Id.Should().Be(_messageOne.Id); //should read the messages from the outbox _retrievedMessages.Should().HaveCount(3); } private async Task SetUpMessagesAsync() { - _messageEarliest = new Message(new MessageHeader(Guid.NewGuid(), "Test", MessageType.MT_COMMAND, DateTime.UtcNow.AddHours(-3)), new MessageBody("Body")); - await _sqlOutbox.AddAsync(_messageEarliest); + _messageOne = new Message(new MessageHeader(Guid.NewGuid(), "Test", MessageType.MT_COMMAND, DateTime.UtcNow.AddHours(-3)), new MessageBody("Body")); + await _sqlOutbox.AddAsync(_messageOne); - _message2 = new Message(new MessageHeader(Guid.NewGuid(), "Test2", MessageType.MT_COMMAND, DateTime.UtcNow.AddHours(-2)), new MessageBody("Body2")); - await _sqlOutbox.AddAsync(_message2); + _messageTwo = new Message(new MessageHeader(Guid.NewGuid(), "Test2", MessageType.MT_COMMAND, DateTime.UtcNow.AddHours(-2)), new MessageBody("Body2")); + await _sqlOutbox.AddAsync(_messageTwo); - _messageLatest = new Message(new MessageHeader(Guid.NewGuid(), "Test3", MessageType.MT_COMMAND, DateTime.UtcNow.AddHours(-1)), new MessageBody("Body3")); - await _sqlOutbox.AddAsync(_messageLatest); + _messageThree = new Message(new MessageHeader(Guid.NewGuid(), "Test3", MessageType.MT_COMMAND, DateTime.UtcNow.AddHours(-1)), new MessageBody("Body3")); + await _sqlOutbox.AddAsync(_messageThree); } private void Release() diff --git a/tests/Paramore.Brighter.MySQL.Tests/MySqlTestHelper.cs b/tests/Paramore.Brighter.MySQL.Tests/MySqlTestHelper.cs index 2fa7c781c8..4eddaeedf4 100644 --- a/tests/Paramore.Brighter.MySQL.Tests/MySqlTestHelper.cs +++ b/tests/Paramore.Brighter.MySQL.Tests/MySqlTestHelper.cs @@ -9,11 +9,19 @@ namespace Paramore.Brighter.MySQL.Tests { public class MySqlTestHelper { + private readonly bool _binaryMessagePayload; private string _tableName; private MySqlSettings _mysqlSettings; + + public RelationalDatabaseConfiguration InboxConfiguration => + new(_mysqlSettings.TestsBrighterConnectionString, inboxTableName: _tableName); - public MySqlTestHelper() + public RelationalDatabaseConfiguration OutboxConfiguration => + new(_mysqlSettings.TestsBrighterConnectionString, outBoxTableName: _tableName, binaryMessagePayload: _binaryMessagePayload); + + public MySqlTestHelper(bool binaryMessagePayload = false) { + _binaryMessagePayload = binaryMessagePayload; var builder = new ConfigurationBuilder().AddEnvironmentVariables(); var configuration = builder.Build(); @@ -23,7 +31,7 @@ public MySqlTestHelper() _tableName = $"test_{Guid.NewGuid()}"; } - public void CreateDatabase() + public void CreateDatabase() { using (var connection = new MySqlConnection(_mysqlSettings.TestsMasterConnectionString)) { @@ -48,11 +56,7 @@ public void SetupCommandDb() CreateInboxTable(); } - public MySqlInboxConfiguration InboxConfiguration => new MySqlInboxConfiguration(_mysqlSettings.TestsBrighterConnectionString, _tableName); - - public MySqlConfiguration OutboxConfiguration => new MySqlConfiguration(_mysqlSettings.TestsBrighterConnectionString, _tableName); - - public void CleanUpDb() + public void CleanUpDb() { using (var connection = new MySqlConnection(_mysqlSettings.TestsBrighterConnectionString)) { @@ -70,7 +74,8 @@ public void CreateOutboxTable() using (var connection = new MySqlConnection(_mysqlSettings.TestsBrighterConnectionString)) { _tableName = $"`message_{_tableName}`"; - var createTableSql = MySqlOutboxBuilder.GetDDL(_tableName); + var createTableSql = + MySqlOutboxBuilder.GetDDL(_tableName, hasBinaryMessagePayload: _binaryMessagePayload); connection.Open(); using (var command = connection.CreateCommand()) @@ -100,7 +105,9 @@ public void CreateInboxTable() internal class MySqlSettings { - public string TestsBrighterConnectionString { get; set; } = "Server=localhost;Uid=root;Pwd=root;Database=BrighterTests"; + public string TestsBrighterConnectionString { get; set; } = + "Server=localhost;Uid=root;Pwd=root;Database=BrighterTests"; + public string TestsMasterConnectionString { get; set; } = "Server=localhost;Uid=root;Pwd=root;"; } } diff --git a/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_removing_messages_from_the_outbox.cs b/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_removing_messages_from_the_outbox.cs index 883deeaa89..9ae392f8d6 100644 --- a/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_removing_messages_from_the_outbox.cs +++ b/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_removing_messages_from_the_outbox.cs @@ -38,7 +38,7 @@ namespace Paramore.Brighter.MySQL.Tests.Outbox public class MySqlOutboxDeletingMessagesTests { private readonly MySqlTestHelper _mySqlTestHelper; - private readonly MySqlOutboxSync _mySqlOutboxSync; + private readonly MySqlOutbox _mySqlOutbox; private readonly Message _messageEarliest; private readonly Message _message2; private readonly Message _messageLatest; @@ -48,7 +48,7 @@ public MySqlOutboxDeletingMessagesTests() { _mySqlTestHelper = new MySqlTestHelper(); _mySqlTestHelper.SetupMessageDb(); - _mySqlOutboxSync = new MySqlOutboxSync(_mySqlTestHelper.OutboxConfiguration); + _mySqlOutbox = new MySqlOutbox(_mySqlTestHelper.OutboxConfiguration); _messageEarliest = new Message(new MessageHeader(Guid.NewGuid(), "Test", MessageType.MT_COMMAND, DateTime.UtcNow.AddHours(-3)), new MessageBody("Body")); _message2 = new Message(new MessageHeader(Guid.NewGuid(), "Test2", MessageType.MT_COMMAND, DateTime.UtcNow.AddHours(-2)), new MessageBody("Body2")); @@ -59,22 +59,22 @@ public MySqlOutboxDeletingMessagesTests() [Fact] public void When_Removing_Messages_From_The_Outbox() { - _mySqlOutboxSync.Add(_messageEarliest); - _mySqlOutboxSync.Add(_message2); - _mySqlOutboxSync.Add(_messageLatest); - _retrievedMessages = _mySqlOutboxSync.Get(); + _mySqlOutbox.Add(_messageEarliest); + _mySqlOutbox.Add(_message2); + _mySqlOutbox.Add(_messageLatest); + _retrievedMessages = _mySqlOutbox.Get(); - _mySqlOutboxSync.Delete(_retrievedMessages.First().Id); + _mySqlOutbox.Delete(_retrievedMessages.First().Id); - var remainingMessages = _mySqlOutboxSync.Get(); + var remainingMessages = _mySqlOutbox.Get(); remainingMessages.Should().HaveCount(2); remainingMessages.Should().Contain(_retrievedMessages.ToList()[1]); remainingMessages.Should().Contain(_retrievedMessages.ToList()[2]); - _mySqlOutboxSync.Delete(remainingMessages.Select(m => m.Id).ToArray()); + _mySqlOutbox.Delete(remainingMessages.Select(m => m.Id).ToArray()); - var messages = _mySqlOutboxSync.Get(); + var messages = _mySqlOutbox.Get(); messages.Should().HaveCount(0); } @@ -83,20 +83,20 @@ public void When_Removing_Messages_From_The_Outbox() public async Task When_Removing_Messages_From_The_OutboxAsync() { var messages = new List { _messageEarliest, _message2, _messageLatest }; - _mySqlOutboxSync.Add(messages); - _retrievedMessages = await _mySqlOutboxSync.GetAsync(); + _mySqlOutbox.Add(messages); + _retrievedMessages = await _mySqlOutbox.GetAsync(); - await _mySqlOutboxSync.DeleteAsync(CancellationToken.None, _retrievedMessages.First().Id); + await _mySqlOutbox.DeleteAsync(CancellationToken.None, _retrievedMessages.First().Id); - var remainingMessages = await _mySqlOutboxSync.GetAsync(); + var remainingMessages = await _mySqlOutbox.GetAsync(); remainingMessages.Should().HaveCount(2); remainingMessages.Should().Contain(_retrievedMessages.ToList()[1]); remainingMessages.Should().Contain(_retrievedMessages.ToList()[2]); - await _mySqlOutboxSync.DeleteAsync(CancellationToken.None, remainingMessages.Select(m => m.Id).ToArray()); + await _mySqlOutbox.DeleteAsync(CancellationToken.None, remainingMessages.Select(m => m.Id).ToArray()); - var finalMessages = await _mySqlOutboxSync.GetAsync(); + var finalMessages = await _mySqlOutbox.GetAsync(); finalMessages.Should().HaveCount(0); } diff --git a/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_the_message_is_already_in_the_outbox.cs b/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_the_message_is_already_in_the_outbox.cs index b26096a344..42f0e8cf00 100644 --- a/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_the_message_is_already_in_the_outbox.cs +++ b/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_the_message_is_already_in_the_outbox.cs @@ -35,7 +35,7 @@ public class MySqlOutboxMessageAlreadyExistsTests : IDisposable { private Exception _exception; private readonly Message _messageEarliest; - private readonly MySqlOutboxSync _mySqlOutboxSync; + private readonly MySqlOutbox _mySqlOutbox; private readonly MySqlTestHelper _mySqlTestHelper; public MySqlOutboxMessageAlreadyExistsTests() @@ -43,15 +43,15 @@ public MySqlOutboxMessageAlreadyExistsTests() _mySqlTestHelper = new MySqlTestHelper(); _mySqlTestHelper.SetupMessageDb(); - _mySqlOutboxSync = new MySqlOutboxSync(_mySqlTestHelper.OutboxConfiguration); + _mySqlOutbox = new MySqlOutbox(_mySqlTestHelper.OutboxConfiguration); _messageEarliest = new Message(new MessageHeader(Guid.NewGuid(), "test_topic", MessageType.MT_DOCUMENT), new MessageBody("message body")); - _mySqlOutboxSync.Add(_messageEarliest); + _mySqlOutbox.Add(_messageEarliest); } [Fact] public void When_The_Message_Is_Already_In_The_Outbox() { - _exception = Catch.Exception(() => _mySqlOutboxSync.Add(_messageEarliest)); + _exception = Catch.Exception(() => _mySqlOutbox.Add(_messageEarliest)); _exception.Should().BeNull(); } diff --git a/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_the_message_is_already_in_the_outbox_async.cs b/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_the_message_is_already_in_the_outbox_async.cs index 585bb7f5af..2f55781d96 100644 --- a/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_the_message_is_already_in_the_outbox_async.cs +++ b/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_the_message_is_already_in_the_outbox_async.cs @@ -36,7 +36,7 @@ public class MySqlOutboxMessageAlreadyExistsAsyncTests : IDisposable { private Exception _exception; private readonly Message _messageEarliest; - private readonly MySqlOutboxSync _sqlOutboxSync; + private readonly MySqlOutbox _sqlOutbox; private readonly MySqlTestHelper _msSqlTestHelper; public MySqlOutboxMessageAlreadyExistsAsyncTests() @@ -44,15 +44,15 @@ public MySqlOutboxMessageAlreadyExistsAsyncTests() _msSqlTestHelper = new MySqlTestHelper(); _msSqlTestHelper.SetupMessageDb(); - _sqlOutboxSync = new MySqlOutboxSync(_msSqlTestHelper.OutboxConfiguration); + _sqlOutbox = new MySqlOutbox(_msSqlTestHelper.OutboxConfiguration); _messageEarliest = new Message(new MessageHeader(Guid.NewGuid(), "test_topic", MessageType.MT_DOCUMENT), new MessageBody("message body")); } [Fact] public async Task When_The_Message_Is_Already_In_The_Outbox_Async() { - await _sqlOutboxSync.AddAsync(_messageEarliest); - _exception = await Catch.ExceptionAsync(() => _sqlOutboxSync.AddAsync(_messageEarliest)); + await _sqlOutbox.AddAsync(_messageEarliest); + _exception = await Catch.ExceptionAsync(() => _sqlOutbox.AddAsync(_messageEarliest)); _exception.Should().BeNull(); } diff --git a/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_there_are_multiple_messages_and_some_are_receivied_and_Dispatched_bulk_Async.cs b/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_there_are_multiple_messages_and_some_are_receivied_and_Dispatched_bulk_Async.cs index a88003b04d..bdd19bca7c 100644 --- a/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_there_are_multiple_messages_and_some_are_receivied_and_Dispatched_bulk_Async.cs +++ b/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_there_are_multiple_messages_and_some_are_receivied_and_Dispatched_bulk_Async.cs @@ -19,14 +19,14 @@ public class MySqlOutboxBulkAsyncTests : IDisposable private readonly Message _message2; private readonly Message _message3; private readonly Message _message; - private readonly MySqlOutboxSync _sqlOutbox; + private readonly MySqlOutbox _sqlOutbox; public MySqlOutboxBulkAsyncTests() { _mySqlTestHelper = new MySqlTestHelper(); _mySqlTestHelper.SetupMessageDb(); - _sqlOutbox = new MySqlOutboxSync(_mySqlTestHelper.OutboxConfiguration); + _sqlOutbox = new MySqlOutbox(_mySqlTestHelper.OutboxConfiguration); _message = new Message(new MessageHeader(Guid.NewGuid(), _Topic1, MessageType.MT_COMMAND), new MessageBody("message body")); _message1 = new Message(new MessageHeader(Guid.NewGuid(), _Topic2, MessageType.MT_EVENT), diff --git a/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_there_are_multiple_messages_in_the_message_store_and_a_range_is_fetched_async.cs b/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_there_are_multiple_messages_in_the_message_store_and_a_range_is_fetched_async.cs index 28e4effcab..ac09b6187c 100644 --- a/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_there_are_multiple_messages_in_the_message_store_and_a_range_is_fetched_async.cs +++ b/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_there_are_multiple_messages_in_the_message_store_and_a_range_is_fetched_async.cs @@ -37,7 +37,7 @@ namespace Paramore.Brighter.MySQL.Tests.Outbox public class MySqlOutboxRangeRequestAsyncTests : IDisposable { private readonly MySqlTestHelper _mySqlTestHelper; - private readonly MySqlOutboxSync _mySqlOutboxSync; + private readonly MySqlOutbox _mySqlOutbox; private readonly string _TopicFirstMessage = "test_topic"; private readonly string _TopicLastMessage = "test_topic3"; private IEnumerable _messages; @@ -49,7 +49,7 @@ public MySqlOutboxRangeRequestAsyncTests() { _mySqlTestHelper = new MySqlTestHelper(); _mySqlTestHelper.SetupMessageDb(); - _mySqlOutboxSync = new MySqlOutboxSync(_mySqlTestHelper.OutboxConfiguration); + _mySqlOutbox = new MySqlOutbox(_mySqlTestHelper.OutboxConfiguration); _messageEarliest = new Message(new MessageHeader(Guid.NewGuid(), _TopicFirstMessage, MessageType.MT_DOCUMENT), new MessageBody("message body")); _message1 = new Message(new MessageHeader(Guid.NewGuid(), "test_topic2", MessageType.MT_DOCUMENT), new MessageBody("message body2")); _message2 = new Message(new MessageHeader(Guid.NewGuid(), _TopicLastMessage, MessageType.MT_DOCUMENT), new MessageBody("message body3")); @@ -58,13 +58,13 @@ public MySqlOutboxRangeRequestAsyncTests() [Fact] public async Task When_There_Are_Multiple_Messages_In_The_Outbox_And_A_Range_Is_Fetched_Async() { - await _mySqlOutboxSync.AddAsync(_messageEarliest); + await _mySqlOutbox.AddAsync(_messageEarliest); await Task.Delay(100); - await _mySqlOutboxSync.AddAsync(_message1); + await _mySqlOutbox.AddAsync(_message1); await Task.Delay(100); - await _mySqlOutboxSync.AddAsync(_message2); + await _mySqlOutbox.AddAsync(_message2); - _messages = await _mySqlOutboxSync.GetAsync(1, 3); + _messages = await _mySqlOutbox.GetAsync(1, 3); //should fetch 1 message _messages.Should().HaveCount(1); diff --git a/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_there_are_multiple_messages_in_the_outbox_and_a_range_is_fetched.cs b/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_there_are_multiple_messages_in_the_outbox_and_a_range_is_fetched.cs index cc66a45ec2..f8f1f53573 100644 --- a/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_there_are_multiple_messages_in_the_outbox_and_a_range_is_fetched.cs +++ b/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_there_are_multiple_messages_in_the_outbox_and_a_range_is_fetched.cs @@ -37,7 +37,7 @@ namespace Paramore.Brighter.MySQL.Tests.Outbox public class MySqlOutboxRangeRequestTests : IDisposable { private readonly MySqlTestHelper _mySqlTestHelper; - private readonly MySqlOutboxSync _mySqlOutboxSync; + private readonly MySqlOutbox _mySqlOutbox; private readonly string _TopicFirstMessage = "test_topic"; private readonly string _TopicLastMessage = "test_topic3"; private IEnumerable messages; @@ -49,22 +49,22 @@ public MySqlOutboxRangeRequestTests() { _mySqlTestHelper = new MySqlTestHelper(); _mySqlTestHelper.SetupMessageDb(); - _mySqlOutboxSync = new MySqlOutboxSync(_mySqlTestHelper.OutboxConfiguration); + _mySqlOutbox = new MySqlOutbox(_mySqlTestHelper.OutboxConfiguration); _messageEarliest = new Message(new MessageHeader(Guid.NewGuid(), _TopicFirstMessage, MessageType.MT_DOCUMENT), new MessageBody("message body")); _message1 = new Message(new MessageHeader(Guid.NewGuid(), "test_topic2", MessageType.MT_DOCUMENT), new MessageBody("message body2")); _message2 = new Message(new MessageHeader(Guid.NewGuid(), _TopicLastMessage, MessageType.MT_DOCUMENT), new MessageBody("message body3")); - _mySqlOutboxSync.Add(_messageEarliest); + _mySqlOutbox.Add(_messageEarliest); Task.Delay(100); - _mySqlOutboxSync.Add(_message1); + _mySqlOutbox.Add(_message1); Task.Delay(100); - _mySqlOutboxSync.Add(_message2); + _mySqlOutbox.Add(_message2); } [Fact] public void When_There_Are_Multiple_Messages_In_The_Outbox_And_A_Range_Is_Fetched() { - messages = _mySqlOutboxSync.Get(1, 3); + messages = _mySqlOutbox.Get(1, 3); //should fetch 1 message messages.Should().HaveCount(1); diff --git a/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_there_are_multiple_outstanding_messages_in_the_outbox_and_messages_within_an_interval_are_fetched.cs b/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_there_are_multiple_outstanding_messages_in_the_outbox_and_messages_within_an_interval_are_fetched.cs index 776e2ae177..41ab96da1c 100644 --- a/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_there_are_multiple_outstanding_messages_in_the_outbox_and_messages_within_an_interval_are_fetched.cs +++ b/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_there_are_multiple_outstanding_messages_in_the_outbox_and_messages_within_an_interval_are_fetched.cs @@ -12,7 +12,7 @@ namespace Paramore.Brighter.MySQL.Tests.Outbox public class MySqlOutboxFetchOutstandingMessageTests : IDisposable { private readonly MySqlTestHelper _mySqlTestHelper; - private readonly MySqlOutboxSync _mySqlOutboxSync; + private readonly MySqlOutbox _mySqlOutbox; private readonly string _TopicFirstMessage = "test_topic"; private readonly string _TopicLastMessage = "test_topic3"; private readonly Message _message1; @@ -23,16 +23,16 @@ public MySqlOutboxFetchOutstandingMessageTests() { _mySqlTestHelper = new MySqlTestHelper(); _mySqlTestHelper.SetupMessageDb(); - _mySqlOutboxSync = new MySqlOutboxSync(_mySqlTestHelper.OutboxConfiguration); + _mySqlOutbox = new MySqlOutbox(_mySqlTestHelper.OutboxConfiguration); _messageEarliest = new Message(new MessageHeader(Guid.NewGuid(), _TopicFirstMessage, MessageType.MT_DOCUMENT), new MessageBody("message body")); _message1 = new Message(new MessageHeader(Guid.NewGuid(), "test_topic2", MessageType.MT_DOCUMENT), new MessageBody("message body2")); _message2 = new Message(new MessageHeader(Guid.NewGuid(), _TopicLastMessage, MessageType.MT_DOCUMENT), new MessageBody("message body3")); - _mySqlOutboxSync.Add(_messageEarliest); + _mySqlOutbox.Add(_messageEarliest); Thread.Sleep(100); - _mySqlOutboxSync.Add(_message1); + _mySqlOutbox.Add(_message1); Thread.Sleep(100); - _mySqlOutboxSync.Add(_message2); + _mySqlOutbox.Add(_message2); // Not sure why (assuming time skew) but needs time to settle Thread.Sleep(7000); @@ -41,7 +41,7 @@ public MySqlOutboxFetchOutstandingMessageTests() [Fact] public void When_there_are_multiple_outstanding_messages_in_the_outbox_and_messages_within_an_interval_are_fetched() { - var messages = _mySqlOutboxSync.OutstandingMessages(millSecondsSinceSent: 0); + var messages = _mySqlOutbox.OutstandingMessages(millSecondsSinceSent: 0); messages.Should().NotBeNullOrEmpty(); @@ -51,7 +51,7 @@ public void When_there_are_multiple_outstanding_messages_in_the_outbox_and_messa [Fact] public async Task When_there_are_multiple_outstanding_messages_in_the_outbox_and_messages_within_an_interval_are_fetched_async() { - var messages = await _mySqlOutboxSync.OutstandingMessagesAsync(millSecondsSinceSent: 0); + var messages = await _mySqlOutbox.OutstandingMessagesAsync(millSecondsSinceSent: 0); messages.Should().NotBeNullOrEmpty(); diff --git a/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_there_is_no_message_in_the_sql_message_store_async.cs b/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_there_is_no_message_in_the_sql_message_store_async.cs index 18d8ba69cd..a01dc87405 100644 --- a/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_there_is_no_message_in_the_sql_message_store_async.cs +++ b/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_there_is_no_message_in_the_sql_message_store_async.cs @@ -35,7 +35,7 @@ namespace Paramore.Brighter.MySQL.Tests.Outbox public class MySqlOutboxEmptyStoreAsyncTests : IDisposable { private readonly MySqlTestHelper _mySqlTestHelper; - private readonly MySqlOutboxSync _mySqlOutboxSync; + private readonly MySqlOutbox _mySqlOutbox; private readonly Message _messageEarliest; private Message _storedMessage; @@ -43,7 +43,7 @@ public MySqlOutboxEmptyStoreAsyncTests() { _mySqlTestHelper = new MySqlTestHelper(); _mySqlTestHelper.SetupMessageDb(); - _mySqlOutboxSync = new MySqlOutboxSync(_mySqlTestHelper.OutboxConfiguration); + _mySqlOutbox = new MySqlOutbox(_mySqlTestHelper.OutboxConfiguration); _messageEarliest = new Message(new MessageHeader(Guid.NewGuid(), "test_topic", MessageType.MT_DOCUMENT), new MessageBody("message body")); } @@ -51,7 +51,7 @@ public MySqlOutboxEmptyStoreAsyncTests() [Fact] public async Task When_There_Is_No_Message_In_The_Sql_Outbox_Async() { - _storedMessage = await _mySqlOutboxSync.GetAsync(_messageEarliest.Id); + _storedMessage = await _mySqlOutbox.GetAsync(_messageEarliest.Id); //should return an empty message _storedMessage.Header.MessageType.Should().Be(MessageType.MT_NONE); diff --git a/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_there_is_no_message_in_the_sql_outbox.cs b/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_there_is_no_message_in_the_sql_outbox.cs index 40190d7540..38df528a7f 100644 --- a/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_there_is_no_message_in_the_sql_outbox.cs +++ b/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_there_is_no_message_in_the_sql_outbox.cs @@ -34,7 +34,7 @@ namespace Paramore.Brighter.MySQL.Tests.Outbox public class MySqlOutboxEmptyStoreTests : IDisposable { private readonly MySqlTestHelper _mySqlTestHelper; - private readonly MySqlOutboxSync _mySqlOutboxSync; + private readonly MySqlOutbox _mySqlOutbox; private readonly Message _messageEarliest; private Message _storedMessage; @@ -42,7 +42,7 @@ public MySqlOutboxEmptyStoreTests() { _mySqlTestHelper = new MySqlTestHelper(); _mySqlTestHelper.SetupMessageDb(); - _mySqlOutboxSync = new MySqlOutboxSync(_mySqlTestHelper.OutboxConfiguration); + _mySqlOutbox = new MySqlOutbox(_mySqlTestHelper.OutboxConfiguration); _messageEarliest = new Message(new MessageHeader(Guid.NewGuid(), "test_topic", MessageType.MT_DOCUMENT), new MessageBody("message body")); } @@ -50,7 +50,7 @@ public MySqlOutboxEmptyStoreTests() [Fact] public void When_There_Is_No_Message_In_The_Sql_Outbox() { - _storedMessage = _mySqlOutboxSync.Get(_messageEarliest.Id); + _storedMessage = _mySqlOutbox.Get(_messageEarliest.Id); //should return an empty message _storedMessage.Header.MessageType.Should().Be(MessageType.MT_NONE); diff --git a/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_writing_a_message_to_a_binary_body_outbox.cs b/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_writing_a_message_to_a_binary_body_outbox.cs new file mode 100644 index 0000000000..e09c0b0ba5 --- /dev/null +++ b/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_writing_a_message_to_a_binary_body_outbox.cs @@ -0,0 +1,87 @@ +using System; +using FluentAssertions; +using Paramore.Brighter.Outbox.MySql; +using Xunit; + +namespace Paramore.Brighter.MySQL.Tests +{ + public class MySqlOutboxWritingBinaryMessageTests + { + private readonly MySqlTestHelper _mySqlTestHelper; + private readonly MySqlOutbox _mySqlOutbox; + private readonly string _key1 = "name1"; + private readonly string _key2 = "name2"; + private readonly string _key3 = "name3"; + private readonly string _key4 = "name4"; + private readonly string _key5 = "name5"; + private readonly Message _messageEarliest; + private Message _storedMessage; + private readonly string _value1 = "value1"; + private readonly string _value2 = "value2"; + private readonly int _value3 = 123; + private readonly Guid _value4 = Guid.NewGuid(); + private readonly DateTime _value5 = DateTime.UtcNow; + + public MySqlOutboxWritingBinaryMessageTests() + { + _mySqlTestHelper = new MySqlTestHelper(binaryMessagePayload: true); + _mySqlTestHelper.SetupMessageDb(); + _mySqlOutbox = new MySqlOutbox(_mySqlTestHelper.OutboxConfiguration); + var messageHeader = new MessageHeader( + messageId: Guid.NewGuid(), + topic: "test_topic", + messageType: MessageType.MT_DOCUMENT, + timeStamp: DateTime.UtcNow.AddDays(-1), + handledCount: 5, + delayedMilliseconds: 5, + correlationId: new Guid(), + replyTo: "ReplyTo", + contentType: "application/octet-stream", + partitionKey: Guid.NewGuid().ToString()); + messageHeader.Bag.Add(_key1, _value1); + messageHeader.Bag.Add(_key2, _value2); + messageHeader.Bag.Add(_key3, _value3); + messageHeader.Bag.Add(_key4, _value4); + messageHeader.Bag.Add(_key5, _value5); + + _messageEarliest = new Message( + messageHeader, + new MessageBody(new byte[] { 1, 2, 3, 4, 5 }, "application/octet-stream", CharacterEncoding.Raw ) + ); + _mySqlOutbox.Add(_messageEarliest); + } + + [Fact] + public void When_writing_a_message_to_a_binary_body_outbox() + { + _storedMessage = _mySqlOutbox.Get(_messageEarliest.Id); + + //should read the message from the sql outbox + _storedMessage.Body.Bytes.Should().Equal(_messageEarliest.Body.Bytes); + + //should read the header from the sql outbox + _storedMessage.Header.Topic.Should().Be(_messageEarliest.Header.Topic); + _storedMessage.Header.MessageType.Should().Be(_messageEarliest.Header.MessageType); + _storedMessage.Header.TimeStamp.ToString("yyyy-MM-ddTHH:mm:ss.fZ") + .Should().Be(_messageEarliest.Header.TimeStamp.ToString("yyyy-MM-ddTHH:mm:ss.fZ")); + _storedMessage.Header.HandledCount.Should().Be(0); // -- should be zero when read from outbox + _storedMessage.Header.DelayedMilliseconds.Should().Be(0); // -- should be zero when read from outbox + _storedMessage.Header.CorrelationId.Should().Be(_messageEarliest.Header.CorrelationId); + _storedMessage.Header.ReplyTo.Should().Be(_messageEarliest.Header.ReplyTo); + _storedMessage.Header.ContentType.Should().Be(_messageEarliest.Header.ContentType); + _storedMessage.Header.PartitionKey.Should().Be(_messageEarliest.Header.PartitionKey); + + //Bag serialization + _storedMessage.Header.Bag.ContainsKey(_key1).Should().BeTrue(); + _storedMessage.Header.Bag[_key1].Should().Be(_value1); + _storedMessage.Header.Bag.ContainsKey(_key2).Should().BeTrue(); + _storedMessage.Header.Bag[_key2].Should().Be(_value2); + _storedMessage.Header.Bag.ContainsKey(_key3).Should().BeTrue(); + _storedMessage.Header.Bag[_key3].Should().Be(_value3); + _storedMessage.Header.Bag.ContainsKey(_key4).Should().BeTrue(); + _storedMessage.Header.Bag[_key4].Should().Be(_value4); + _storedMessage.Header.Bag.ContainsKey(_key5).Should().BeTrue(); + _storedMessage.Header.Bag[_key5].Should().Be(_value5); + } + } +} diff --git a/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_writing_a_message_to_the_message_store_async.cs b/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_writing_a_message_to_the_message_store_async.cs index 3b5e161e0b..74620d0f0e 100644 --- a/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_writing_a_message_to_the_message_store_async.cs +++ b/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_writing_a_message_to_the_message_store_async.cs @@ -34,7 +34,7 @@ namespace Paramore.Brighter.MySQL.Tests.Outbox public class MySqlOutboxWritingMessageAsyncTests : IDisposable { private readonly MySqlTestHelper _mySqlTestHelper; - private readonly MySqlOutboxSync _mySqlOutboxSync; + private readonly MySqlOutbox _mySqlOutbox; private readonly string _key1 = "name1"; private readonly string _key2 = "name2"; private readonly string _key3 = "name3"; @@ -51,7 +51,7 @@ public MySqlOutboxWritingMessageAsyncTests() { _mySqlTestHelper = new MySqlTestHelper(); _mySqlTestHelper.SetupMessageDb(); - _mySqlOutboxSync = new MySqlOutboxSync(_mySqlTestHelper.OutboxConfiguration); + _mySqlOutbox = new MySqlOutbox(_mySqlTestHelper.OutboxConfiguration); var messageHeader = new MessageHeader(Guid.NewGuid(), "test_topic", MessageType.MT_DOCUMENT,DateTime.UtcNow.AddDays(-1), 5, 5); messageHeader.Bag.Add(_key1, _value1); @@ -66,16 +66,17 @@ public MySqlOutboxWritingMessageAsyncTests() [Fact] public async Task When_Writing_A_Message_To_The_Outbox_Async() { - await _mySqlOutboxSync.AddAsync(_messageEarliest); + await _mySqlOutbox.AddAsync(_messageEarliest); - _storedMessage = await _mySqlOutboxSync.GetAsync(_messageEarliest.Id); + _storedMessage = await _mySqlOutbox.GetAsync(_messageEarliest.Id); //should read the message from the sql outbox _storedMessage.Body.Value.Should().Be(_messageEarliest.Body.Value); //should read the header from the sql outbox _storedMessage.Header.Topic.Should().Be(_messageEarliest.Header.Topic); _storedMessage.Header.MessageType.Should().Be(_messageEarliest.Header.MessageType); - _storedMessage.Header.TimeStamp.Should().Be(_messageEarliest.Header.TimeStamp); + _storedMessage.Header.TimeStamp.ToString("yyyy-MM-ddTHH:mm:ss.fZ") + .Should().Be(_messageEarliest.Header.TimeStamp.ToString("yyyy-MM-ddTHH:mm:ss.fZ")); _storedMessage.Header.HandledCount.Should().Be(0); // -- should be zero when read from outbox _storedMessage.Header.DelayedMilliseconds.Should().Be(0); // -- should be zero when read from outbox _storedMessage.Header.CorrelationId.Should().Be(_messageEarliest.Header.CorrelationId); diff --git a/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_writing_a_message_to_the_outbox.cs b/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_writing_a_message_to_the_outbox.cs index 990a773a71..1df94a1b9c 100644 --- a/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_writing_a_message_to_the_outbox.cs +++ b/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_writing_a_message_to_the_outbox.cs @@ -34,7 +34,7 @@ namespace Paramore.Brighter.MySQL.Tests.Outbox public class MySqlOutboxWritingMessageTests : IDisposable { private readonly MySqlTestHelper _mySqlTestHelper; - private readonly MySqlOutboxSync _mySqlOutboxSync; + private readonly MySqlOutbox _mySqlOutbox; private readonly string _key1 = "name1"; private readonly string _key2 = "name2"; private readonly string _key3 = "name3"; @@ -51,7 +51,7 @@ public MySqlOutboxWritingMessageTests() { _mySqlTestHelper = new MySqlTestHelper(); _mySqlTestHelper.SetupMessageDb(); - _mySqlOutboxSync = new MySqlOutboxSync(_mySqlTestHelper.OutboxConfiguration); + _mySqlOutbox = new MySqlOutbox(_mySqlTestHelper.OutboxConfiguration); var messageHeader = new MessageHeader( messageId:Guid.NewGuid(), topic: "test_topic", @@ -61,7 +61,8 @@ public MySqlOutboxWritingMessageTests() delayedMilliseconds: 5, correlationId: new Guid(), replyTo: "ReplyTo", - contentType: "text/plain"); + contentType: "text/plain", + partitionKey: Guid.NewGuid().ToString()); messageHeader.Bag.Add(_key1, _value1); messageHeader.Bag.Add(_key2, _value2); messageHeader.Bag.Add(_key3, _value3); @@ -69,25 +70,27 @@ public MySqlOutboxWritingMessageTests() messageHeader.Bag.Add(_key5, _value5); _messageEarliest = new Message(messageHeader, new MessageBody("message body")); - _mySqlOutboxSync.Add(_messageEarliest); + _mySqlOutbox.Add(_messageEarliest); } [Fact] public void When_Writing_A_Message_To_The_Outbox() { - _storedMessage = _mySqlOutboxSync.Get(_messageEarliest.Id); + _storedMessage = _mySqlOutbox.Get(_messageEarliest.Id); //should read the message from the sql outbox _storedMessage.Body.Value.Should().Be(_messageEarliest.Body.Value); //should read the header from the sql outbox _storedMessage.Header.Topic.Should().Be(_messageEarliest.Header.Topic); _storedMessage.Header.MessageType.Should().Be(_messageEarliest.Header.MessageType); - _storedMessage.Header.TimeStamp.Should().Be(_messageEarliest.Header.TimeStamp); + _storedMessage.Header.TimeStamp.ToString("yyyy-MM-ddTHH:mm:ss.fZ") + .Should().Be(_messageEarliest.Header.TimeStamp.ToString("yyyy-MM-ddTHH:mm:ss.fZ")); _storedMessage.Header.HandledCount.Should().Be(0); // -- should be zero when read from outbox _storedMessage.Header.DelayedMilliseconds.Should().Be(0); // -- should be zero when read from outbox _storedMessage.Header.CorrelationId.Should().Be(_messageEarliest.Header.CorrelationId); _storedMessage.Header.ReplyTo.Should().Be(_messageEarliest.Header.ReplyTo); _storedMessage.Header.ContentType.Should().Be(_messageEarliest.Header.ContentType); + _storedMessage.Header.PartitionKey.Should().Be(_messageEarliest.Header.PartitionKey); //Bag serialization diff --git a/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_writing_messages_to_the_outbox.cs b/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_writing_messages_to_the_outbox.cs index 23cd722d7f..f391eae507 100644 --- a/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_writing_messages_to_the_outbox.cs +++ b/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_writing_messages_to_the_outbox.cs @@ -36,37 +36,37 @@ namespace Paramore.Brighter.MySQL.Tests.Outbox public class MySqlOutboxWritngMessagesTests { private readonly MySqlTestHelper _mySqlTestHelper; - private readonly MySqlOutboxSync _mySqlOutboxSync; - private readonly Message _messageEarliest; - private readonly Message _message2; - private readonly Message _messageLatest; + private readonly MySqlOutbox _mySqlOutbox; + private readonly Message _messageOne; + private readonly Message _messageTwo; + private readonly Message _messageThree; private IEnumerable _retrievedMessages; public MySqlOutboxWritngMessagesTests() { _mySqlTestHelper = new MySqlTestHelper(); _mySqlTestHelper.SetupMessageDb(); - _mySqlOutboxSync = new MySqlOutboxSync(_mySqlTestHelper.OutboxConfiguration); + _mySqlOutbox = new MySqlOutbox(_mySqlTestHelper.OutboxConfiguration); - _messageEarliest = new Message(new MessageHeader(Guid.NewGuid(), "Test", MessageType.MT_COMMAND, DateTime.UtcNow.AddHours(-3)), new MessageBody("Body")); - _message2 = new Message(new MessageHeader(Guid.NewGuid(), "Test2", MessageType.MT_COMMAND, DateTime.UtcNow.AddHours(-2)), new MessageBody("Body2")); - _messageLatest = new Message(new MessageHeader(Guid.NewGuid(), "Test3", MessageType.MT_COMMAND, DateTime.UtcNow.AddHours(-1)), new MessageBody("Body3")); + _messageOne = new Message(new MessageHeader(Guid.NewGuid(), "Test", MessageType.MT_COMMAND, DateTime.UtcNow.AddHours(-3)), new MessageBody("Body")); + _messageTwo = new Message(new MessageHeader(Guid.NewGuid(), "Test2", MessageType.MT_COMMAND, DateTime.UtcNow.AddHours(-2)), new MessageBody("Body2")); + _messageThree = new Message(new MessageHeader(Guid.NewGuid(), "Test3", MessageType.MT_COMMAND, DateTime.UtcNow.AddHours(-1)), new MessageBody("Body3")); } [Fact] public void When_Writing_Messages_To_The_Outbox() { - _mySqlOutboxSync.Add(_messageEarliest); - _mySqlOutboxSync.Add(_message2); - _mySqlOutboxSync.Add(_messageLatest); + _mySqlOutbox.Add(_messageOne); + _mySqlOutbox.Add(_messageTwo); + _mySqlOutbox.Add(_messageThree); - _retrievedMessages = _mySqlOutboxSync.Get(); + _retrievedMessages = _mySqlOutbox.Get(); - //should read first message last from the outbox - _retrievedMessages.Last().Id.Should().Be(_messageEarliest.Id); + //should read last message last from the outbox + _retrievedMessages.Last().Id.Should().Be(_messageThree.Id); //should read last message first from the outbox - _retrievedMessages.First().Id.Should().Be(_messageLatest.Id); + _retrievedMessages.First().Id.Should().Be(_messageOne.Id); //should read the messages from the outbox _retrievedMessages.Should().HaveCount(3); } @@ -74,14 +74,14 @@ public void When_Writing_Messages_To_The_Outbox() [Fact] public void When_Writing_Messages_To_The_Outbox_Bulk() { - var messages = new List { _messageEarliest, _message2, _messageLatest }; - _mySqlOutboxSync.Add(messages); - _retrievedMessages = _mySqlOutboxSync.Get(); + var messages = new List { _messageOne, _messageTwo, _messageThree }; + _mySqlOutbox.Add(messages); + _retrievedMessages = _mySqlOutbox.Get(); - //should read first message last from the outbox - _retrievedMessages.Last().Id.Should().Be(_messageEarliest.Id); - //should read last message first from the outbox - _retrievedMessages.First().Id.Should().Be(_messageLatest.Id); + //should read last message last from the outbox + _retrievedMessages.Last().Id.Should().Be(_messageThree.Id); + //should read first message first from the outbox + _retrievedMessages.First().Id.Should().Be(_messageOne.Id); //should read the messages from the outbox _retrievedMessages.Should().HaveCount(3); } diff --git a/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_writing_messages_to_the_outbox_async.cs b/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_writing_messages_to_the_outbox_async.cs index b8c57a13a0..35693ae657 100644 --- a/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_writing_messages_to_the_outbox_async.cs +++ b/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_writing_messages_to_the_outbox_async.cs @@ -37,17 +37,17 @@ namespace Paramore.Brighter.MySQL.Tests.Outbox public class MySqlOutboxWritingMessagesAsyncTests : IDisposable { private readonly MySqlTestHelper _mySqlTestHelper; - private readonly MySqlOutboxSync _mySqlOutboxSync; - private Message _message2; - private Message _messageEarliest; - private Message _messageLatest; + private readonly MySqlOutbox _mySqlOutbox; + private Message _messageTwo; + private Message _messageOne; + private Message _messageThree; private IList _retrievedMessages; public MySqlOutboxWritingMessagesAsyncTests() { _mySqlTestHelper = new MySqlTestHelper(); _mySqlTestHelper.SetupMessageDb(); - _mySqlOutboxSync = new MySqlOutboxSync(_mySqlTestHelper.OutboxConfiguration); + _mySqlOutbox = new MySqlOutbox(_mySqlTestHelper.OutboxConfiguration); } [Fact] @@ -55,12 +55,12 @@ public async Task When_Writing_Messages_To_The_Outbox_Async() { await SetUpMessagesAsync(); - _retrievedMessages = await _mySqlOutboxSync.GetAsync(); + _retrievedMessages = await _mySqlOutbox.GetAsync(); - //should read first message last from the outbox - _retrievedMessages.Last().Id.Should().Be(_messageEarliest.Id); + //should read last message last from the outbox + _retrievedMessages.Last().Id.Should().Be(_messageThree.Id); //should read last message first from the outbox - _retrievedMessages.First().Id.Should().Be(_messageLatest.Id); + _retrievedMessages.First().Id.Should().Be(_messageOne.Id); //should read the messages from the outbox _retrievedMessages.Should().HaveCount(3); } @@ -69,30 +69,30 @@ public async Task When_Writing_Messages_To_The_Outbox_Async() public async Task When_Writing_Messages_To_The_Outbox_Async_Bulk() { var messages = await SetUpMessagesAsync(false); - await _mySqlOutboxSync.AddAsync(messages); + await _mySqlOutbox.AddAsync(messages); - _retrievedMessages = await _mySqlOutboxSync.GetAsync(); + _retrievedMessages = await _mySqlOutbox.GetAsync(); - //should read first message last from the outbox - _retrievedMessages.Last().Id.Should().Be(_messageEarliest.Id); - //should read last message first from the outbox - _retrievedMessages.First().Id.Should().Be(_messageLatest.Id); + //should read last message last from the outbox + _retrievedMessages.Last().Id.Should().Be(_messageThree.Id); + //should read first message first from the outbox + _retrievedMessages.First().Id.Should().Be(_messageOne.Id); //should read the messages from the outbox _retrievedMessages.Should().HaveCount(3); } private async Task> SetUpMessagesAsync(bool addMessagesToOutbox = true) { - _messageEarliest = new Message(new MessageHeader(Guid.NewGuid(), "Test", MessageType.MT_COMMAND, DateTime.UtcNow.AddHours(-3)), new MessageBody("Body")); - if(addMessagesToOutbox) await _mySqlOutboxSync.AddAsync(_messageEarliest); + _messageOne = new Message(new MessageHeader(Guid.NewGuid(), "Test", MessageType.MT_COMMAND, DateTime.UtcNow.AddHours(-3)), new MessageBody("Body")); + if(addMessagesToOutbox) await _mySqlOutbox.AddAsync(_messageOne); - _message2 = new Message(new MessageHeader(Guid.NewGuid(), "Test2", MessageType.MT_COMMAND, DateTime.UtcNow.AddHours(-2)), new MessageBody("Body2")); - if(addMessagesToOutbox) await _mySqlOutboxSync.AddAsync(_message2); + _messageTwo = new Message(new MessageHeader(Guid.NewGuid(), "Test2", MessageType.MT_COMMAND, DateTime.UtcNow.AddHours(-2)), new MessageBody("Body2")); + if(addMessagesToOutbox) await _mySqlOutbox.AddAsync(_messageTwo); - _messageLatest = new Message(new MessageHeader(Guid.NewGuid(), "Test3", MessageType.MT_COMMAND, DateTime.UtcNow.AddHours(-1)), new MessageBody("Body3")); - if(addMessagesToOutbox) await _mySqlOutboxSync.AddAsync(_messageLatest); + _messageThree = new Message(new MessageHeader(Guid.NewGuid(), "Test3", MessageType.MT_COMMAND, DateTime.UtcNow.AddHours(-1)), new MessageBody("Body3")); + if(addMessagesToOutbox) await _mySqlOutbox.AddAsync(_messageThree); - return new List { _messageEarliest, _message2, _messageLatest }; + return new List { _messageOne, _messageTwo, _messageThree }; } public void Dispose() diff --git a/tests/Paramore.Brighter.PostgresSQL.Tests/Inbox/When_the_message_Is_already_in_the_Inbox_async.cs b/tests/Paramore.Brighter.PostgresSQL.Tests/Inbox/When_the_message_Is_already_in_the_Inbox_async.cs index 76c0ef9d37..f7302e6bbc 100644 --- a/tests/Paramore.Brighter.PostgresSQL.Tests/Inbox/When_the_message_Is_already_in_the_Inbox_async.cs +++ b/tests/Paramore.Brighter.PostgresSQL.Tests/Inbox/When_the_message_Is_already_in_the_Inbox_async.cs @@ -37,7 +37,7 @@ namespace Paramore.Brighter.PostgresSQL.Tests.Inbox public class SqlInboxDuplicateMessageAsyncTests : IDisposable { private readonly PostgresSqlTestHelper _pgTestHelper; - private readonly PostgresSqlInbox _pgSqlInbox; + private readonly PostgreSqlInbox _pgSqlInbox; private readonly MyCommand _raisedCommand; private readonly string _contextKey; private Exception _exception; @@ -47,7 +47,7 @@ public SqlInboxDuplicateMessageAsyncTests() _pgTestHelper = new PostgresSqlTestHelper(); _pgTestHelper.SetupCommandDb(); - _pgSqlInbox = new PostgresSqlInbox(_pgTestHelper.InboxConfiguration); + _pgSqlInbox = new PostgreSqlInbox(_pgTestHelper.InboxConfiguration); _raisedCommand = new MyCommand { Value = "Test" }; _contextKey = "test-context"; } diff --git a/tests/Paramore.Brighter.PostgresSQL.Tests/Inbox/When_the_message_is_already_in_the_inbox.cs b/tests/Paramore.Brighter.PostgresSQL.Tests/Inbox/When_the_message_is_already_in_the_inbox.cs index 229ea89421..dc9e82ee56 100644 --- a/tests/Paramore.Brighter.PostgresSQL.Tests/Inbox/When_the_message_is_already_in_the_inbox.cs +++ b/tests/Paramore.Brighter.PostgresSQL.Tests/Inbox/When_the_message_is_already_in_the_inbox.cs @@ -36,7 +36,7 @@ namespace Paramore.Brighter.PostgresSQL.Tests.Inbox public class SqlInboxDuplicateMessageTests : IDisposable { private readonly PostgresSqlTestHelper _pgTestHelper; - private readonly PostgresSqlInbox _pgSqlInbox; + private readonly PostgreSqlInbox _pgSqlInbox; private readonly MyCommand _raisedCommand; private readonly string _contextKey; private Exception _exception; @@ -46,7 +46,7 @@ public SqlInboxDuplicateMessageTests() _pgTestHelper = new PostgresSqlTestHelper(); _pgTestHelper.SetupCommandDb(); - _pgSqlInbox = new PostgresSqlInbox(_pgTestHelper.InboxConfiguration); + _pgSqlInbox = new PostgreSqlInbox(_pgTestHelper.InboxConfiguration); _raisedCommand = new MyCommand { Value = "Test" }; _contextKey = Guid.NewGuid().ToString(); } diff --git a/tests/Paramore.Brighter.PostgresSQL.Tests/Inbox/When_there_Is_no_message_in_the_sql_inbox_async.cs b/tests/Paramore.Brighter.PostgresSQL.Tests/Inbox/When_there_Is_no_message_in_the_sql_inbox_async.cs index 7626060ddd..c762105283 100644 --- a/tests/Paramore.Brighter.PostgresSQL.Tests/Inbox/When_there_Is_no_message_in_the_sql_inbox_async.cs +++ b/tests/Paramore.Brighter.PostgresSQL.Tests/Inbox/When_there_Is_no_message_in_the_sql_inbox_async.cs @@ -38,14 +38,14 @@ namespace Paramore.Brighter.PostgresSQL.Tests.Inbox public class SqlInboxEmptyWhenSearchedAsyncTests : IDisposable { private readonly PostgresSqlTestHelper _pgTestHelper; - private readonly PostgresSqlInbox _sqlSqlInbox; + private readonly PostgreSqlInbox _sqlSqlInbox; public SqlInboxEmptyWhenSearchedAsyncTests() { _pgTestHelper = new PostgresSqlTestHelper(); _pgTestHelper.SetupCommandDb(); - _sqlSqlInbox = new PostgresSqlInbox(_pgTestHelper.InboxConfiguration); + _sqlSqlInbox = new PostgreSqlInbox(_pgTestHelper.InboxConfiguration); } [Fact] diff --git a/tests/Paramore.Brighter.PostgresSQL.Tests/Inbox/When_there_is_no_message_in_the_sql_inbox.cs b/tests/Paramore.Brighter.PostgresSQL.Tests/Inbox/When_there_is_no_message_in_the_sql_inbox.cs index 6271a66144..be641b6907 100644 --- a/tests/Paramore.Brighter.PostgresSQL.Tests/Inbox/When_there_is_no_message_in_the_sql_inbox.cs +++ b/tests/Paramore.Brighter.PostgresSQL.Tests/Inbox/When_there_is_no_message_in_the_sql_inbox.cs @@ -37,7 +37,7 @@ namespace Paramore.Brighter.PostgresSQL.Tests.Inbox public class SqlInboxEmptyWhenSearchedTests : IDisposable { private readonly PostgresSqlTestHelper _pgTestHelper; - private readonly PostgresSqlInbox _pgSqlInbox; + private readonly PostgreSqlInbox _pgSqlInbox; private readonly string _contextKey; private MyCommand _storedCommand; @@ -46,7 +46,7 @@ public SqlInboxEmptyWhenSearchedTests() _pgTestHelper = new PostgresSqlTestHelper(); _pgTestHelper.SetupCommandDb(); - _pgSqlInbox = new PostgresSqlInbox(_pgTestHelper.InboxConfiguration); + _pgSqlInbox = new PostgreSqlInbox(_pgTestHelper.InboxConfiguration); _contextKey = "context-key"; } diff --git a/tests/Paramore.Brighter.PostgresSQL.Tests/Inbox/When_writing_a_message_to_the_inbox.cs b/tests/Paramore.Brighter.PostgresSQL.Tests/Inbox/When_writing_a_message_to_the_inbox.cs index 0589eab475..73594346fa 100644 --- a/tests/Paramore.Brighter.PostgresSQL.Tests/Inbox/When_writing_a_message_to_the_inbox.cs +++ b/tests/Paramore.Brighter.PostgresSQL.Tests/Inbox/When_writing_a_message_to_the_inbox.cs @@ -37,7 +37,7 @@ namespace Paramore.Brighter.PostgresSQL.Tests.Inbox public class SqlInboxAddMessageTests : IDisposable { private readonly PostgresSqlTestHelper _pgTestHelper; - private readonly PostgresSqlInbox _pgSqlInbox; + private readonly PostgreSqlInbox _pgSqlInbox; private readonly MyCommand _raisedCommand; private readonly string _contextKey; private MyCommand _storedCommand; @@ -47,7 +47,7 @@ public SqlInboxAddMessageTests() _pgTestHelper = new PostgresSqlTestHelper(); _pgTestHelper.SetupCommandDb(); - _pgSqlInbox = new PostgresSqlInbox(_pgTestHelper.InboxConfiguration); + _pgSqlInbox = new PostgreSqlInbox(_pgTestHelper.InboxConfiguration); _raisedCommand = new MyCommand { Value = "Test" }; _contextKey = "context-key"; _pgSqlInbox.Add(_raisedCommand, _contextKey); diff --git a/tests/Paramore.Brighter.PostgresSQL.Tests/Inbox/When_writing_a_message_to_the_inbox_async.cs b/tests/Paramore.Brighter.PostgresSQL.Tests/Inbox/When_writing_a_message_to_the_inbox_async.cs index bda78637ed..cc9887bcf6 100644 --- a/tests/Paramore.Brighter.PostgresSQL.Tests/Inbox/When_writing_a_message_to_the_inbox_async.cs +++ b/tests/Paramore.Brighter.PostgresSQL.Tests/Inbox/When_writing_a_message_to_the_inbox_async.cs @@ -37,7 +37,7 @@ namespace Paramore.Brighter.PostgresSQL.Tests.Inbox public class SqlInboxAddMessageAsyncTests : IDisposable { private readonly PostgresSqlTestHelper _pgTestHelper; - private readonly PostgresSqlInbox _pgSqlInbox; + private readonly PostgreSqlInbox _pgSqlInbox; private readonly MyCommand _raisedCommand; private readonly string _contextKey; private MyCommand _storedCommand; @@ -47,7 +47,7 @@ public SqlInboxAddMessageAsyncTests() _pgTestHelper = new PostgresSqlTestHelper(); _pgTestHelper.SetupCommandDb(); - _pgSqlInbox = new PostgresSqlInbox(_pgTestHelper.InboxConfiguration); + _pgSqlInbox = new PostgreSqlInbox(_pgTestHelper.InboxConfiguration); _raisedCommand = new MyCommand { Value = "Test" }; _contextKey = "context-key"; } diff --git a/tests/Paramore.Brighter.PostgresSQL.Tests/Outbox/When_Removing_Messages_From_The_Outbox.cs b/tests/Paramore.Brighter.PostgresSQL.Tests/Outbox/When_Removing_Messages_From_The_Outbox.cs index 886bf15703..dd2569fad4 100644 --- a/tests/Paramore.Brighter.PostgresSQL.Tests/Outbox/When_Removing_Messages_From_The_Outbox.cs +++ b/tests/Paramore.Brighter.PostgresSQL.Tests/Outbox/When_Removing_Messages_From_The_Outbox.cs @@ -40,14 +40,14 @@ public class SqlOutboxDeletingMessagesTests : IDisposable private readonly Message _message2; private readonly Message _messageLatest; private IEnumerable _retrievedMessages; - private readonly PostgreSqlOutboxSync _sqlOutboxSync; + private readonly PostgreSqlOutbox _sqlOutbox; public SqlOutboxDeletingMessagesTests() { _postgresSqlTestHelper = new PostgresSqlTestHelper(); _postgresSqlTestHelper.SetupMessageDb(); - _sqlOutboxSync = new PostgreSqlOutboxSync(_postgresSqlTestHelper.OutboxConfiguration); + _sqlOutbox = new PostgreSqlOutbox(_postgresSqlTestHelper.Configuration); _messageEarliest = new Message(new MessageHeader(Guid.NewGuid(), "Test", MessageType.MT_COMMAND, DateTime.UtcNow.AddHours(-3)), new MessageBody("Body")); _message2 = new Message(new MessageHeader(Guid.NewGuid(), "Test2", MessageType.MT_COMMAND, DateTime.UtcNow.AddHours(-2)), new MessageBody("Body2")); @@ -59,23 +59,23 @@ public SqlOutboxDeletingMessagesTests() [Fact] public void When_Removing_Messages_From_The_Outbox() { - _sqlOutboxSync.Add(_messageEarliest); - _sqlOutboxSync.Add(_message2); - _sqlOutboxSync.Add(_messageLatest); + _sqlOutbox.Add(_messageEarliest); + _sqlOutbox.Add(_message2); + _sqlOutbox.Add(_messageLatest); - _retrievedMessages = _sqlOutboxSync.Get(); + _retrievedMessages = _sqlOutbox.Get(); - _sqlOutboxSync.Delete(_retrievedMessages.First().Id); + _sqlOutbox.Delete(_retrievedMessages.First().Id); - var remainingMessages = _sqlOutboxSync.Get(); + var remainingMessages = _sqlOutbox.Get(); remainingMessages.Should().HaveCount(2); remainingMessages.Should().Contain(_retrievedMessages.ToList()[1]); remainingMessages.Should().Contain(_retrievedMessages.ToList()[2]); - _sqlOutboxSync.Delete(remainingMessages.Select(m => m.Id).ToArray()); + _sqlOutbox.Delete(remainingMessages.Select(m => m.Id).ToArray()); - var messages = _sqlOutboxSync.Get(); + var messages = _sqlOutbox.Get(); messages.Should().HaveCount(0); } diff --git a/tests/Paramore.Brighter.PostgresSQL.Tests/Outbox/When_The_Message_Is_Already_In_The_Outbox.cs b/tests/Paramore.Brighter.PostgresSQL.Tests/Outbox/When_The_Message_Is_Already_In_The_Outbox.cs index c083c548b4..f7d4ea02d7 100644 --- a/tests/Paramore.Brighter.PostgresSQL.Tests/Outbox/When_The_Message_Is_Already_In_The_Outbox.cs +++ b/tests/Paramore.Brighter.PostgresSQL.Tests/Outbox/When_The_Message_Is_Already_In_The_Outbox.cs @@ -35,7 +35,7 @@ public class PostgreSqlOutboxMessageAlreadyExistsTests : IDisposable { private Exception _exception; private readonly Message _messageEarliest; - private readonly PostgreSqlOutboxSync _sqlOutboxSync; + private readonly PostgreSqlOutbox _sqlOutbox; private readonly PostgresSqlTestHelper _postgresSqlTestHelper; public PostgreSqlOutboxMessageAlreadyExistsTests() @@ -43,15 +43,15 @@ public PostgreSqlOutboxMessageAlreadyExistsTests() _postgresSqlTestHelper = new PostgresSqlTestHelper(); _postgresSqlTestHelper.SetupMessageDb(); - _sqlOutboxSync = new PostgreSqlOutboxSync(_postgresSqlTestHelper.OutboxConfiguration); + _sqlOutbox = new PostgreSqlOutbox(_postgresSqlTestHelper.Configuration); _messageEarliest = new Message(new MessageHeader(Guid.NewGuid(), "test_topic", MessageType.MT_DOCUMENT), new MessageBody("message body")); - _sqlOutboxSync.Add(_messageEarliest); + _sqlOutbox.Add(_messageEarliest); } [Fact] public void When_The_Message_Is_Already_In_The_Outbox() { - _exception = Catch.Exception(() => _sqlOutboxSync.Add(_messageEarliest)); + _exception = Catch.Exception(() => _sqlOutbox.Add(_messageEarliest)); //should ignore the duplicate key and still succeed _exception.Should().BeNull(); diff --git a/tests/Paramore.Brighter.PostgresSQL.Tests/Outbox/When_There_Are_Multiple_Messages_In_The_Outbox_And_A_Range_Is_Fetched.cs b/tests/Paramore.Brighter.PostgresSQL.Tests/Outbox/When_There_Are_Multiple_Messages_In_The_Outbox_And_A_Range_Is_Fetched.cs index 23825c147e..e48028730e 100644 --- a/tests/Paramore.Brighter.PostgresSQL.Tests/Outbox/When_There_Are_Multiple_Messages_In_The_Outbox_And_A_Range_Is_Fetched.cs +++ b/tests/Paramore.Brighter.PostgresSQL.Tests/Outbox/When_There_Are_Multiple_Messages_In_The_Outbox_And_A_Range_Is_Fetched.cs @@ -37,36 +37,38 @@ namespace Paramore.Brighter.PostgresSQL.Tests.Outbox public class PostgreSqlOutboxRangeRequestTests : IDisposable { private readonly PostgresSqlTestHelper _postgresSqlTestHelper; - private readonly string _TopicFirstMessage = "test_topic"; - private readonly string _TopicLastMessage = "test_topic3"; + private readonly string _topicFirstMessage = "test_topic"; + private readonly string _topicSecondMessage = "test_topic2"; + private readonly string _topicLastMessage = "test_topic3"; private IEnumerable _messages; - private readonly PostgreSqlOutboxSync _sqlOutboxSync; + private readonly PostgreSqlOutbox _sqlOutbox; public PostgreSqlOutboxRangeRequestTests() { _postgresSqlTestHelper = new PostgresSqlTestHelper(); _postgresSqlTestHelper.SetupMessageDb(); - _sqlOutboxSync = new PostgreSqlOutboxSync(_postgresSqlTestHelper.OutboxConfiguration); - var messageEarliest = new Message(new MessageHeader(Guid.NewGuid(), _TopicFirstMessage, MessageType.MT_DOCUMENT), new MessageBody("message body")); - var message1 = new Message(new MessageHeader(Guid.NewGuid(), "test_topic2", MessageType.MT_DOCUMENT), new MessageBody("message body2")); - var message2 = new Message(new MessageHeader(Guid.NewGuid(), _TopicLastMessage, MessageType.MT_DOCUMENT), new MessageBody("message body3")); - _sqlOutboxSync.Add(messageEarliest); + _sqlOutbox = new PostgreSqlOutbox(_postgresSqlTestHelper.Configuration); + var messageOne = new Message(new MessageHeader(Guid.NewGuid(), _topicFirstMessage, MessageType.MT_DOCUMENT), new MessageBody("message body")); + var messageTwo = new Message(new MessageHeader(Guid.NewGuid(), _topicSecondMessage, MessageType.MT_DOCUMENT), new MessageBody("message body2")); + var messageThree = new Message(new MessageHeader(Guid.NewGuid(), _topicLastMessage, MessageType.MT_DOCUMENT), new MessageBody("message body3")); + + _sqlOutbox.Add(messageOne); Task.Delay(100); - _sqlOutboxSync.Add(message1); + _sqlOutbox.Add(messageTwo); Task.Delay(100); - _sqlOutboxSync.Add(message2); + _sqlOutbox.Add(messageThree); } [Fact] public void When_There_Are_Multiple_Messages_In_The_Outbox_And_A_Range_Is_Fetched() { - _messages = _sqlOutboxSync.Get(1, 3); + _messages = _sqlOutbox.Get(1, 3); //should fetch 1 message _messages.Should().HaveCount(1); //should fetch expected message - _messages.First().Header.Topic.Should().Be(_TopicLastMessage); + _messages.First().Header.Topic.Should().Be(_topicLastMessage); //should not fetch null messages _messages.Should().NotBeNull(); } diff --git a/tests/Paramore.Brighter.PostgresSQL.Tests/Outbox/When_There_Is_No_Message_In_The_Sql_Outbox.cs b/tests/Paramore.Brighter.PostgresSQL.Tests/Outbox/When_There_Is_No_Message_In_The_Sql_Outbox.cs index f6b9b1e6c3..3bd30295d1 100644 --- a/tests/Paramore.Brighter.PostgresSQL.Tests/Outbox/When_There_Is_No_Message_In_The_Sql_Outbox.cs +++ b/tests/Paramore.Brighter.PostgresSQL.Tests/Outbox/When_There_Is_No_Message_In_The_Sql_Outbox.cs @@ -35,7 +35,7 @@ public class PostgreSqlOutboxEmptyStoreTests : IDisposable { private readonly PostgresSqlTestHelper _postgresSqlTestHelper; private readonly Message _messageEarliest; - private readonly PostgreSqlOutboxSync _sqlOutboxSync; + private readonly PostgreSqlOutbox _sqlOutbox; private Message _storedMessage; public PostgreSqlOutboxEmptyStoreTests() @@ -43,14 +43,14 @@ public PostgreSqlOutboxEmptyStoreTests() _postgresSqlTestHelper = new PostgresSqlTestHelper(); _postgresSqlTestHelper.SetupMessageDb(); - _sqlOutboxSync = new PostgreSqlOutboxSync(_postgresSqlTestHelper.OutboxConfiguration); + _sqlOutbox = new PostgreSqlOutbox(_postgresSqlTestHelper.Configuration); _messageEarliest = new Message(new MessageHeader(Guid.NewGuid(), "test_topic", MessageType.MT_DOCUMENT), new MessageBody("message body")); } [Fact] public void When_There_Is_No_Message_In_The_Sql_Outbox() { - _storedMessage = _sqlOutboxSync.Get(_messageEarliest.Id); + _storedMessage = _sqlOutbox.Get(_messageEarliest.Id); //should return a empty message _storedMessage.Header.MessageType.Should().Be(MessageType.MT_NONE); diff --git a/tests/Paramore.Brighter.PostgresSQL.Tests/Outbox/When_Writing_A_Message_To_A_Binary_Body_Outbox.cs b/tests/Paramore.Brighter.PostgresSQL.Tests/Outbox/When_Writing_A_Message_To_A_Binary_Body_Outbox.cs new file mode 100644 index 0000000000..11117b9ee1 --- /dev/null +++ b/tests/Paramore.Brighter.PostgresSQL.Tests/Outbox/When_Writing_A_Message_To_A_Binary_Body_Outbox.cs @@ -0,0 +1,89 @@ +using System; +using FluentAssertions; +using Paramore.Brighter.Outbox.PostgreSql; +using Xunit; + +namespace Paramore.Brighter.PostgresSQL.Tests.Outbox +{ + [Trait("Category", "PostgresSql")] + public class SqlBinaryOutboxWritingMessageTests : IDisposable + { + private readonly string _key1 = "name1"; + private readonly string _key2 = "name2"; + private readonly string _key3 = "name3"; + private readonly string _key4 = "name4"; + private readonly string _key5 = "name5"; + private readonly Message _messageEarliest; + private readonly PostgreSqlOutbox _sqlOutbox; + private Message _storedMessage; + private readonly string _value1 = "value1"; + private readonly string _value2 = "value2"; + private readonly int _value3 = 123; + private readonly Guid _value4 = Guid.NewGuid(); + private readonly DateTime _value5 = DateTime.UtcNow; + private readonly PostgresSqlTestHelper _postgresSqlTestHelper; + + public SqlBinaryOutboxWritingMessageTests() + { + _postgresSqlTestHelper = new PostgresSqlTestHelper(binaryMessagePayload: true); + _postgresSqlTestHelper.SetupMessageDb(); + + _sqlOutbox = new PostgreSqlOutbox(_postgresSqlTestHelper.Configuration); + var messageHeader = new MessageHeader( + messageId: Guid.NewGuid(), + topic: "test_topic", + messageType: MessageType.MT_DOCUMENT, + timeStamp: DateTime.UtcNow.AddDays(-1), + handledCount: 5, + delayedMilliseconds: 5, + correlationId: Guid.NewGuid(), + replyTo: "ReplyTo", + contentType: "text/plain", + partitionKey: Guid.NewGuid().ToString()); + messageHeader.Bag.Add(_key1, _value1); + messageHeader.Bag.Add(_key2, _value2); + messageHeader.Bag.Add(_key3, _value3); + messageHeader.Bag.Add(_key4, _value4); + messageHeader.Bag.Add(_key5, _value5); + + _messageEarliest = new Message(messageHeader, new MessageBody("message body")); + _sqlOutbox.Add(_messageEarliest); + } + + public void When_Writing_A_Message_To_A_Binary_Body_Outbox() + { + _storedMessage = _sqlOutbox.Get(_messageEarliest.Id); + + //should read the message from the sql outbox + _storedMessage.Body.Value.Should().Be(_messageEarliest.Body.Value); + //should read the header from the sql outbox + _storedMessage.Header.Topic.Should().Be(_messageEarliest.Header.Topic); + _storedMessage.Header.MessageType.Should().Be(_messageEarliest.Header.MessageType); + _storedMessage.Header.TimeStamp.ToString("yyyy-MM-ddTHH:mm:ss.fZ") + .Should().Be(_messageEarliest.Header.TimeStamp.ToString("yyyy-MM-ddTHH:mm:ss.fZ")); + _storedMessage.Header.HandledCount.Should().Be(0); // -- should be zero when read from outbox + _storedMessage.Header.DelayedMilliseconds.Should().Be(0); // -- should be zero when read from outbox + _storedMessage.Header.CorrelationId.Should().Be(_messageEarliest.Header.CorrelationId); + _storedMessage.Header.ReplyTo.Should().Be(_messageEarliest.Header.ReplyTo); + _storedMessage.Header.ContentType.Should().Be(_messageEarliest.Header.ContentType); + _storedMessage.Header.PartitionKey.Should().Be(_messageEarliest.Header.PartitionKey); + + //Bag serialization + _storedMessage.Header.Bag.ContainsKey(_key1).Should().BeTrue(); + _storedMessage.Header.Bag[_key1].Should().Be(_value1); + _storedMessage.Header.Bag.ContainsKey(_key2).Should().BeTrue(); + _storedMessage.Header.Bag[_key2].Should().Be(_value2); + _storedMessage.Header.Bag.ContainsKey(_key3).Should().BeTrue(); + _storedMessage.Header.Bag[_key3].Should().Be(_value3); + _storedMessage.Header.Bag.ContainsKey(_key4).Should().BeTrue(); + _storedMessage.Header.Bag[_key4].Should().Be(_value4); + _storedMessage.Header.Bag.ContainsKey(_key5).Should().BeTrue(); + _storedMessage.Header.Bag[_key5].Should().Be(_value5); + } + + public void Dispose() + { + _postgresSqlTestHelper.CleanUpDb(); + } + } +} diff --git a/tests/Paramore.Brighter.PostgresSQL.Tests/Outbox/When_Writing_A_Message_To_The_Outbox.cs b/tests/Paramore.Brighter.PostgresSQL.Tests/Outbox/When_Writing_A_Message_To_The_Outbox.cs index ae86b66ebb..89d6611064 100644 --- a/tests/Paramore.Brighter.PostgresSQL.Tests/Outbox/When_Writing_A_Message_To_The_Outbox.cs +++ b/tests/Paramore.Brighter.PostgresSQL.Tests/Outbox/When_Writing_A_Message_To_The_Outbox.cs @@ -39,7 +39,7 @@ public class SqlOutboxWritingMessageTests : IDisposable private readonly string _key4 = "name4"; private readonly string _key5 = "name5"; private readonly Message _messageEarliest; - private readonly PostgreSqlOutboxSync _sqlOutboxSync; + private readonly PostgreSqlOutbox _sqlOutbox; private Message _storedMessage; private readonly string _value1 = "value1"; private readonly string _value2 = "value2"; @@ -53,7 +53,7 @@ public SqlOutboxWritingMessageTests() _postgresSqlTestHelper = new PostgresSqlTestHelper(); _postgresSqlTestHelper.SetupMessageDb(); - _sqlOutboxSync = new PostgreSqlOutboxSync(_postgresSqlTestHelper.OutboxConfiguration); + _sqlOutbox = new PostgreSqlOutbox(_postgresSqlTestHelper.Configuration); var messageHeader = new MessageHeader( messageId:Guid.NewGuid(), topic: "test_topic", @@ -63,7 +63,8 @@ public SqlOutboxWritingMessageTests() delayedMilliseconds:5, correlationId: Guid.NewGuid(), replyTo: "ReplyTo", - contentType: "text/plain"); + contentType: "text/plain", + partitionKey: Guid.NewGuid().ToString()); messageHeader.Bag.Add(_key1, _value1); messageHeader.Bag.Add(_key2, _value2); messageHeader.Bag.Add(_key3, _value3); @@ -71,26 +72,27 @@ public SqlOutboxWritingMessageTests() messageHeader.Bag.Add(_key5, _value5); _messageEarliest = new Message(messageHeader, new MessageBody("message body")); - _sqlOutboxSync.Add(_messageEarliest); + _sqlOutbox.Add(_messageEarliest); } [Fact] public void When_Writing_A_Message_To_The_PostgreSql_Outbox() { - _storedMessage = _sqlOutboxSync.Get(_messageEarliest.Id); + _storedMessage = _sqlOutbox.Get(_messageEarliest.Id); //should read the message from the sql outbox _storedMessage.Body.Value.Should().Be(_messageEarliest.Body.Value); //should read the header from the sql outbox _storedMessage.Header.Topic.Should().Be(_messageEarliest.Header.Topic); _storedMessage.Header.MessageType.Should().Be(_messageEarliest.Header.MessageType); - _storedMessage.Header.TimeStamp.Should().Be(_messageEarliest.Header.TimeStamp); + _storedMessage.Header.TimeStamp.ToString("yyyy-MM-ddTHH:mm:ss.fZ") + .Should().Be(_messageEarliest.Header.TimeStamp.ToString("yyyy-MM-ddTHH:mm:ss.fZ")); _storedMessage.Header.HandledCount.Should().Be(0); // -- should be zero when read from outbox _storedMessage.Header.DelayedMilliseconds.Should().Be(0); // -- should be zero when read from outbox _storedMessage.Header.CorrelationId.Should().Be(_messageEarliest.Header.CorrelationId); _storedMessage.Header.ReplyTo.Should().Be(_messageEarliest.Header.ReplyTo); _storedMessage.Header.ContentType.Should().Be(_messageEarliest.Header.ContentType); - + _storedMessage.Header.PartitionKey.Should().Be(_messageEarliest.Header.PartitionKey); //Bag serialization _storedMessage.Header.Bag.ContainsKey(_key1).Should().BeTrue(); diff --git a/tests/Paramore.Brighter.PostgresSQL.Tests/Outbox/When_Writing_Messages_To_The_Outbox.cs b/tests/Paramore.Brighter.PostgresSQL.Tests/Outbox/When_Writing_Messages_To_The_Outbox.cs index 898ce423cf..62989518cc 100644 --- a/tests/Paramore.Brighter.PostgresSQL.Tests/Outbox/When_Writing_Messages_To_The_Outbox.cs +++ b/tests/Paramore.Brighter.PostgresSQL.Tests/Outbox/When_Writing_Messages_To_The_Outbox.cs @@ -36,39 +36,39 @@ namespace Paramore.Brighter.PostgresSQL.Tests.Outbox public class SqlOutboxWritngMessagesTests : IDisposable { private readonly PostgresSqlTestHelper _postgresSqlTestHelper; - private readonly Message _messageEarliest; - private readonly Message _message2; - private readonly Message _messageLatest; + private readonly Message _messageOne; + private readonly Message _messageTwo; + private readonly Message _messageThree; private IEnumerable _retrievedMessages; - private readonly PostgreSqlOutboxSync _sqlOutboxSync; + private readonly PostgreSqlOutbox _sqlOutbox; public SqlOutboxWritngMessagesTests() { _postgresSqlTestHelper = new PostgresSqlTestHelper(); _postgresSqlTestHelper.SetupMessageDb(); - _sqlOutboxSync = new PostgreSqlOutboxSync(_postgresSqlTestHelper.OutboxConfiguration); - _messageEarliest = new Message(new MessageHeader(Guid.NewGuid(), "Test", MessageType.MT_COMMAND, DateTime.UtcNow.AddHours(-3)), new MessageBody("Body")); + _sqlOutbox = new PostgreSqlOutbox(_postgresSqlTestHelper.Configuration); + _messageOne = new Message(new MessageHeader(Guid.NewGuid(), "Test", MessageType.MT_COMMAND, DateTime.UtcNow.AddHours(-3)), new MessageBody("Body")); - _message2 = new Message(new MessageHeader(Guid.NewGuid(), "Test2", MessageType.MT_COMMAND, DateTime.UtcNow.AddHours(-2)), new MessageBody("Body2")); + _messageTwo = new Message(new MessageHeader(Guid.NewGuid(), "Test2", MessageType.MT_COMMAND, DateTime.UtcNow.AddHours(-2)), new MessageBody("Body2")); - _messageLatest = new Message(new MessageHeader(Guid.NewGuid(), "Test3", MessageType.MT_COMMAND, DateTime.UtcNow.AddHours(-1)), new MessageBody("Body3")); + _messageThree = new Message(new MessageHeader(Guid.NewGuid(), "Test3", MessageType.MT_COMMAND, DateTime.UtcNow.AddHours(-1)), new MessageBody("Body3")); } [Fact] public void When_Writing_Messages_To_The_Outbox() { - _sqlOutboxSync.Add(_messageEarliest); - _sqlOutboxSync.Add(_message2); - _sqlOutboxSync.Add(_messageLatest); + _sqlOutbox.Add(_messageOne); + _sqlOutbox.Add(_messageTwo); + _sqlOutbox.Add(_messageThree); - _retrievedMessages = _sqlOutboxSync.Get(); + _retrievedMessages = _sqlOutbox.Get(); - //should read first message last from the outbox - _retrievedMessages.Last().Id.Should().Be(_messageEarliest.Id); + //should read last message last from the outbox + _retrievedMessages.Last().Id.Should().Be(_messageThree.Id); //should read last message first from the outbox - _retrievedMessages.First().Id.Should().Be(_messageLatest.Id); + _retrievedMessages.First().Id.Should().Be(_messageOne.Id); //should read the messages from the outbox _retrievedMessages.Should().HaveCount(3); } @@ -76,13 +76,13 @@ public void When_Writing_Messages_To_The_Outbox() [Fact] public void When_Writing_Messages_To_The_Outbox_Bulk() { - _sqlOutboxSync.Add(new List{_messageEarliest, _message2, _messageLatest}); - _retrievedMessages = _sqlOutboxSync.Get(); + _sqlOutbox.Add(new List{_messageOne, _messageTwo, _messageThree}); + _retrievedMessages = _sqlOutbox.Get(); - //should read first message last from the outbox - _retrievedMessages.Last().Id.Should().Be(_messageEarliest.Id); - //should read last message first from the outbox - _retrievedMessages.First().Id.Should().Be(_messageLatest.Id); + //should read last message last from the outbox + _retrievedMessages.Last().Id.Should().Be(_messageThree.Id); + //should read first message first from the outbox + _retrievedMessages.First().Id.Should().Be(_messageOne.Id); //should read the messages from the outbox _retrievedMessages.Should().HaveCount(3); } diff --git a/tests/Paramore.Brighter.PostgresSQL.Tests/Paramore.Brighter.PostgresSQL.Tests.csproj b/tests/Paramore.Brighter.PostgresSQL.Tests/Paramore.Brighter.PostgresSQL.Tests.csproj index e1d1b913c7..fd81bac820 100644 --- a/tests/Paramore.Brighter.PostgresSQL.Tests/Paramore.Brighter.PostgresSQL.Tests.csproj +++ b/tests/Paramore.Brighter.PostgresSQL.Tests/Paramore.Brighter.PostgresSQL.Tests.csproj @@ -26,4 +26,10 @@ + + + ..\..\libs\Npgsql\net6.0\Npgsql.dll + + + diff --git a/tests/Paramore.Brighter.PostgresSQL.Tests/PostgresSqlTestHelper.cs b/tests/Paramore.Brighter.PostgresSQL.Tests/PostgresSqlTestHelper.cs index a13f50bf1a..9c4bea9578 100644 --- a/tests/Paramore.Brighter.PostgresSQL.Tests/PostgresSqlTestHelper.cs +++ b/tests/Paramore.Brighter.PostgresSQL.Tests/PostgresSqlTestHelper.cs @@ -10,13 +10,21 @@ namespace Paramore.Brighter.PostgresSQL.Tests { internal class PostgresSqlTestHelper { + private readonly bool _binaryMessagePayload; private static readonly ILogger s_logger = ApplicationLogging.CreateLogger(); private readonly PostgreSqlSettings _postgreSqlSettings; private string _tableName; private readonly object syncObject = new object(); - public PostgresSqlTestHelper() + public RelationalDatabaseConfiguration Configuration + => new(_postgreSqlSettings.TestsBrighterConnectionString, outBoxTableName: _tableName, binaryMessagePayload: _binaryMessagePayload); + + public RelationalDatabaseConfiguration InboxConfiguration + => new(_postgreSqlSettings.TestsBrighterConnectionString, inboxTableName: _tableName); + + public PostgresSqlTestHelper(bool binaryMessagePayload = false) { + _binaryMessagePayload = binaryMessagePayload; var builder = new ConfigurationBuilder().AddEnvironmentVariables(); var configuration = builder.Build(); @@ -26,10 +34,6 @@ public PostgresSqlTestHelper() _tableName = $"test_{Guid.NewGuid():N}"; } - public PostgreSqlOutboxConfiguration OutboxConfiguration => new PostgreSqlOutboxConfiguration(_postgreSqlSettings.TestsBrighterConnectionString, _tableName); - - public PostgresSqlInboxConfiguration InboxConfiguration => new PostgresSqlInboxConfiguration(_postgreSqlSettings.TestsBrighterConnectionString, _tableName); - public void SetupMessageDb() { @@ -92,7 +96,7 @@ private void CreateOutboxTable() using (var connection = new NpgsqlConnection(_postgreSqlSettings.TestsBrighterConnectionString)) { _tableName = $"message_{_tableName}"; - var createTableSql = PostgreSqlOutboxBulder.GetDDL(_tableName); + var createTableSql = PostgreSqlOutboxBulder.GetDDL(_tableName, Configuration.BinaryMessagePayload); connection.Open(); using (var command = connection.CreateCommand()) @@ -107,7 +111,7 @@ public void CreateInboxTable() using (var connection = new NpgsqlConnection(_postgreSqlSettings.TestsBrighterConnectionString)) { _tableName = $"command_{_tableName}"; - var createTableSql = PostgresSqlInboxBuilder.GetDDL(_tableName); + var createTableSql = PostgreSqlInboxBuilder.GetDDL(_tableName); connection.Open(); using (var command = connection.CreateCommand()) diff --git a/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_reading_a_delayed_message_via_the_messaging_gateway.cs b/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_reading_a_delayed_message_via_the_messaging_gateway.cs index 2ed8351860..044f48c2eb 100644 --- a/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_reading_a_delayed_message_via_the_messaging_gateway.cs +++ b/tests/Paramore.Brighter.RMQ.Tests/MessagingGateway/When_reading_a_delayed_message_via_the_messaging_gateway.cs @@ -96,7 +96,7 @@ public void When_requeing_a_failed_message_with_delay() _messageConsumer.Acknowledge(message); //now requeue with a delay - _message.UpdateHandledCount(); + _message.Header.UpdateHandledCount(); _messageConsumer.Requeue(_message, 1000); //receive and assert diff --git a/tests/Paramore.Brighter.Sqlite.Tests/Inbox/When_the_message_is_already_in_The_inbox_async.cs b/tests/Paramore.Brighter.Sqlite.Tests/Inbox/When_the_message_is_already_in_The_inbox_async.cs index 1554ada1af..e45d3b9c87 100644 --- a/tests/Paramore.Brighter.Sqlite.Tests/Inbox/When_the_message_is_already_in_The_inbox_async.cs +++ b/tests/Paramore.Brighter.Sqlite.Tests/Inbox/When_the_message_is_already_in_The_inbox_async.cs @@ -45,7 +45,7 @@ public SqliteInboxDuplicateMessageAsyncTests() _sqliteTestHelper = new SqliteTestHelper(); _sqliteTestHelper.SetupCommandDb(); - _sqlInbox = new SqliteInbox(new SqliteInboxConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableName)); + _sqlInbox = new SqliteInbox(_sqliteTestHelper.InboxConfiguration); _raisedCommand = new MyCommand {Value = "Test"}; _contextKey = "context-key"; } diff --git a/tests/Paramore.Brighter.Sqlite.Tests/Inbox/When_the_message_is_already_in_the_inbox.cs b/tests/Paramore.Brighter.Sqlite.Tests/Inbox/When_the_message_is_already_in_the_inbox.cs index 3ff9e52b42..dd349b3666 100644 --- a/tests/Paramore.Brighter.Sqlite.Tests/Inbox/When_the_message_is_already_in_the_inbox.cs +++ b/tests/Paramore.Brighter.Sqlite.Tests/Inbox/When_the_message_is_already_in_the_inbox.cs @@ -43,7 +43,7 @@ public SqliteInboxDuplicateMessageTests() { _sqliteTestHelper = new SqliteTestHelper(); _sqliteTestHelper.SetupCommandDb(); - _sqlInbox = new SqliteInbox(new SqliteInboxConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableName)); + _sqlInbox = new SqliteInbox(_sqliteTestHelper.InboxConfiguration); _raisedCommand = new MyCommand { Value = "Test" }; _contextKey = "context-key"; _sqlInbox.Add(_raisedCommand, _contextKey); diff --git a/tests/Paramore.Brighter.Sqlite.Tests/Inbox/When_there_is_no_message_in_the_sql_command_store.cs b/tests/Paramore.Brighter.Sqlite.Tests/Inbox/When_there_is_no_message_in_the_sql_command_store.cs index 3bde9d1acc..ce9a53c367 100644 --- a/tests/Paramore.Brighter.Sqlite.Tests/Inbox/When_there_is_no_message_in_the_sql_command_store.cs +++ b/tests/Paramore.Brighter.Sqlite.Tests/Inbox/When_there_is_no_message_in_the_sql_command_store.cs @@ -42,7 +42,7 @@ public SqliteInboxEmptyWhenSearchedTests() { _sqliteTestHelper = new SqliteTestHelper(); _sqliteTestHelper.SetupCommandDb(); - _sqlInbox = new SqliteInbox(new SqliteInboxConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableName)); + _sqlInbox = new SqliteInbox(_sqliteTestHelper.InboxConfiguration); _contextKey = "context-key"; } diff --git a/tests/Paramore.Brighter.Sqlite.Tests/Inbox/When_there_is_no_message_in_the_sql_command_store_async.cs b/tests/Paramore.Brighter.Sqlite.Tests/Inbox/When_there_is_no_message_in_the_sql_command_store_async.cs index 16d8d427f2..f252312176 100644 --- a/tests/Paramore.Brighter.Sqlite.Tests/Inbox/When_there_is_no_message_in_the_sql_command_store_async.cs +++ b/tests/Paramore.Brighter.Sqlite.Tests/Inbox/When_there_is_no_message_in_the_sql_command_store_async.cs @@ -44,7 +44,7 @@ public SqliteInboxEmptyWhenSearchedAsyncTests() _sqliteTestHelper = new SqliteTestHelper(); _sqliteTestHelper.SetupCommandDb(); - _sqlInbox = new SqliteInbox(new SqliteInboxConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableName)); + _sqlInbox = new SqliteInbox(_sqliteTestHelper.InboxConfiguration); _contextKey = "context-key"; } diff --git a/tests/Paramore.Brighter.Sqlite.Tests/Inbox/When_writing_a_message_to_the_command_store.cs b/tests/Paramore.Brighter.Sqlite.Tests/Inbox/When_writing_a_message_to_the_command_store.cs index 6313b32755..8110c1e9ab 100644 --- a/tests/Paramore.Brighter.Sqlite.Tests/Inbox/When_writing_a_message_to_the_command_store.cs +++ b/tests/Paramore.Brighter.Sqlite.Tests/Inbox/When_writing_a_message_to_the_command_store.cs @@ -44,7 +44,7 @@ public SqliteInboxAddMessageTests() _sqliteTestHelper = new SqliteTestHelper(); _sqliteTestHelper.SetupCommandDb(); - _sqlInbox = new SqliteInbox(new SqliteInboxConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableName)); + _sqlInbox = new SqliteInbox(_sqliteTestHelper.InboxConfiguration); _raisedCommand = new MyCommand {Value = "Test"}; _contextKey = "context-key"; _sqlInbox.Add(_raisedCommand, _contextKey); diff --git a/tests/Paramore.Brighter.Sqlite.Tests/Inbox/When_writing_a_message_to_the_command_store_async.cs b/tests/Paramore.Brighter.Sqlite.Tests/Inbox/When_writing_a_message_to_the_command_store_async.cs index 922cc1da45..34d70ee4ec 100644 --- a/tests/Paramore.Brighter.Sqlite.Tests/Inbox/When_writing_a_message_to_the_command_store_async.cs +++ b/tests/Paramore.Brighter.Sqlite.Tests/Inbox/When_writing_a_message_to_the_command_store_async.cs @@ -45,7 +45,7 @@ public SqliteInboxAddMessageAsyncTests() _sqliteTestHelper = new SqliteTestHelper(); _sqliteTestHelper.SetupCommandDb(); - _sqlInbox = new SqliteInbox(new SqliteInboxConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableName)); + _sqlInbox = new SqliteInbox(_sqliteTestHelper.InboxConfiguration); _raisedCommand = new MyCommand {Value = "Test"}; _contextKey = "context-key"; } diff --git a/tests/Paramore.Brighter.Sqlite.Tests/Outbox/SQlOutboxMigrationTests.cs b/tests/Paramore.Brighter.Sqlite.Tests/Outbox/SQlOutboxMigrationTests.cs index 07bb4e7153..82ff79bef8 100644 --- a/tests/Paramore.Brighter.Sqlite.Tests/Outbox/SQlOutboxMigrationTests.cs +++ b/tests/Paramore.Brighter.Sqlite.Tests/Outbox/SQlOutboxMigrationTests.cs @@ -34,7 +34,7 @@ namespace Paramore.Brighter.Sqlite.Tests.Outbox [Trait("Category", "Sqlite")] public class SQlOutboxMigrationTests : IDisposable { - private readonly SqliteOutboxSync _sqlOutboxSync; + private readonly SqliteOutbox _sqlOutbox; private readonly Message _message; private Message _storedMessage; private readonly SqliteTestHelper _sqliteTestHelper; @@ -43,7 +43,7 @@ public SQlOutboxMigrationTests() { _sqliteTestHelper = new SqliteTestHelper(); _sqliteTestHelper.SetupMessageDb(); - _sqlOutboxSync = new SqliteOutboxSync(new SqliteConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableName_Messages)); + _sqlOutbox = new SqliteOutbox(new RelationalDatabaseConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.OutboxTableName)); _message = new Message(new MessageHeader(Guid.NewGuid(), "test_topic", MessageType.MT_DOCUMENT), new MessageBody("message body")); AddHistoricMessage(_message); @@ -51,7 +51,7 @@ public SQlOutboxMigrationTests() private void AddHistoricMessage(Message message) { - var sql = string.Format("INSERT INTO {0} (MessageId, MessageType, Topic, Timestamp, HeaderBag, Body) VALUES (@MessageId, @MessageType, @Topic, @Timestamp, @HeaderBag, @Body)", _sqliteTestHelper.TableName_Messages); + var sql = string.Format("INSERT INTO {0} (MessageId, MessageType, Topic, Timestamp, HeaderBag, Body) VALUES (@MessageId, @MessageType, @Topic, @Timestamp, @HeaderBag, @Body)", _sqliteTestHelper.OutboxTableName); var parameters = new[] { new SqliteParameter("MessageId", message.Id.ToString()), @@ -80,7 +80,7 @@ private void AddHistoricMessage(Message message) [Fact] public void When_writing_a_message_with_minimal_header_information_to_the_outbox() { - _storedMessage = _sqlOutboxSync.Get(_message.Id); + _storedMessage = _sqlOutbox.Get(_message.Id); //_should_read_the_message_from_the__sql_outbox _storedMessage.Body.Value.Should().Be(_message.Body.Value); @@ -89,7 +89,8 @@ public void When_writing_a_message_with_minimal_header_information_to_the_outbox //_should_read_the_message_header_topic_from_the__sql_outbox _storedMessage.Header.Topic.Should().Be(_message.Header.Topic); //_should_default_the_timestamp_from_the__sql_outbox - _storedMessage.Header.TimeStamp.Should().BeOnOrAfter(_message.Header.TimeStamp); + _storedMessage.Header.TimeStamp.ToString("yyyy-MM-ddTHH:mm:ss").Should() + .Be(_message.Header.TimeStamp.ToString("yyyy-MM-ddTHH:mm:ss")); //_should_read_empty_header_bag_from_the__sql_outbox _storedMessage.Header.Bag.Keys.Should().BeEmpty(); } diff --git a/tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_Removing_Messages_From_The_Outbox.cs b/tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_Removing_Messages_From_The_Outbox.cs index 8c4734554b..18d7b674f7 100644 --- a/tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_Removing_Messages_From_The_Outbox.cs +++ b/tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_Removing_Messages_From_The_Outbox.cs @@ -36,7 +36,7 @@ namespace Paramore.Brighter.Sqlite.Tests.Outbox public class SqlOutboxDeletingMessagesTests { private readonly SqliteTestHelper _sqliteTestHelper; - private readonly SqliteOutboxSync _sqlOutboxSync; + private readonly SqliteOutbox _sqlOutbox; private readonly Message _messageEarliest; private readonly Message _message2; private readonly Message _messageLatest; @@ -46,7 +46,7 @@ public SqlOutboxDeletingMessagesTests() { _sqliteTestHelper = new SqliteTestHelper(); _sqliteTestHelper.SetupMessageDb(); - _sqlOutboxSync = new SqliteOutboxSync(new SqliteConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableName_Messages)); + _sqlOutbox = new SqliteOutbox(_sqliteTestHelper.OutboxConfiguration); _messageEarliest = new Message(new MessageHeader(Guid.NewGuid(), "Test", MessageType.MT_COMMAND, DateTime.UtcNow.AddHours(-3)), new MessageBody("Body")); _message2 = new Message(new MessageHeader(Guid.NewGuid(), "Test2", MessageType.MT_COMMAND, DateTime.UtcNow.AddHours(-2)), new MessageBody("Body2")); @@ -56,22 +56,22 @@ public SqlOutboxDeletingMessagesTests() [Fact] public void When_Removing_Messages_From_The_Outbox() { - _sqlOutboxSync.Add(_messageEarliest); - _sqlOutboxSync.Add(_message2); - _sqlOutboxSync.Add(_messageLatest); + _sqlOutbox.Add(_messageEarliest); + _sqlOutbox.Add(_message2); + _sqlOutbox.Add(_messageLatest); - _retrievedMessages = _sqlOutboxSync.Get(); - _sqlOutboxSync.Delete(_retrievedMessages.First().Id); + _retrievedMessages = _sqlOutbox.Get(); + _sqlOutbox.Delete(_retrievedMessages.First().Id); - var remainingMessages = _sqlOutboxSync.Get(); + var remainingMessages = _sqlOutbox.Get(); remainingMessages.Should().HaveCount(2); remainingMessages.Should().Contain(_retrievedMessages.ToList()[1]); remainingMessages.Should().Contain(_retrievedMessages.ToList()[2]); - _sqlOutboxSync.Delete(remainingMessages.Select(m => m.Id).ToArray()); + _sqlOutbox.Delete(remainingMessages.Select(m => m.Id).ToArray()); - var messages = _sqlOutboxSync.Get(); + var messages = _sqlOutbox.Get(); messages.Should().HaveCount(0); } diff --git a/tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_The_Message_Is_Already_In_The_Outbox.cs b/tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_The_Message_Is_Already_In_The_Outbox.cs index 8963b6f42c..48eee6bc3f 100644 --- a/tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_The_Message_Is_Already_In_The_Outbox.cs +++ b/tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_The_Message_Is_Already_In_The_Outbox.cs @@ -34,7 +34,7 @@ namespace Paramore.Brighter.Sqlite.Tests.Outbox public class SqliteOutboxMessageAlreadyExistsTests : IDisposable { private readonly SqliteTestHelper _sqliteTestHelper; - private readonly SqliteOutboxSync _sqlOutboxSync; + private readonly SqliteOutbox _sqlOutbox; private Exception _exception; private readonly Message _messageEarliest; @@ -42,16 +42,16 @@ public SqliteOutboxMessageAlreadyExistsTests() { _sqliteTestHelper = new SqliteTestHelper(); _sqliteTestHelper.SetupMessageDb(); - _sqlOutboxSync = new SqliteOutboxSync(new SqliteConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableName_Messages)); + _sqlOutbox = new SqliteOutbox(_sqliteTestHelper.OutboxConfiguration); _messageEarliest = new Message(new MessageHeader(Guid.NewGuid(), "test_topic", MessageType.MT_DOCUMENT), new MessageBody("message body")); - _sqlOutboxSync.Add(_messageEarliest); + _sqlOutbox.Add(_messageEarliest); } [Fact] public void When_The_Message_Is_Already_In_The_Outbox() { - _exception = Catch.Exception(() => _sqlOutboxSync.Add(_messageEarliest)); + _exception = Catch.Exception(() => _sqlOutbox.Add(_messageEarliest)); //should ignore the duplicate key and still succeed _exception.Should().BeNull(); diff --git a/tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_The_Message_Is_Already_In_The_Outbox_Async.cs b/tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_The_Message_Is_Already_In_The_Outbox_Async.cs index 5e93e6574b..27336c26a3 100644 --- a/tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_The_Message_Is_Already_In_The_Outbox_Async.cs +++ b/tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_The_Message_Is_Already_In_The_Outbox_Async.cs @@ -35,7 +35,7 @@ namespace Paramore.Brighter.Sqlite.Tests.Outbox public class SqliteOutboxMessageAlreadyExistsAsyncTests : IDisposable { private readonly SqliteTestHelper _sqliteTestHelper; - private readonly SqliteOutboxSync _sqlOutboxSync; + private readonly SqliteOutbox _sqlOutbox; private Exception _exception; private readonly Message _messageEarliest; @@ -43,7 +43,7 @@ public SqliteOutboxMessageAlreadyExistsAsyncTests() { _sqliteTestHelper = new SqliteTestHelper(); _sqliteTestHelper.SetupMessageDb(); - _sqlOutboxSync = new SqliteOutboxSync(new SqliteConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableName_Messages)); + _sqlOutbox = new SqliteOutbox(_sqliteTestHelper.OutboxConfiguration); _messageEarliest = new Message( new MessageHeader( Guid.NewGuid(), @@ -55,9 +55,9 @@ public SqliteOutboxMessageAlreadyExistsAsyncTests() [Fact] public async Task When_The_Message_Is_Already_In_The_Outbox_Async() { - await _sqlOutboxSync.AddAsync(_messageEarliest); + await _sqlOutbox.AddAsync(_messageEarliest); - _exception = await Catch.ExceptionAsync(() => _sqlOutboxSync.AddAsync(_messageEarliest)); + _exception = await Catch.ExceptionAsync(() => _sqlOutbox.AddAsync(_messageEarliest)); //should ignore the duplicate key and still succeed _exception.Should().BeNull(); diff --git a/tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_There_Are_Multiple_Messages_In_The_Outbox_And_A_Range_Is_Fetched.cs b/tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_There_Are_Multiple_Messages_In_The_Outbox_And_A_Range_Is_Fetched.cs index e46cf06ec6..8185390aa7 100644 --- a/tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_There_Are_Multiple_Messages_In_The_Outbox_And_A_Range_Is_Fetched.cs +++ b/tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_There_Are_Multiple_Messages_In_The_Outbox_And_A_Range_Is_Fetched.cs @@ -37,7 +37,7 @@ namespace Paramore.Brighter.Sqlite.Tests.Outbox public class SqliteOutboxRangeRequestTests : IDisposable { private readonly SqliteTestHelper _sqliteTestHelper; - private readonly SqliteOutboxSync _sqlOutboxSync; + private readonly SqliteOutbox _sqlOutbox; private readonly string _TopicFirstMessage = "test_topic"; private readonly string _TopicLastMessage = "test_topic3"; private IEnumerable messages; @@ -49,21 +49,21 @@ public SqliteOutboxRangeRequestTests() { _sqliteTestHelper = new SqliteTestHelper(); _sqliteTestHelper.SetupMessageDb(); - _sqlOutboxSync = new SqliteOutboxSync(new SqliteConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableName_Messages)); + _sqlOutbox = new SqliteOutbox(_sqliteTestHelper.OutboxConfiguration); _messageEarliest = new Message(new MessageHeader(Guid.NewGuid(), _TopicFirstMessage, MessageType.MT_DOCUMENT), new MessageBody("message body")); _message1 = new Message(new MessageHeader(Guid.NewGuid(), "test_topic2", MessageType.MT_DOCUMENT), new MessageBody("message body2")); _message2 = new Message(new MessageHeader(Guid.NewGuid(), _TopicLastMessage, MessageType.MT_DOCUMENT), new MessageBody("message body3")); - _sqlOutboxSync.Add(_messageEarliest); + _sqlOutbox.Add(_messageEarliest); Task.Delay(100); - _sqlOutboxSync.Add(_message1); + _sqlOutbox.Add(_message1); Task.Delay(100); - _sqlOutboxSync.Add(_message2); + _sqlOutbox.Add(_message2); } [Fact] public void When_There_Are_Multiple_Messages_In_The_Outbox_And_A_Range_Is_Fetched() { - messages = _sqlOutboxSync.Get(1, 3); + messages = _sqlOutbox.Get(1, 3); //should fetch 1 message messages.Should().HaveCount(1); diff --git a/tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_There_Are_Multiple_Messages_In_The_Outbox_And_A_Range_Is_Fetched_Async.cs b/tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_There_Are_Multiple_Messages_In_The_Outbox_And_A_Range_Is_Fetched_Async.cs index 2b1b09484c..eafb4e3642 100644 --- a/tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_There_Are_Multiple_Messages_In_The_Outbox_And_A_Range_Is_Fetched_Async.cs +++ b/tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_There_Are_Multiple_Messages_In_The_Outbox_And_A_Range_Is_Fetched_Async.cs @@ -37,7 +37,7 @@ namespace Paramore.Brighter.Sqlite.Tests.Outbox public class SqliteOutboxRangeRequestAsyncTests : IDisposable { private readonly SqliteTestHelper _sqliteTestHelper; - private readonly SqliteOutboxSync _sqlOutboxSync; + private readonly SqliteOutbox _sqlOutbox; private readonly string _TopicFirstMessage = "test_topic"; private readonly string _TopicLastMessage = "test_topic3"; private IEnumerable _messages; @@ -49,7 +49,7 @@ public SqliteOutboxRangeRequestAsyncTests() { _sqliteTestHelper = new SqliteTestHelper(); _sqliteTestHelper.SetupMessageDb(); - _sqlOutboxSync = new SqliteOutboxSync(new SqliteConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableName_Messages)); + _sqlOutbox = new SqliteOutbox(_sqliteTestHelper.OutboxConfiguration); _messageEarliest = new Message(new MessageHeader(Guid.NewGuid(), _TopicFirstMessage, MessageType.MT_DOCUMENT), new MessageBody("message body")); _message1 = new Message(new MessageHeader(Guid.NewGuid(), "test_topic2", MessageType.MT_DOCUMENT), new MessageBody("message body2")); _message2 = new Message(new MessageHeader(Guid.NewGuid(), _TopicLastMessage, MessageType.MT_DOCUMENT), new MessageBody("message body3")); @@ -58,11 +58,11 @@ public SqliteOutboxRangeRequestAsyncTests() [Fact] public async Task When_There_Are_Multiple_Messages_In_The_Outbox_And_A_Range_Is_Fetched_Async() { - await _sqlOutboxSync.AddAsync(_messageEarliest); - await _sqlOutboxSync.AddAsync(_message1); - await _sqlOutboxSync.AddAsync(_message2); + await _sqlOutbox.AddAsync(_messageEarliest); + await _sqlOutbox.AddAsync(_message1); + await _sqlOutbox.AddAsync(_message2); - _messages = await _sqlOutboxSync.GetAsync(1, 3); + _messages = await _sqlOutbox.GetAsync(1, 3); //should fetch 1 message _messages.Should().HaveCount(1); diff --git a/tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_There_Is_No_Message_In_The_Sql_Outbox.cs b/tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_There_Is_No_Message_In_The_Sql_Outbox.cs index 007925b76c..62040fcd87 100644 --- a/tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_There_Is_No_Message_In_The_Sql_Outbox.cs +++ b/tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_There_Is_No_Message_In_The_Sql_Outbox.cs @@ -34,7 +34,7 @@ namespace Paramore.Brighter.Sqlite.Tests.Outbox public class SqliteOutboxEmptyStoreTests : IDisposable { private readonly SqliteTestHelper _sqliteTestHelper; - private readonly SqliteOutboxSync _sqlOutboxSync; + private readonly SqliteOutbox _sqlOutbox; private readonly Message _messageEarliest; private Message _storedMessage; @@ -42,7 +42,7 @@ public SqliteOutboxEmptyStoreTests() { _sqliteTestHelper = new SqliteTestHelper(); _sqliteTestHelper.SetupMessageDb(); - _sqlOutboxSync = new SqliteOutboxSync(new SqliteConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableName_Messages)); + _sqlOutbox = new SqliteOutbox(_sqliteTestHelper.OutboxConfiguration); _messageEarliest = new Message(new MessageHeader(Guid.NewGuid(), "test_topic", MessageType.MT_DOCUMENT), new MessageBody("message body")); } @@ -50,7 +50,7 @@ public SqliteOutboxEmptyStoreTests() [Fact] public void When_There_Is_No_Message_In_The_Sql_Outbox() { - _storedMessage = _sqlOutboxSync.Get(_messageEarliest.Id); + _storedMessage = _sqlOutbox.Get(_messageEarliest.Id); //should return a empty message _storedMessage.Header.MessageType.Should().Be(MessageType.MT_NONE); diff --git a/tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_There_Is_No_Message_In_The_Sql_Outbox_Async.cs b/tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_There_Is_No_Message_In_The_Sql_Outbox_Async.cs index 1c5a1057f8..12b620cc2d 100644 --- a/tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_There_Is_No_Message_In_The_Sql_Outbox_Async.cs +++ b/tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_There_Is_No_Message_In_The_Sql_Outbox_Async.cs @@ -35,7 +35,7 @@ namespace Paramore.Brighter.Sqlite.Tests.Outbox public class SqliteOutboxEmptyStoreAsyncTests : IDisposable { private readonly SqliteTestHelper _sqliteTestHelper; - private readonly SqliteOutboxSync _sqlOutboxSync; + private readonly SqliteOutbox _sqlOutbox; private readonly Message _messageEarliest; private Message _storedMessage; @@ -43,7 +43,7 @@ public SqliteOutboxEmptyStoreAsyncTests() { _sqliteTestHelper = new SqliteTestHelper(); _sqliteTestHelper.SetupMessageDb(); - _sqlOutboxSync = new SqliteOutboxSync(new SqliteConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableName_Messages)); + _sqlOutbox = new SqliteOutbox(_sqliteTestHelper.OutboxConfiguration); _messageEarliest = new Message(new MessageHeader(Guid.NewGuid(), "test_topic", MessageType.MT_DOCUMENT), new MessageBody("message body")); } @@ -51,7 +51,7 @@ public SqliteOutboxEmptyStoreAsyncTests() [Fact] public async Task When_There_Is_No_Message_In_The_Sql_Outbox_Async() { - _storedMessage = await _sqlOutboxSync.GetAsync(_messageEarliest.Id); + _storedMessage = await _sqlOutbox.GetAsync(_messageEarliest.Id); //should return a empty message _storedMessage.Header.MessageType.Should().Be(MessageType.MT_NONE); diff --git a/tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_Writing_A_Message_To_A_Binary_Body_Outbox.cs b/tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_Writing_A_Message_To_A_Binary_Body_Outbox.cs new file mode 100644 index 0000000000..48526f23c1 --- /dev/null +++ b/tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_Writing_A_Message_To_A_Binary_Body_Outbox.cs @@ -0,0 +1,120 @@ +#region Licence + +/* The MIT License (MIT) +Copyright © 2014 Francesco Pighi + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the “Software”), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +using System; +using System.Text; +using FluentAssertions; +using Paramore.Brighter.Outbox.Sqlite; +using Xunit; + +namespace Paramore.Brighter.Sqlite.Tests.Outbox +{ + [Trait("Category", "Sqlite")] + public class SqliteOutboxWritingBinaryMessageTests : IDisposable + { + private readonly SqliteTestHelper _sqliteTestHelper; + private readonly SqliteOutbox _sqlOutbox; + private readonly string _key1 = "name1"; + private readonly string _key2 = "name2"; + private readonly string _key3 = "name3"; + private readonly string _key4 = "name4"; + private readonly string _key5 = "name5"; + private readonly string _value1 = "_value1"; + private readonly string _value2 = "_value2"; + private readonly int _value3 = 123; + private readonly Guid _value4 = Guid.NewGuid(); + private readonly DateTime _value5 = DateTime.UtcNow; + private readonly Message _messageEarliest; + private Message _storedMessage; + + public SqliteOutboxWritingBinaryMessageTests() + { + _sqliteTestHelper = new SqliteTestHelper(binaryMessagePayload: true); + _sqliteTestHelper.SetupMessageDb(); + _sqlOutbox = new SqliteOutbox(_sqliteTestHelper.OutboxConfiguration); + var messageHeader = new MessageHeader( + messageId:Guid.NewGuid(), + topic: "test_topic", + messageType:MessageType.MT_DOCUMENT, + timeStamp: DateTime.UtcNow.AddDays(-1), + handledCount:5, + delayedMilliseconds:5, + correlationId: Guid.NewGuid(), + replyTo: "ReplyTo", + contentType: "application/octet-stream", + partitionKey: "123456789"); + messageHeader.Bag.Add(_key1, _value1); + messageHeader.Bag.Add(_key2, _value2); + messageHeader.Bag.Add(_key3, _value3); + messageHeader.Bag.Add(_key4, _value4); + messageHeader.Bag.Add(_key5, _value5); + + //get the string as raw bytes + var bytes = System.Text.Encoding.UTF8.GetBytes("message body"); + + _messageEarliest = new Message(messageHeader, new MessageBody(bytes, contentType:"application/octet-stream", CharacterEncoding.Raw)); + _sqlOutbox.Add(_messageEarliest); + } + + [Fact] + public void When_Writing_A_Message_To_The_Outbox() + { + _storedMessage = _sqlOutbox.Get(_messageEarliest.Id); + //should read the message from the sql outbox + _storedMessage.Body.Bytes.Should().Equal(_messageEarliest.Body.Bytes); + var bodyAsString = Encoding.UTF8.GetString(_storedMessage.Body.Bytes); + bodyAsString.Should().Be("message body"); + //should read the header from the sql outbox + _storedMessage.Header.Topic.Should().Be(_messageEarliest.Header.Topic); + _storedMessage.Header.MessageType.Should().Be(_messageEarliest.Header.MessageType); + _storedMessage.Header.TimeStamp.ToString("yyyy-MM-ddTHH:mm:ss") + .Should().Be(_messageEarliest.Header.TimeStamp.ToString("yyyy-MM-ddTHH:mm:ss")); + _storedMessage.Header.HandledCount.Should().Be(0); // -- should be zero when read from outbox + _storedMessage.Header.DelayedMilliseconds.Should().Be(0); // -- should be zero when read from outbox + _storedMessage.Header.CorrelationId.Should().Be(_messageEarliest.Header.CorrelationId); + _storedMessage.Header.ReplyTo.Should().Be(_messageEarliest.Header.ReplyTo); + _storedMessage.Header.ContentType.Should().Be(_messageEarliest.Header.ContentType); + _storedMessage.Header.PartitionKey.Should().Be(_messageEarliest.Header.PartitionKey); + + + //Bag serialization + _storedMessage.Header.Bag.ContainsKey(_key1).Should().BeTrue(); + _storedMessage.Header.Bag[_key1].Should().Be(_value1); + _storedMessage.Header.Bag.ContainsKey(_key2).Should().BeTrue(); + _storedMessage.Header.Bag[_key2].Should().Be(_value2); + _storedMessage.Header.Bag.ContainsKey(_key3).Should().BeTrue(); + _storedMessage.Header.Bag[_key3].Should().Be(_value3); + _storedMessage.Header.Bag.ContainsKey(_key4).Should().BeTrue(); + _storedMessage.Header.Bag[_key4].Should().Be(_value4); + _storedMessage.Header.Bag.ContainsKey(_key5).Should().BeTrue(); + _storedMessage.Header.Bag[_key5].Should().Be(_value5); + } + + public void Dispose() + { + _sqliteTestHelper.CleanUpDb(); + } + } +} diff --git a/tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_Writing_A_Message_To_The_Outbox.cs b/tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_Writing_A_Message_To_The_Outbox.cs index 8c61db7b57..889453936e 100644 --- a/tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_Writing_A_Message_To_The_Outbox.cs +++ b/tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_Writing_A_Message_To_The_Outbox.cs @@ -34,7 +34,7 @@ namespace Paramore.Brighter.Sqlite.Tests.Outbox public class SqliteOutboxWritingMessageTests : IDisposable { private readonly SqliteTestHelper _sqliteTestHelper; - private readonly SqliteOutboxSync _sqlOutboxSync; + private readonly SqliteOutbox _sqlOutbox; private readonly string _key1 = "name1"; private readonly string _key2 = "name2"; private readonly string _key3 = "name3"; @@ -52,7 +52,7 @@ public SqliteOutboxWritingMessageTests() { _sqliteTestHelper = new SqliteTestHelper(); _sqliteTestHelper.SetupMessageDb(); - _sqlOutboxSync = new SqliteOutboxSync(new SqliteConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableName_Messages)); + _sqlOutbox = new SqliteOutbox(_sqliteTestHelper.OutboxConfiguration); var messageHeader = new MessageHeader( messageId:Guid.NewGuid(), topic: "test_topic", @@ -62,7 +62,8 @@ public SqliteOutboxWritingMessageTests() delayedMilliseconds:5, correlationId: Guid.NewGuid(), replyTo: "ReplyTo", - contentType: "text/plain"); + contentType: "text/plain", + partitionKey: Guid.NewGuid().ToString()); messageHeader.Bag.Add(_key1, _value1); messageHeader.Bag.Add(_key2, _value2); messageHeader.Bag.Add(_key3, _value3); @@ -70,24 +71,26 @@ public SqliteOutboxWritingMessageTests() messageHeader.Bag.Add(_key5, _value5); _messageEarliest = new Message(messageHeader, new MessageBody("message body")); - _sqlOutboxSync.Add(_messageEarliest); + _sqlOutbox.Add(_messageEarliest); } [Fact] public void When_Writing_A_Message_To_The_Outbox() { - _storedMessage = _sqlOutboxSync.Get(_messageEarliest.Id); + _storedMessage = _sqlOutbox.Get(_messageEarliest.Id); //should read the message from the sql outbox _storedMessage.Body.Value.Should().Be(_messageEarliest.Body.Value); //should read the header from the sql outbox _storedMessage.Header.Topic.Should().Be(_messageEarliest.Header.Topic); _storedMessage.Header.MessageType.Should().Be(_messageEarliest.Header.MessageType); - _storedMessage.Header.TimeStamp.Should().Be(_messageEarliest.Header.TimeStamp); + _storedMessage.Header.TimeStamp.ToString("yyyy-MM-ddTHH:mm:ss") + .Should().Be(_messageEarliest.Header.TimeStamp.ToString("yyyy-MM-ddTHH:mm:ss")); _storedMessage.Header.HandledCount.Should().Be(0); // -- should be zero when read from outbox _storedMessage.Header.DelayedMilliseconds.Should().Be(0); // -- should be zero when read from outbox _storedMessage.Header.CorrelationId.Should().Be(_messageEarliest.Header.CorrelationId); _storedMessage.Header.ReplyTo.Should().Be(_messageEarliest.Header.ReplyTo); _storedMessage.Header.ContentType.Should().Be(_messageEarliest.Header.ContentType); + _storedMessage.Header.PartitionKey.Should().Be(_messageEarliest.Header.PartitionKey); //Bag serialization diff --git a/tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_Writing_A_Message_To_The_Outbox_Async.cs b/tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_Writing_A_Message_To_The_Outbox_Async.cs index a612ddc35a..661479ab49 100644 --- a/tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_Writing_A_Message_To_The_Outbox_Async.cs +++ b/tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_Writing_A_Message_To_The_Outbox_Async.cs @@ -34,7 +34,7 @@ namespace Paramore.Brighter.Sqlite.Tests.Outbox public class SqliteOutboxWritingMessageAsyncTests : IDisposable { private readonly SqliteTestHelper _sqliteTestHelper; - private readonly SqliteOutboxSync _sqlOutboxSync; + private readonly SqliteOutbox _sqlOutbox; private readonly string _key1 = "name1"; private readonly string _key2 = "name2"; private readonly string _key3 = "name3"; @@ -52,8 +52,8 @@ public SqliteOutboxWritingMessageAsyncTests() { _sqliteTestHelper = new SqliteTestHelper(); _sqliteTestHelper.SetupMessageDb(); - _sqlOutboxSync = new SqliteOutboxSync(new SqliteConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableName_Messages)); - + _sqlOutbox = new SqliteOutbox(_sqliteTestHelper.OutboxConfiguration); + var messageHeader = new MessageHeader( Guid.NewGuid(), "test_topic", @@ -71,15 +71,16 @@ public SqliteOutboxWritingMessageAsyncTests() [Fact] public async Task When_Writing_A_Message_To_The_Outbox_Async() { - await _sqlOutboxSync.AddAsync(_messageEarliest); + await _sqlOutbox.AddAsync(_messageEarliest); - _storedMessage = await _sqlOutboxSync.GetAsync(_messageEarliest.Id); + _storedMessage = await _sqlOutbox.GetAsync(_messageEarliest.Id); //should read the message from the sql outbox _storedMessage.Body.Value.Should().Be(_messageEarliest.Body.Value); //should read the message header first bag item from the sql outbox //should read the message header timestamp from the sql outbox - _storedMessage.Header.TimeStamp.Should().Be(_messageEarliest.Header.TimeStamp); + _storedMessage.Header.TimeStamp.ToString("yyyy-MM-ddTHH:mm:ss") + .Should().Be(_messageEarliest.Header.TimeStamp.ToString("yyyy-MM-ddTHH:mm:ss")); //should read the message header topic from the sql outbox = _storedMessage.Header.Topic.Should().Be(_messageEarliest.Header.Topic); //should read the message header type from the sql outbox diff --git a/tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_Writing_Messages_To_The_Outbox.cs b/tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_Writing_Messages_To_The_Outbox.cs index 0ff7ede57f..a526d8d427 100644 --- a/tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_Writing_Messages_To_The_Outbox.cs +++ b/tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_Writing_Messages_To_The_Outbox.cs @@ -36,37 +36,37 @@ namespace Paramore.Brighter.Sqlite.Tests.Outbox public class SqlOutboxWritngMessagesTests { private readonly SqliteTestHelper _sqliteTestHelper; - private readonly SqliteOutboxSync _sqlOutboxSync; - private readonly Message _messageEarliest; - private readonly Message _message2; - private readonly Message _messageLatest; + private readonly SqliteOutbox _sqlOutbox; + private readonly Message _messageOne; + private readonly Message _messageTwo; + private readonly Message _messageThree; private IEnumerable _retrievedMessages; public SqlOutboxWritngMessagesTests() { _sqliteTestHelper = new SqliteTestHelper(); _sqliteTestHelper.SetupMessageDb(); - _sqlOutboxSync = new SqliteOutboxSync(new SqliteConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableName_Messages)); - - _messageEarliest = new Message(new MessageHeader(Guid.NewGuid(), "Test", MessageType.MT_COMMAND, DateTime.UtcNow.AddHours(-3)), new MessageBody("Body")); - _message2 = new Message(new MessageHeader(Guid.NewGuid(), "Test2", MessageType.MT_COMMAND, DateTime.UtcNow.AddHours(-2)), new MessageBody("Body2")); - _messageLatest = new Message(new MessageHeader(Guid.NewGuid(), "Test3", MessageType.MT_COMMAND, DateTime.UtcNow.AddHours(-1)), new MessageBody("Body3")); + _sqlOutbox = new SqliteOutbox(_sqliteTestHelper.OutboxConfiguration); + + _messageOne = new Message(new MessageHeader(Guid.NewGuid(), "Test", MessageType.MT_COMMAND, DateTime.UtcNow.AddHours(-3)), new MessageBody("Body")); + _messageTwo = new Message(new MessageHeader(Guid.NewGuid(), "Test2", MessageType.MT_COMMAND, DateTime.UtcNow.AddHours(-2)), new MessageBody("Body2")); + _messageThree = new Message(new MessageHeader(Guid.NewGuid(), "Test3", MessageType.MT_COMMAND, DateTime.UtcNow.AddHours(-1)), new MessageBody("Body3")); } [Fact] public void When_Writing_Messages_To_The_Outbox() { - _sqlOutboxSync.Add(_messageEarliest); - _sqlOutboxSync.Add(_message2); - _sqlOutboxSync.Add(_messageLatest); + _sqlOutbox.Add(_messageOne); + _sqlOutbox.Add(_messageTwo); + _sqlOutbox.Add(_messageThree); - _retrievedMessages = _sqlOutboxSync.Get(); + _retrievedMessages = _sqlOutbox.Get(); - //should read first message last from the outbox - _retrievedMessages.Last().Id.Should().Be(_messageEarliest.Id); - //should read last message first from the outbox - _retrievedMessages.First().Id.Should().Be(_messageLatest.Id); + //should read last message last from the outbox + _retrievedMessages.Last().Id.Should().Be(_messageThree.Id); + //should read first message first from the outbox + _retrievedMessages.First().Id.Should().Be(_messageOne.Id); //should read the messages from the outbox _retrievedMessages.Should().HaveCount(3); } @@ -74,14 +74,14 @@ public void When_Writing_Messages_To_The_Outbox() [Fact] public void When_Writing_Messages_To_The_Outbox_Bulk() { - _sqlOutboxSync.Add(new List {_messageEarliest, _message2, _messageLatest}); + _sqlOutbox.Add(new List {_messageOne, _messageTwo, _messageThree}); - _retrievedMessages = _sqlOutboxSync.Get(); + _retrievedMessages = _sqlOutbox.Get(); //should read first message last from the outbox - _retrievedMessages.Last().Id.Should().Be(_messageEarliest.Id); + _retrievedMessages.Last().Id.Should().Be(_messageThree.Id); //should read last message first from the outbox - _retrievedMessages.First().Id.Should().Be(_messageLatest.Id); + _retrievedMessages.First().Id.Should().Be(_messageOne.Id); //should read the messages from the outbox _retrievedMessages.Should().HaveCount(3); } diff --git a/tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_Writing_Messages_To_The_Outbox_Async.cs b/tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_Writing_Messages_To_The_Outbox_Async.cs index 9bc0617fce..4ad121de2a 100644 --- a/tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_Writing_Messages_To_The_Outbox_Async.cs +++ b/tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_Writing_Messages_To_The_Outbox_Async.cs @@ -37,17 +37,17 @@ namespace Paramore.Brighter.Sqlite.Tests.Outbox public class SqlOutboxWritngMessagesAsyncTests : IDisposable { private readonly SqliteTestHelper _sqliteTestHelper; - private readonly SqliteOutboxSync _sSqlOutboxSync; - private Message _message2; - private Message _messageEarliest; - private Message _messageLatest; + private readonly SqliteOutbox _sqlOutbox; + private Message _messageTwo; + private Message _messageOne; + private Message _messageThree; private IList _retrievedMessages; public SqlOutboxWritngMessagesAsyncTests() { _sqliteTestHelper = new SqliteTestHelper(); _sqliteTestHelper.SetupMessageDb(); - _sSqlOutboxSync = new SqliteOutboxSync(new SqliteConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableName_Messages)); + _sqlOutbox = new SqliteOutbox(_sqliteTestHelper.OutboxConfiguration); } [Fact] @@ -55,12 +55,12 @@ public async Task When_Writing_Messages_To_The_Outbox_Async() { await SetUpMessagesAsync(); - _retrievedMessages = await _sSqlOutboxSync.GetAsync(); + _retrievedMessages = await _sqlOutbox.GetAsync(); - //should read first message last from the outbox - _retrievedMessages.Last().Id.Should().Be(_messageEarliest.Id); - //should read last message first from the outbox - _retrievedMessages.First().Id.Should().Be(_messageLatest.Id); + //should read last message last from the outbox + _retrievedMessages.Last().Id.Should().Be(_messageThree.Id); + //should read first message first from the outbox + _retrievedMessages.First().Id.Should().Be(_messageOne.Id); //should read the messages from the outbox _retrievedMessages.Should().HaveCount(3); } @@ -69,32 +69,30 @@ public async Task When_Writing_Messages_To_The_Outbox_Async() public async Task When_Writing_Messages_To_The_Outbox_Async_Bulk() { var messages = await SetUpMessagesAsync(false); - await _sSqlOutboxSync.AddAsync(messages); + await _sqlOutbox.AddAsync(messages); - _retrievedMessages = await _sSqlOutboxSync.GetAsync(); + _retrievedMessages = await _sqlOutbox.GetAsync(); - //should read first message last from the outbox - _retrievedMessages.Last().Id.Should().Be(_messageEarliest.Id); + //should read last message last from the outbox + _retrievedMessages.Last().Id.Should().Be(_messageThree.Id); //should read last message first from the outbox - _retrievedMessages.First().Id.Should().Be(_messageLatest.Id); + _retrievedMessages.First().Id.Should().Be(_messageOne.Id); //should read the messages from the outbox _retrievedMessages.Should().HaveCount(3); - - } private async Task> SetUpMessagesAsync(bool addMessagesToOutbox = true) { - _messageEarliest = new Message(new MessageHeader(Guid.NewGuid(), "Test", MessageType.MT_COMMAND, DateTime.UtcNow.AddHours(-3)), new MessageBody("Body")); - if(addMessagesToOutbox) await _sSqlOutboxSync.AddAsync(_messageEarliest); + _messageOne = new Message(new MessageHeader(Guid.NewGuid(), "Test", MessageType.MT_COMMAND, DateTime.UtcNow.AddHours(-3)), new MessageBody("Body")); + if(addMessagesToOutbox) await _sqlOutbox.AddAsync(_messageOne); - _message2 = new Message(new MessageHeader(Guid.NewGuid(), "Test2", MessageType.MT_COMMAND, DateTime.UtcNow.AddHours(-2)), new MessageBody("Body2")); - if(addMessagesToOutbox) await _sSqlOutboxSync.AddAsync(_message2); + _messageTwo = new Message(new MessageHeader(Guid.NewGuid(), "Test2", MessageType.MT_COMMAND, DateTime.UtcNow.AddHours(-2)), new MessageBody("Body2")); + if(addMessagesToOutbox) await _sqlOutbox.AddAsync(_messageTwo); - _messageLatest = new Message(new MessageHeader(Guid.NewGuid(), "Test3", MessageType.MT_COMMAND, DateTime.UtcNow.AddHours(-1)), new MessageBody("Body3")); - if(addMessagesToOutbox) await _sSqlOutboxSync.AddAsync(_messageLatest); + _messageThree = new Message(new MessageHeader(Guid.NewGuid(), "Test3", MessageType.MT_COMMAND, DateTime.UtcNow.AddHours(-1)), new MessageBody("Body3")); + if(addMessagesToOutbox) await _sqlOutbox.AddAsync(_messageThree); - return new List { _messageEarliest, _message2, _messageLatest }; + return new List { _messageOne, _messageTwo, _messageThree }; } private void Release() diff --git a/tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_there_are_multiple_messages_and_some_are_received_and_Dispatched_bulk_Async.cs b/tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_there_are_multiple_messages_and_some_are_received_and_Dispatched_bulk_Async.cs index 866494cd74..94d32711b5 100644 --- a/tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_there_are_multiple_messages_and_some_are_received_and_Dispatched_bulk_Async.cs +++ b/tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_there_are_multiple_messages_and_some_are_received_and_Dispatched_bulk_Async.cs @@ -18,14 +18,14 @@ public class SqliteOutboxBulkGetAsyncTests :IDisposable private readonly Message _message2; private readonly Message _message3; private readonly Message _message; - private readonly SqliteOutboxSync _sqlOutbox; + private readonly SqliteOutbox _sqlOutbox; public SqliteOutboxBulkGetAsyncTests() { _sqliteTestHelper = new SqliteTestHelper(); _sqliteTestHelper.SetupMessageDb(); - _sqlOutbox = new SqliteOutboxSync(new SqliteConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableName_Messages)); - + _sqlOutbox = new SqliteOutbox(_sqliteTestHelper.OutboxConfiguration); + _message = new Message(new MessageHeader(Guid.NewGuid(), _Topic1, MessageType.MT_COMMAND), new MessageBody("message body")); _message1 = new Message(new MessageHeader(Guid.NewGuid(), _Topic2, MessageType.MT_EVENT), diff --git a/tests/Paramore.Brighter.Sqlite.Tests/SqliteTestHelper.cs b/tests/Paramore.Brighter.Sqlite.Tests/SqliteTestHelper.cs index c881cd3640..56a89e8a0e 100644 --- a/tests/Paramore.Brighter.Sqlite.Tests/SqliteTestHelper.cs +++ b/tests/Paramore.Brighter.Sqlite.Tests/SqliteTestHelper.cs @@ -8,42 +8,53 @@ namespace Paramore.Brighter.Sqlite.Tests { public class SqliteTestHelper { + private readonly bool _binaryMessagePayload; private const string TestDbPath = "test.db"; public string ConnectionString = $"DataSource=\"{TestDbPath}\""; - public string TableName = "test_commands"; - public string TableName_Messages = "test_messages"; - private string connectionStringPath; - private string connectionStringPathDir; + public readonly string InboxTableName = "test_commands"; + public readonly string OutboxTableName = "test_messages"; + private string _connectionStringPath; + private string _connectionStringPathDir; + + public RelationalDatabaseConfiguration InboxConfiguration => new(ConnectionString, inboxTableName: InboxTableName); + + public RelationalDatabaseConfiguration OutboxConfiguration => + new(ConnectionString, outBoxTableName: OutboxTableName, binaryMessagePayload: _binaryMessagePayload); + + public SqliteTestHelper(bool binaryMessagePayload = false) + { + _binaryMessagePayload = binaryMessagePayload; + } public void SetupCommandDb() { - connectionStringPath = GetUniqueTestDbPathAndCreateDir(); - ConnectionString = $"DataSource=\"{connectionStringPath}\""; - CreateDatabaseWithTable(ConnectionString, SqliteInboxBuilder.GetDDL(TableName)); + _connectionStringPath = GetUniqueTestDbPathAndCreateDir(); + ConnectionString = $"DataSource=\"{_connectionStringPath}\""; + CreateDatabaseWithTable(ConnectionString, SqliteInboxBuilder.GetDDL(InboxTableName)); } public void SetupMessageDb() { - connectionStringPath = GetUniqueTestDbPathAndCreateDir(); - ConnectionString = $"DataSource=\"{connectionStringPath}\""; - CreateDatabaseWithTable(ConnectionString, SqliteOutboxBuilder.GetDDL(TableName_Messages)); + _connectionStringPath = GetUniqueTestDbPathAndCreateDir(); + ConnectionString = $"DataSource=\"{_connectionStringPath}\""; + CreateDatabaseWithTable(ConnectionString, SqliteOutboxBuilder.GetDDL(OutboxTableName, hasBinaryMessagePayload: _binaryMessagePayload)); } private string GetUniqueTestDbPathAndCreateDir() { var testRootPath = Directory.GetCurrentDirectory(); var guidInPath = Guid.NewGuid().ToString(); - connectionStringPathDir = Path.Combine(Path.Combine(Path.Combine(testRootPath, "bin"), "TestResults"), guidInPath); - Directory.CreateDirectory(connectionStringPathDir); - return Path.Combine(connectionStringPathDir, $"test{guidInPath}.db"); + _connectionStringPathDir = Path.Combine(Path.Combine(Path.Combine(testRootPath, "bin"), "TestResults"), guidInPath); + Directory.CreateDirectory(_connectionStringPathDir); + return Path.Combine(_connectionStringPathDir, $"test{guidInPath}.db"); } public void CleanUpDb() { try { - File.Delete(connectionStringPath); - Directory.Delete(connectionStringPathDir, true); + File.Delete(_connectionStringPath); + Directory.Delete(_connectionStringPathDir, true); } catch (Exception e) { From 4fba6eb75696d8bff27bf50ca8a47e49fd13fd68 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Tue, 7 Nov 2023 14:52:22 +0000 Subject: [PATCH 2/3] Patch Tuesday --- samples/ASBTaskQueue/Greetings/Greetings.csproj | 2 +- samples/KafkaSchemaRegistry/Greetings/Greetings.csproj | 4 ++-- .../GreetingsReceiverConsole.csproj | 2 +- samples/WebAPI_Dapper/GreetingsWeb/GreetingsWeb.csproj | 6 +++--- .../GreetingsEntities/GreetingsEntities.csproj | 2 +- .../SalutationEntities/SalutationEntities.csproj | 2 +- samples/WebAPI_EFCore/GreetingsWeb/GreetingsWeb.csproj | 2 +- .../Paramore.Brighter.Archive.Azure.csproj | 2 +- .../Paramore.Brighter.DynamoDb.csproj | 2 +- .../Paramore.Brighter.Inbox.DynamoDB.csproj | 2 +- .../Paramore.Brighter.MessagingGateway.AWSSQS.csproj | 6 +++--- .../Paramore.Brighter.MessagingGateway.Kafka.csproj | 4 ++-- .../Paramore.Brighter.Outbox.DynamoDB.csproj | 2 +- .../Paramore.Brighter.Tranformers.AWS.csproj | 4 ++-- .../Paramore.Brighter.Transformers.Azure.csproj | 2 +- .../Paramore.Brighter.AWS.Tests.csproj | 2 +- .../Paramore.Brighter.Azure.Tests.csproj | 2 +- .../Paramore.Brighter.AzureServiceBus.Tests.csproj | 4 ++-- .../Paramore.Brighter.Core.Tests.csproj | 4 ++-- .../Paramore.Brighter.DynamoDB.Tests.csproj | 2 +- .../Paramore.Brighter.EventStore.Tests.csproj | 2 +- .../Paramore.Brighter.Extensions.Tests.csproj | 2 +- .../Paramore.Brighter.InMemory.Tests.csproj | 2 +- .../Paramore.Brighter.Kafka.Tests.csproj | 4 ++-- .../Paramore.Brighter.MSSQL.Tests.csproj | 2 +- .../Paramore.Brighter.MySQL.Tests.csproj | 2 +- .../Paramore.Brighter.PostgresSQL.Tests.csproj | 2 +- .../Paramore.Brighter.RESTMS.Tests.csproj | 2 +- .../Paramore.Brighter.RMQ.Tests.csproj | 2 +- .../Paramore.Brighter.Redis.Tests.csproj | 2 +- .../Paramore.Brighter.Sqlite.Tests.csproj | 2 +- 31 files changed, 41 insertions(+), 41 deletions(-) diff --git a/samples/ASBTaskQueue/Greetings/Greetings.csproj b/samples/ASBTaskQueue/Greetings/Greetings.csproj index a0657a44a4..c3e286ea2c 100644 --- a/samples/ASBTaskQueue/Greetings/Greetings.csproj +++ b/samples/ASBTaskQueue/Greetings/Greetings.csproj @@ -16,7 +16,7 @@ - + diff --git a/samples/KafkaSchemaRegistry/Greetings/Greetings.csproj b/samples/KafkaSchemaRegistry/Greetings/Greetings.csproj index 6560755cab..e7b7c36f12 100644 --- a/samples/KafkaSchemaRegistry/Greetings/Greetings.csproj +++ b/samples/KafkaSchemaRegistry/Greetings/Greetings.csproj @@ -12,7 +12,7 @@ - - + + \ No newline at end of file diff --git a/samples/KafkaSchemaRegistry/GreetingsReceiverConsole/GreetingsReceiverConsole.csproj b/samples/KafkaSchemaRegistry/GreetingsReceiverConsole/GreetingsReceiverConsole.csproj index d202b0823f..0390f1467c 100644 --- a/samples/KafkaSchemaRegistry/GreetingsReceiverConsole/GreetingsReceiverConsole.csproj +++ b/samples/KafkaSchemaRegistry/GreetingsReceiverConsole/GreetingsReceiverConsole.csproj @@ -4,7 +4,7 @@ Exe - + diff --git a/samples/WebAPI_Dapper/GreetingsWeb/GreetingsWeb.csproj b/samples/WebAPI_Dapper/GreetingsWeb/GreetingsWeb.csproj index 74cb5cb80a..3bb1ef51f4 100644 --- a/samples/WebAPI_Dapper/GreetingsWeb/GreetingsWeb.csproj +++ b/samples/WebAPI_Dapper/GreetingsWeb/GreetingsWeb.csproj @@ -8,10 +8,10 @@ - - + + - + diff --git a/samples/WebAPI_Dynamo/GreetingsEntities/GreetingsEntities.csproj b/samples/WebAPI_Dynamo/GreetingsEntities/GreetingsEntities.csproj index 8a9988ee26..73b72cb964 100644 --- a/samples/WebAPI_Dynamo/GreetingsEntities/GreetingsEntities.csproj +++ b/samples/WebAPI_Dynamo/GreetingsEntities/GreetingsEntities.csproj @@ -5,7 +5,7 @@ - + diff --git a/samples/WebAPI_Dynamo/SalutationEntities/SalutationEntities.csproj b/samples/WebAPI_Dynamo/SalutationEntities/SalutationEntities.csproj index 3e0ea19c37..4d137ef3cd 100644 --- a/samples/WebAPI_Dynamo/SalutationEntities/SalutationEntities.csproj +++ b/samples/WebAPI_Dynamo/SalutationEntities/SalutationEntities.csproj @@ -5,7 +5,7 @@ - + diff --git a/samples/WebAPI_EFCore/GreetingsWeb/GreetingsWeb.csproj b/samples/WebAPI_EFCore/GreetingsWeb/GreetingsWeb.csproj index b4722b5527..b498df8fbe 100644 --- a/samples/WebAPI_EFCore/GreetingsWeb/GreetingsWeb.csproj +++ b/samples/WebAPI_EFCore/GreetingsWeb/GreetingsWeb.csproj @@ -5,7 +5,7 @@ - + diff --git a/src/Paramore.Brighter.Archive.Azure/Paramore.Brighter.Archive.Azure.csproj b/src/Paramore.Brighter.Archive.Azure/Paramore.Brighter.Archive.Azure.csproj index fc01f28fa0..e50983bdae 100644 --- a/src/Paramore.Brighter.Archive.Azure/Paramore.Brighter.Archive.Azure.csproj +++ b/src/Paramore.Brighter.Archive.Azure/Paramore.Brighter.Archive.Azure.csproj @@ -12,7 +12,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Paramore.Brighter.DynamoDb/Paramore.Brighter.DynamoDb.csproj b/src/Paramore.Brighter.DynamoDb/Paramore.Brighter.DynamoDb.csproj index 207ca882f1..1c72030886 100644 --- a/src/Paramore.Brighter.DynamoDb/Paramore.Brighter.DynamoDb.csproj +++ b/src/Paramore.Brighter.DynamoDb/Paramore.Brighter.DynamoDb.csproj @@ -5,7 +5,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Paramore.Brighter.Inbox.DynamoDB/Paramore.Brighter.Inbox.DynamoDB.csproj b/src/Paramore.Brighter.Inbox.DynamoDB/Paramore.Brighter.Inbox.DynamoDB.csproj index 52fe066206..b8aca3b83d 100644 --- a/src/Paramore.Brighter.Inbox.DynamoDB/Paramore.Brighter.Inbox.DynamoDB.csproj +++ b/src/Paramore.Brighter.Inbox.DynamoDB/Paramore.Brighter.Inbox.DynamoDB.csproj @@ -3,7 +3,7 @@ netstandard2.0;net6.0;net7.0 - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Paramore.Brighter.MessagingGateway.AWSSQS/Paramore.Brighter.MessagingGateway.AWSSQS.csproj b/src/Paramore.Brighter.MessagingGateway.AWSSQS/Paramore.Brighter.MessagingGateway.AWSSQS.csproj index ab35333c91..26f9025499 100644 --- a/src/Paramore.Brighter.MessagingGateway.AWSSQS/Paramore.Brighter.MessagingGateway.AWSSQS.csproj +++ b/src/Paramore.Brighter.MessagingGateway.AWSSQS/Paramore.Brighter.MessagingGateway.AWSSQS.csproj @@ -9,11 +9,11 @@ - + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Paramore.Brighter.MessagingGateway.Kafka/Paramore.Brighter.MessagingGateway.Kafka.csproj b/src/Paramore.Brighter.MessagingGateway.Kafka/Paramore.Brighter.MessagingGateway.Kafka.csproj index fa570100eb..55181f4224 100644 --- a/src/Paramore.Brighter.MessagingGateway.Kafka/Paramore.Brighter.MessagingGateway.Kafka.csproj +++ b/src/Paramore.Brighter.MessagingGateway.Kafka/Paramore.Brighter.MessagingGateway.Kafka.csproj @@ -10,8 +10,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Paramore.Brighter.Outbox.DynamoDB/Paramore.Brighter.Outbox.DynamoDB.csproj b/src/Paramore.Brighter.Outbox.DynamoDB/Paramore.Brighter.Outbox.DynamoDB.csproj index 49b163ebd4..e4cededb3a 100644 --- a/src/Paramore.Brighter.Outbox.DynamoDB/Paramore.Brighter.Outbox.DynamoDB.csproj +++ b/src/Paramore.Brighter.Outbox.DynamoDB/Paramore.Brighter.Outbox.DynamoDB.csproj @@ -3,7 +3,7 @@ netstandard2.0;net6.0;net7.0 - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Paramore.Brighter.Tranformers.AWS/Paramore.Brighter.Tranformers.AWS.csproj b/src/Paramore.Brighter.Tranformers.AWS/Paramore.Brighter.Tranformers.AWS.csproj index 091483b672..6df6fc2ca9 100644 --- a/src/Paramore.Brighter.Tranformers.AWS/Paramore.Brighter.Tranformers.AWS.csproj +++ b/src/Paramore.Brighter.Tranformers.AWS/Paramore.Brighter.Tranformers.AWS.csproj @@ -18,8 +18,8 @@ - - + + diff --git a/src/Paramore.Brighter.Transformers.Azure/Paramore.Brighter.Transformers.Azure.csproj b/src/Paramore.Brighter.Transformers.Azure/Paramore.Brighter.Transformers.Azure.csproj index 0d553935e5..ca37b9a2ce 100644 --- a/src/Paramore.Brighter.Transformers.Azure/Paramore.Brighter.Transformers.Azure.csproj +++ b/src/Paramore.Brighter.Transformers.Azure/Paramore.Brighter.Transformers.Azure.csproj @@ -8,7 +8,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/tests/Paramore.Brighter.AWS.Tests/Paramore.Brighter.AWS.Tests.csproj b/tests/Paramore.Brighter.AWS.Tests/Paramore.Brighter.AWS.Tests.csproj index f8ca9f8a68..d9ac5c4157 100644 --- a/tests/Paramore.Brighter.AWS.Tests/Paramore.Brighter.AWS.Tests.csproj +++ b/tests/Paramore.Brighter.AWS.Tests/Paramore.Brighter.AWS.Tests.csproj @@ -14,7 +14,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/tests/Paramore.Brighter.Azure.Tests/Paramore.Brighter.Azure.Tests.csproj b/tests/Paramore.Brighter.Azure.Tests/Paramore.Brighter.Azure.Tests.csproj index a6343defc5..bbdd4987fb 100644 --- a/tests/Paramore.Brighter.Azure.Tests/Paramore.Brighter.Azure.Tests.csproj +++ b/tests/Paramore.Brighter.Azure.Tests/Paramore.Brighter.Azure.Tests.csproj @@ -12,7 +12,7 @@ - + all diff --git a/tests/Paramore.Brighter.AzureServiceBus.Tests/Paramore.Brighter.AzureServiceBus.Tests.csproj b/tests/Paramore.Brighter.AzureServiceBus.Tests/Paramore.Brighter.AzureServiceBus.Tests.csproj index ba19e59075..ffdf323e13 100644 --- a/tests/Paramore.Brighter.AzureServiceBus.Tests/Paramore.Brighter.AzureServiceBus.Tests.csproj +++ b/tests/Paramore.Brighter.AzureServiceBus.Tests/Paramore.Brighter.AzureServiceBus.Tests.csproj @@ -7,14 +7,14 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/tests/Paramore.Brighter.Core.Tests/Paramore.Brighter.Core.Tests.csproj b/tests/Paramore.Brighter.Core.Tests/Paramore.Brighter.Core.Tests.csproj index bdb15b12d1..914852cea0 100644 --- a/tests/Paramore.Brighter.Core.Tests/Paramore.Brighter.Core.Tests.csproj +++ b/tests/Paramore.Brighter.Core.Tests/Paramore.Brighter.Core.Tests.csproj @@ -14,14 +14,14 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/tests/Paramore.Brighter.DynamoDB.Tests/Paramore.Brighter.DynamoDB.Tests.csproj b/tests/Paramore.Brighter.DynamoDB.Tests/Paramore.Brighter.DynamoDB.Tests.csproj index aaabee7c4a..24264a9eb7 100644 --- a/tests/Paramore.Brighter.DynamoDB.Tests/Paramore.Brighter.DynamoDB.Tests.csproj +++ b/tests/Paramore.Brighter.DynamoDB.Tests/Paramore.Brighter.DynamoDB.Tests.csproj @@ -13,7 +13,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/tests/Paramore.Brighter.EventStore.Tests/Paramore.Brighter.EventStore.Tests.csproj b/tests/Paramore.Brighter.EventStore.Tests/Paramore.Brighter.EventStore.Tests.csproj index c1f90a0728..abac6f3825 100644 --- a/tests/Paramore.Brighter.EventStore.Tests/Paramore.Brighter.EventStore.Tests.csproj +++ b/tests/Paramore.Brighter.EventStore.Tests/Paramore.Brighter.EventStore.Tests.csproj @@ -13,7 +13,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/tests/Paramore.Brighter.Extensions.Tests/Paramore.Brighter.Extensions.Tests.csproj b/tests/Paramore.Brighter.Extensions.Tests/Paramore.Brighter.Extensions.Tests.csproj index 5de89b5831..bc687ddf45 100644 --- a/tests/Paramore.Brighter.Extensions.Tests/Paramore.Brighter.Extensions.Tests.csproj +++ b/tests/Paramore.Brighter.Extensions.Tests/Paramore.Brighter.Extensions.Tests.csproj @@ -12,7 +12,7 @@ - + all runtime; build; native; contentfiles; analyzers diff --git a/tests/Paramore.Brighter.InMemory.Tests/Paramore.Brighter.InMemory.Tests.csproj b/tests/Paramore.Brighter.InMemory.Tests/Paramore.Brighter.InMemory.Tests.csproj index d675a480ea..705f4132d9 100644 --- a/tests/Paramore.Brighter.InMemory.Tests/Paramore.Brighter.InMemory.Tests.csproj +++ b/tests/Paramore.Brighter.InMemory.Tests/Paramore.Brighter.InMemory.Tests.csproj @@ -13,7 +13,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/tests/Paramore.Brighter.Kafka.Tests/Paramore.Brighter.Kafka.Tests.csproj b/tests/Paramore.Brighter.Kafka.Tests/Paramore.Brighter.Kafka.Tests.csproj index 42aaa68f36..3324ed30f8 100644 --- a/tests/Paramore.Brighter.Kafka.Tests/Paramore.Brighter.Kafka.Tests.csproj +++ b/tests/Paramore.Brighter.Kafka.Tests/Paramore.Brighter.Kafka.Tests.csproj @@ -6,14 +6,14 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/tests/Paramore.Brighter.MSSQL.Tests/Paramore.Brighter.MSSQL.Tests.csproj b/tests/Paramore.Brighter.MSSQL.Tests/Paramore.Brighter.MSSQL.Tests.csproj index 65051cce6e..2266fa6cd0 100644 --- a/tests/Paramore.Brighter.MSSQL.Tests/Paramore.Brighter.MSSQL.Tests.csproj +++ b/tests/Paramore.Brighter.MSSQL.Tests/Paramore.Brighter.MSSQL.Tests.csproj @@ -15,7 +15,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/tests/Paramore.Brighter.MySQL.Tests/Paramore.Brighter.MySQL.Tests.csproj b/tests/Paramore.Brighter.MySQL.Tests/Paramore.Brighter.MySQL.Tests.csproj index 01f68d1c0a..18b4b27d34 100644 --- a/tests/Paramore.Brighter.MySQL.Tests/Paramore.Brighter.MySQL.Tests.csproj +++ b/tests/Paramore.Brighter.MySQL.Tests/Paramore.Brighter.MySQL.Tests.csproj @@ -14,7 +14,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/tests/Paramore.Brighter.PostgresSQL.Tests/Paramore.Brighter.PostgresSQL.Tests.csproj b/tests/Paramore.Brighter.PostgresSQL.Tests/Paramore.Brighter.PostgresSQL.Tests.csproj index fd81bac820..0b0c6c67c2 100644 --- a/tests/Paramore.Brighter.PostgresSQL.Tests/Paramore.Brighter.PostgresSQL.Tests.csproj +++ b/tests/Paramore.Brighter.PostgresSQL.Tests/Paramore.Brighter.PostgresSQL.Tests.csproj @@ -14,7 +14,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/tests/Paramore.Brighter.RESTMS.Tests/Paramore.Brighter.RESTMS.Tests.csproj b/tests/Paramore.Brighter.RESTMS.Tests/Paramore.Brighter.RESTMS.Tests.csproj index 5d28b8bb84..dbfaee94b1 100644 --- a/tests/Paramore.Brighter.RESTMS.Tests/Paramore.Brighter.RESTMS.Tests.csproj +++ b/tests/Paramore.Brighter.RESTMS.Tests/Paramore.Brighter.RESTMS.Tests.csproj @@ -12,7 +12,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/tests/Paramore.Brighter.RMQ.Tests/Paramore.Brighter.RMQ.Tests.csproj b/tests/Paramore.Brighter.RMQ.Tests/Paramore.Brighter.RMQ.Tests.csproj index c44677a5c6..c914aee111 100644 --- a/tests/Paramore.Brighter.RMQ.Tests/Paramore.Brighter.RMQ.Tests.csproj +++ b/tests/Paramore.Brighter.RMQ.Tests/Paramore.Brighter.RMQ.Tests.csproj @@ -12,7 +12,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/tests/Paramore.Brighter.Redis.Tests/Paramore.Brighter.Redis.Tests.csproj b/tests/Paramore.Brighter.Redis.Tests/Paramore.Brighter.Redis.Tests.csproj index ef8dea867d..7583d5333b 100644 --- a/tests/Paramore.Brighter.Redis.Tests/Paramore.Brighter.Redis.Tests.csproj +++ b/tests/Paramore.Brighter.Redis.Tests/Paramore.Brighter.Redis.Tests.csproj @@ -12,7 +12,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/tests/Paramore.Brighter.Sqlite.Tests/Paramore.Brighter.Sqlite.Tests.csproj b/tests/Paramore.Brighter.Sqlite.Tests/Paramore.Brighter.Sqlite.Tests.csproj index 705fb630bf..07cf6f42fe 100644 --- a/tests/Paramore.Brighter.Sqlite.Tests/Paramore.Brighter.Sqlite.Tests.csproj +++ b/tests/Paramore.Brighter.Sqlite.Tests/Paramore.Brighter.Sqlite.Tests.csproj @@ -12,7 +12,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive From 1e8bc9ec4357e255d958dccfe546f5fb30bcfc4a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Nov 2023 17:27:32 +0000 Subject: [PATCH 3/3] chore(deps): bump AWSSDK.S3 from 3.7.205.23 to 3.7.300 Bumps [AWSSDK.S3](https://github.com/aws/aws-sdk-net) from 3.7.205.23 to 3.7.300. - [Changelog](https://github.com/aws/aws-sdk-net/blob/main/SDK.CHANGELOG.MD) - [Commits](https://github.com/aws/aws-sdk-net/commits/3.7.300.0) --- updated-dependencies: - dependency-name: AWSSDK.S3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .../Paramore.Brighter.Tranformers.AWS.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Paramore.Brighter.Tranformers.AWS/Paramore.Brighter.Tranformers.AWS.csproj b/src/Paramore.Brighter.Tranformers.AWS/Paramore.Brighter.Tranformers.AWS.csproj index 6df6fc2ca9..24ee64ef43 100644 --- a/src/Paramore.Brighter.Tranformers.AWS/Paramore.Brighter.Tranformers.AWS.csproj +++ b/src/Paramore.Brighter.Tranformers.AWS/Paramore.Brighter.Tranformers.AWS.csproj @@ -18,7 +18,7 @@ - + /// The factory for used within the pipeline to pass information between steps. If you do not need to override /// provide . @@ -225,50 +251,30 @@ public IAmACommandProcessorBuilder RequestContextFactory(IAmARequestContextFacto /// CommandProcessor. public CommandProcessor Build() { - if (!(_useExternalBus || _useRequestReplyQueues)) + if (_bus == null) { - return new CommandProcessor( - - subscriberRegistry: _registry, - handlerFactory: _handlerFactory, - requestContextFactory: _requestContextFactory, - policyRegistry: _policyRegistry, + return new CommandProcessor(subscriberRegistry: _registry, handlerFactory: _handlerFactory, + requestContextFactory: _requestContextFactory, policyRegistry: _policyRegistry, featureSwitchRegistry: _featureSwitchRegistry); } - else if (_useExternalBus) - { - return new CommandProcessor( - subscriberRegistry: _registry, - handlerFactory: _handlerFactory, - requestContextFactory: _requestContextFactory, - policyRegistry: _policyRegistry, - mapperRegistry: _messageMapperRegistry, - outBox: _outbox, - producerRegistry: _producers, - outboxTimeout: _outboxWriteTimeout, - featureSwitchRegistry: _featureSwitchRegistry, - boxTransactionConnectionProvider: _overridingBoxTransactionConnectionProvider, - outboxBulkChunkSize: _outboxBulkChunkSize, - messageTransformerFactory: _transformerFactory - ); - } - else if (_useRequestReplyQueues) - { - return new CommandProcessor( - subscriberRegistry: _registry, - handlerFactory: _handlerFactory, - requestContextFactory: _requestContextFactory, - policyRegistry: _policyRegistry, - mapperRegistry: _messageMapperRegistry, - outBox: _outbox, - producerRegistry: _producers, - replySubscriptions: _replySubscriptions, - responseChannelFactory: _responseChannelFactory, boxTransactionConnectionProvider: _overridingBoxTransactionConnectionProvider); - } - else - { - throw new ConfigurationException("Unknown Command Processor Type"); - } + + if (!_useRequestReplyQueues) + return new CommandProcessor(subscriberRegistry: _registry, handlerFactory: _handlerFactory, + requestContextFactory: _requestContextFactory, policyRegistry: _policyRegistry, + mapperRegistry: _messageMapperRegistry, bus: _bus, + featureSwitchRegistry: _featureSwitchRegistry, inboxConfiguration: _inboxConfiguration, + messageTransformerFactory: _transformerFactory); + + if (_useRequestReplyQueues) + return new CommandProcessor(subscriberRegistry: _registry, handlerFactory: _handlerFactory, + requestContextFactory: _requestContextFactory, policyRegistry: _policyRegistry, + mapperRegistry: _messageMapperRegistry, bus: _bus, + featureSwitchRegistry: _featureSwitchRegistry, inboxConfiguration: _inboxConfiguration, + messageTransformerFactory: _transformerFactory, replySubscriptions: _replySubscriptions, + responseChannelFactory: _responseChannelFactory); + + throw new ConfigurationException( + "The configuration options chosen cannot be used to construct a command processor"); } } @@ -319,13 +325,26 @@ public interface INeedPolicy public interface INeedMessaging { /// - /// Configure a task queue to send messages out of process + /// The wants to support or using an external bus. + /// You need to provide a policy to specify how QoS issues, specifically or + /// are handled by adding appropriate when choosing this option. /// - /// The configuration. - /// The outbox. - /// The connection provider to use when adding messages to the bus - /// INeedARequestContext. - INeedARequestContext ExternalBus(ExternalBusConfiguration configuration, IAmAnOutbox outbox, IAmABoxTransactionConnectionProvider boxTransactionConnectionProvider = null); + /// The type of Bus: In-memory, Db, or RPC + /// The bus that we wish to use + /// The register for message mappers that map outgoing requests to messages + /// A factory for transforms used for common transformations to outgoing messages + /// If using RPC the factory for reply channels + /// If using RPC, any reply subscriptions + /// + INeedARequestContext ExternalBus( + ExternalBusType busType, + IAmAnExternalBusService bus, + IAmAMessageMapperRegistry messageMapperRegistry, + IAmAMessageTransformerFactory transformerFactory, + IAmAChannelFactory responseChannelFactory = null, + IEnumerable subscriptions = null + ); + /// /// We don't send messages out of process /// @@ -333,12 +352,19 @@ public interface INeedMessaging INeedARequestContext NoExternalBus(); /// - /// We want to use RPC to send messages to another process + /// The wants to support or using an external bus. + /// You need to provide a policy to specify how QoS issues, specifically or + /// are handled by adding appropriate when choosing this option. + /// /// - /// - /// The outbox - /// Subscriptions for creating Reply queues - INeedARequestContext ExternalRPC(ExternalBusConfiguration externalBusConfiguration, IAmAnOutbox outboxSync, IEnumerable subscriptions); + /// The Task Queues configuration. + /// The Outbox. + /// + /// INeedARequestContext. + INeedARequestContext ExternalBusCreate( + ExternalBusConfiguration configuration, + IAmAnOutbox outbox, + IAmABoxTransactionProvider transactionProvider); } /// diff --git a/src/Paramore.Brighter/CommittableTransactionProvider.cs b/src/Paramore.Brighter/CommittableTransactionProvider.cs new file mode 100644 index 0000000000..ede5a4a17e --- /dev/null +++ b/src/Paramore.Brighter/CommittableTransactionProvider.cs @@ -0,0 +1,62 @@ +using System.Threading; +using System.Threading.Tasks; +using System.Transactions; + +namespace Paramore.Brighter +{ + public class CommittableTransactionProvider : IAmABoxTransactionProvider + { + private CommittableTransaction _transaction; + private Transaction _existingTransaction; + + public void Close() + { + Transaction.Current = _existingTransaction; + _transaction = null; + } + + public void Commit() + { + _transaction?.Commit(); + Close(); + } + + public Task CommitAsync(CancellationToken cancellationToken = default) + { + return Task.Factory.FromAsync(_transaction.BeginCommit, _transaction.EndCommit, null, TaskCreationOptions.RunContinuationsAsynchronously); + } + + public CommittableTransaction GetTransaction() + { + if (_transaction == null) + { + _existingTransaction = Transaction.Current; + _transaction = new CommittableTransaction(); + Transaction.Current = _transaction; + } + return _transaction; + } + + public Task GetTransactionAsync(CancellationToken cancellationToken = default) + { + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + tcs.SetResult(GetTransaction()); + return tcs.Task; + } + + public bool HasOpenTransaction { get { return _transaction != null; } } + public bool IsSharedConnection => true; + + public void Rollback() + { + _transaction.Rollback(); + Close(); + } + + public Task RollbackAsync(CancellationToken cancellationToken = default) + { + Rollback(); + return Task.CompletedTask; + } + } +} diff --git a/src/Paramore.Brighter/ControlBusSender.cs b/src/Paramore.Brighter/ControlBusSender.cs index 0e87339956..2f78c7cf75 100644 --- a/src/Paramore.Brighter/ControlBusSender.cs +++ b/src/Paramore.Brighter/ControlBusSender.cs @@ -1,4 +1,5 @@ #region Licence + /* The MIT License (MIT) Copyright © 2015 Ian Cooper @@ -28,27 +29,26 @@ THE SOFTWARE. */ namespace Paramore.Brighter { - /// - /// Class ControlBusSender. - /// This is really just a 'convenience' wrapper over a command processor to make it easy to configure two different command processors, one for normal messages the other for control messages. - /// Why? The issue arises because an application providing a lot of monitoring messages might find that the load of those control messages begins to negatively impact the throughput of normal messages. - /// To avoid this you can put control messages over a seperate broker. (There are some availability advantages here too). - /// But many IoC containers make your life hard when you do this, as you have to indicate that you want to build the MonitorHandler with one command processor and the other handlers with another - /// Wrapping the Command Processor in this class helps to alleviate that issue, by taking a dependency on a seperate interface. - /// What goes over a control bus? - /// The Control Bus is used carry the following types of messages: + // Class ControlBusSender. + // This is really just a 'convenience' wrapper over a command processor to make it easy to configure two different command processors, one for normal messages the other for control messages. + // Why? The issue arises because an application providing a lot of monitoring messages might find that the load of those control messages begins to negatively impact the throughput of normal messages. + // To avoid this you can put control messages over a seperate broker. (There are some availability advantages here too). + // But many IoC containers make your life hard when you do this, as you have to indicate that you want to build the MonitorHandler with one command processor and the other handlers with another + // Wrapping the Command Processor in this class helps to alleviate that issue, by taking a dependency on a seperate interface. + // What goes over a control bus? + // The Control Bus is used carry the following types of messages: // Configuration - Allows runtime configuration of a service activator node, including stopping and starting, adding and removing of channels, control of resources allocated to channels. // Heartbeat - A ping to service activator node to determine if it is still 'alive'. The node returns a message over a private queue established by the caller.The message also displays diagnostic information on the health of the node. // Exceptions— Any exceptions generated on the node may be sent by the Control Bus to monitoring systems. // Statistics— Each service activator node broadcasts statistics about the processing of messages which can be collated by a listener to the control bus to calculate the nunber of messages processes, average throughput, average time to process a message, and so on.This data is split out by message type, so we can aggregate results. /// - /// public class ControlBusSender : IAmAControlBusSender, IAmAControlBusSenderAsync, IDisposable { /// /// The command processor that underlies the control bus; we only use the Post method /// private readonly CommandProcessor _commandProcessor; + private bool _disposed; /// @@ -63,16 +63,21 @@ public ControlBusSender(CommandProcessor commandProcessor) /// /// Posts the specified request. /// - /// - /// The request. - public void Post(T request) where T : class, IRequest + /// The request + /// The type of request + public void Post(TRequest request) where TRequest : class, IRequest { _commandProcessor.Post(request); } - public async Task PostAsync(T request, bool continueOnCapturedContext = false, CancellationToken cancellationToken = default) where T : class, IRequest + public async Task PostAsync( + TRequest request, + bool continueOnCapturedContext = false, + CancellationToken cancellationToken = default) + where TRequest : class, IRequest { - await _commandProcessor.PostAsync(request, continueOnCapturedContext, cancellationToken).ConfigureAwait(continueOnCapturedContext); + await _commandProcessor.PostAsync(request, continueOnCapturedContext, cancellationToken) + .ConfigureAwait(continueOnCapturedContext); } public void Dispose() diff --git a/src/Paramore.Brighter/ControlBusSenderFactory.cs b/src/Paramore.Brighter/ControlBusSenderFactory.cs index 2f727c34cc..e69a4b2f2a 100644 --- a/src/Paramore.Brighter/ControlBusSenderFactory.cs +++ b/src/Paramore.Brighter/ControlBusSenderFactory.cs @@ -40,15 +40,24 @@ public class ControlBusSenderFactory : IAmAControlBusSenderFactory /// The logger to use /// The outbox for outgoing messages to the control bus /// IAmAControlBusSender. - public IAmAControlBusSender Create(IAmAnOutbox outbox, IAmAProducerRegistry producerRegistry) + public IAmAControlBusSender Create(IAmAnOutbox outbox, IAmAProducerRegistry producerRegistry) + where T : Message { var mapper = new MessageMapperRegistry(new SimpleMessageMapperFactory((_) => new MonitorEventMessageMapper())); mapper.Register(); - return new ControlBusSender(CommandProcessorBuilder.With() - .Handlers(new HandlerConfiguration()) - .DefaultPolicy() - .ExternalBus(new ExternalBusConfiguration(producerRegistry, mapper),outbox) + var busConfiguration = new ExternalBusConfiguration(); + busConfiguration.ProducerRegistry = producerRegistry; + busConfiguration.MessageMapperRegistry = mapper; + return new ControlBusSender( + CommandProcessorBuilder.With() + .Handlers(new HandlerConfiguration()) + .DefaultPolicy() + .ExternalBusCreate( + busConfiguration, + outbox, + new CommittableTransactionProvider() + ) .RequestContextFactory(new InMemoryRequestContextFactory()) .Build() ); diff --git a/src/Paramore.Brighter/ExternalBusConfiguration.cs b/src/Paramore.Brighter/ExternalBusConfiguration.cs index f089c1cca3..09036b4f30 100644 --- a/src/Paramore.Brighter/ExternalBusConfiguration.cs +++ b/src/Paramore.Brighter/ExternalBusConfiguration.cs @@ -22,82 +22,147 @@ THE SOFTWARE. */ #endregion +using System; using System.Collections.Generic; using System.Linq; namespace Paramore.Brighter { + public interface IAmExternalBusConfiguration + { + /// + /// The registry is a collection of producers + /// + /// The registry of producers + IAmAProducerRegistry ProducerRegistry { get; set; } + + /// + /// Gets the message mapper registry. + /// + /// The message mapper registry. + IAmAMessageMapperRegistry MessageMapperRegistry { get; set; } + + /// + /// The Outbox we wish to use for messaging + /// + IAmAnOutbox Outbox { get; set; } + + /// + /// The maximum amount of messages to deposit into the outbox in one transmissions. + /// This is to stop insert statements getting too big + /// + int OutboxBulkChunkSize { get; set; } + + /// + /// When do we timeout writing to the outbox + /// + int OutboxTimeout { get; set; } + + /// + /// Sets a channel factory. We need this for RPC which has to create a channel itself, but otherwise + /// this tends to he handled by a Dispatcher not a Command Processor. + /// + IAmAChannelFactory ResponseChannelFactory { get; set; } + + /// + /// Sets up a transform factory. We need this if you have transforms applied to your MapToMessage or MapToRequest methods + /// of your MessageMappers + /// + IAmAMessageTransformerFactory TransformerFactory { get; set; } + + /// + /// If we are using Rpc, what are the subscriptions for the reply queue? + /// + IEnumerable ReplyQueueSubscriptions { get; set; } + + /// + /// The transaction provider for the outbox + /// + Type TransactionProvider { get; set; } + + /// + /// Do we want to support RPC on an external bus? + /// + bool UseRpc { get; set; } + } + /// /// Used to configure the Event Bus /// - public class ExternalBusConfiguration + public class ExternalBusConfiguration : IAmExternalBusConfiguration { + /// + /// How do obtain a connection to the Outbox that is not part of a shared transaction. + /// NOTE: Must implement IAmARelationalDbConnectionProvider + /// + public Type ConnectionProvider { get; set; } + /// /// The registry is a collection of producers /// /// The registry of producers - public IAmAProducerRegistry ProducerRegistry { get; } + public IAmAProducerRegistry ProducerRegistry { get; set; } /// /// Gets the message mapper registry. /// /// The message mapper registry. - public IAmAMessageMapperRegistry MessageMapperRegistry { get; } - + public IAmAMessageMapperRegistry MessageMapperRegistry { get; set; } + + /// + /// The Outbox we wish to use for messaging + /// + public IAmAnOutbox Outbox { get; set; } + /// /// The maximum amount of messages to deposit into the outbox in one transmissions. /// This is to stop insert statements getting too big /// - public int OutboxBulkChunkSize { get; } + public int OutboxBulkChunkSize { get; set; } /// /// When do we timeout writing to the outbox /// - public int OutboxWriteTimeout { get; } + public int OutboxTimeout { get; set; } + + /// + /// If we are using Rpc, what are the subscriptions for the reply queue? + /// + public IEnumerable ReplyQueueSubscriptions { get; set; } /// /// Sets a channel factory. We need this for RPC which has to create a channel itself, but otherwise /// this tends to he handled by a Dispatcher not a Command Processor. /// - public IAmAChannelFactory ResponseChannelFactory { get; } + public IAmAChannelFactory ResponseChannelFactory { get; set; } /// /// Sets up a transform factory. We need this if you have transforms applied to your MapToMessage or MapToRequest methods /// of your MessageMappers /// - public IAmAMessageTransformerFactory TransformerFactory { get; } + public IAmAMessageTransformerFactory TransformerFactory { get; set; } + + /// + /// The transaction provider for the outbox + /// NOTE: Must implement IAmABoxTransactionProvider< > + /// + public Type TransactionProvider { get; set; } /// - /// The configuration of our inbox + /// Do we want to support RPC on an external bus? /// - public InboxConfiguration UseInbox { get;} + public bool UseRpc { get; set; } /// /// Initializes a new instance of the class. /// - /// Clients for the external bus by topic they send to. The client details are specialised by transport - /// The message mapper registry. - /// The maximum amount of messages to deposit into the outbox in one transmissions. - /// How long to wait when writing to the outbox - /// in a request-response scenario how do we build response pipeline - /// The factory that builds instances of a transforms for us - /// Do we want to create an inbox globally i.e. on every handler (as opposed to by hand). Defaults to null, ,by hand - public ExternalBusConfiguration(IAmAProducerRegistry producerRegistry, - IAmAMessageMapperRegistry messageMapperRegistry, - int outboxBulkChunkSize = 100, - int outboxWriteTimeout = 300, - IAmAChannelFactory responseChannelFactory = null, - IAmAMessageTransformerFactory transformerFactory = null, - InboxConfiguration useInbox = null) + public ExternalBusConfiguration() { - ProducerRegistry = producerRegistry; - MessageMapperRegistry = messageMapperRegistry; - OutboxWriteTimeout = outboxWriteTimeout; - ResponseChannelFactory = responseChannelFactory; - UseInbox = useInbox; - OutboxBulkChunkSize = outboxBulkChunkSize; - TransformerFactory = transformerFactory; + /*allows setting of properties one-by-one, we default the required values here*/ + + ProducerRegistry = new ProducerRegistry(new Dictionary()); } + } } diff --git a/src/Paramore.Brighter/ExternalBusServices.cs b/src/Paramore.Brighter/ExternalBusServices.cs index d6e7325edd..9a14341686 100644 --- a/src/Paramore.Brighter/ExternalBusServices.cs +++ b/src/Paramore.Brighter/ExternalBusServices.cs @@ -15,36 +15,63 @@ namespace Paramore.Brighter /// Provide services to CommandProcessor that persist across the lifetime of the application. Allows separation from elements that have a lifetime linked /// to the scope of a request, or are transient for DI purposes /// - internal class ExternalBusServices : IDisposable + public class ExternalBusServices : IAmAnExternalBusService + where TMessage : Message { private static readonly ILogger s_logger = ApplicationLogging.CreateLogger(); - internal IPolicyRegistry PolicyRegistry { get; set; } - internal IAmAnOutboxSync OutBox { get; set; } - internal IAmAnOutboxAsync AsyncOutbox { get; set; } - - internal int OutboxTimeout { get; set; } = 300; - - internal int OutboxBulkChunkSize { get; set; } = 100; - - internal IAmAProducerRegistry ProducerRegistry { get; set; } - - private static readonly SemaphoreSlim _clearSemaphoreToken = new SemaphoreSlim(1, 1); - private static readonly SemaphoreSlim _backgroundClearSemaphoreToken = new SemaphoreSlim(1, 1); - //Used to checking the limit on outstanding messages for an Outbox. We throw at that point. Writes to the static bool should be made thread-safe by locking the object - private static readonly SemaphoreSlim _checkOutstandingSemaphoreToken = new SemaphoreSlim(1, 1); + private readonly IPolicyRegistry _policyRegistry; + private readonly IAmAnOutboxSync _outBox; + private readonly IAmAnOutboxAsync _asyncOutbox; + private readonly int _outboxTimeout; + private readonly int _outboxBulkChunkSize; + private readonly IAmAProducerRegistry _producerRegistry; + private static readonly SemaphoreSlim s_clearSemaphoreToken = new SemaphoreSlim(1, 1); + private static readonly SemaphoreSlim s_backgroundClearSemaphoreToken = new SemaphoreSlim(1, 1); + //Used to checking the limit on outstanding messages for an Outbox. We throw at that point. Writes to the static bool should be made thread-safe by locking the object + private static readonly SemaphoreSlim s_checkOutstandingSemaphoreToken = new SemaphoreSlim(1, 1); + private const string ADDMESSAGETOOUTBOX = "Add message to outbox"; private const string GETMESSAGESFROMOUTBOX = "Get outstanding messages from the outbox"; private const string DISPATCHMESSAGE = "Dispatching message"; private const string BULKDISPATCHMESSAGE = "Bulk dispatching messages"; private DateTime _lastOutStandingMessageCheckAt = DateTime.UtcNow; - + //Uses -1 to indicate no outbox and will thus force a throw on a failed publish private int _outStandingCount; - private bool _disposed; - + private bool _disposed; + + /// + /// Creates an instance of External Bus Services + /// + /// A registry of producers + /// A registry for reliability policies + /// An outbox for transactional messaging, if none is provided, use an InMemoryOutbox + /// The size of a chunk for bulk work + /// How long to timeout for with an outbox + public ExternalBusServices( + IAmAProducerRegistry producerRegistry, + IPolicyRegistry policyRegistry, + IAmAnOutbox outbox = null, + int outboxBulkChunkSize = 100, + int outboxTimeout = 300 + ) + { + _producerRegistry = producerRegistry ?? throw new ConfigurationException("Missing Producer Registry for External Bus Services"); + _policyRegistry = policyRegistry?? throw new ConfigurationException("Missing Policy Registry for External Bus Services"); + + //default to in-memory; expectation for a in memory box is Message and CommittableTransaction + if (outbox == null) outbox = new InMemoryOutbox() as IAmAnOutbox; + if (outbox is IAmAnOutboxSync syncOutbox) _outBox = syncOutbox; + if (outbox is IAmAnOutboxAsync asyncOutbox) _asyncOutbox = asyncOutbox; + _outboxBulkChunkSize = outboxBulkChunkSize; + _outboxTimeout = outboxTimeout; + + ConfigureCallbacks(); + } + /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// @@ -59,32 +86,63 @@ protected virtual void Dispose(bool disposing) if (_disposed) return; - if (disposing && ProducerRegistry != null) - ProducerRegistry.CloseAll(); + if (disposing && _producerRegistry != null) + _producerRegistry.CloseAll(); _disposed = true; - } - internal async Task AddToOutboxAsync(T request, bool continueOnCapturedContext, CancellationToken cancellationToken, Message message, IAmABoxTransactionConnectionProvider overridingTransactionConnectionProvider = null) - where T : class, IRequest + /// + /// Adds a message to the outbox + /// + /// The request that we are storing (used for id) + /// The message to store in the outbox + /// The provider of the transaction for the outbox + /// Use the same thread for a callback + /// Allow cancellation of the message + /// The type of request we are saving + /// Thrown if we cannot write to the outbox + public async Task AddToOutboxAsync( + TRequest request, + TMessage message, + IAmABoxTransactionProvider overridingTransactionProvider = null, + bool continueOnCapturedContext = false, + CancellationToken cancellationToken = default) where TRequest : IRequest { CheckOutboxOutstandingLimit(); - - var written = await RetryAsync(async ct => { await AsyncOutbox.AddAsync(message, OutboxTimeout, ct, overridingTransactionConnectionProvider).ConfigureAwait(continueOnCapturedContext); }, - continueOnCapturedContext, cancellationToken).ConfigureAwait(continueOnCapturedContext); + + var written = await RetryAsync( + async ct => + { + await _asyncOutbox.AddAsync(message, _outboxTimeout, ct, overridingTransactionProvider) + .ConfigureAwait(continueOnCapturedContext); + }, + continueOnCapturedContext, cancellationToken).ConfigureAwait(continueOnCapturedContext); if (!written) throw new ChannelFailureException($"Could not write request {request.Id} to the outbox"); Activity.Current?.AddEvent(new ActivityEvent(ADDMESSAGETOOUTBOX, - tags: new ActivityTagsCollection {{"MessageId", message.Id}})); + tags: new ActivityTagsCollection { { "MessageId", message.Id } })); } - internal async Task AddToOutboxAsync(IEnumerable messages, bool continueOnCapturedContext, CancellationToken cancellationToken, IAmABoxTransactionConnectionProvider overridingTransactionConnectionProvider = null) + /// + /// Adds a message to the outbox + /// + /// The messages to store in the outbox + /// + /// Use the same thread for a callback + /// Allow cancellation of the message + /// The provider of the transaction for the outbox + /// Thrown if we cannot write to the outbox + public async Task AddToOutboxAsync( + IEnumerable messages, + IAmABoxTransactionProvider overridingTransactionProvider = null, + bool continueOnCapturedContext = false, + CancellationToken cancellationToken = default) { CheckOutboxOutstandingLimit(); #pragma warning disable CS0618 - if (AsyncOutbox is IAmABulkOutboxAsync box) + if (_asyncOutbox is IAmABulkOutboxAsync box) #pragma warning restore CS0618 { foreach (var chunk in ChunkMessages(messages)) @@ -92,7 +150,7 @@ internal async Task AddToOutboxAsync(IEnumerable messages, bool continu var written = await RetryAsync( async ct => { - await box.AddAsync(chunk, OutboxTimeout, ct, overridingTransactionConnectionProvider) + await box.AddAsync(chunk, _outboxTimeout, ct, overridingTransactionProvider) .ConfigureAwait(continueOnCapturedContext); }, continueOnCapturedContext, cancellationToken).ConfigureAwait(continueOnCapturedContext); @@ -103,34 +161,50 @@ await box.AddAsync(chunk, OutboxTimeout, ct, overridingTransactionConnectionProv } else { - throw new InvalidOperationException($"{AsyncOutbox.GetType()} does not implement IAmABulkOutboxAsync"); + throw new InvalidOperationException($"{_asyncOutbox.GetType()} does not implement IAmABulkOutboxAsync"); } - } - - internal void AddToOutbox(T request, Message message, IAmABoxTransactionConnectionProvider overridingTransactionConnectionProvider = null) where T : class, IRequest + } + + /// + /// Adds a message to the outbox + /// + /// The request the message is composed from (used for diagnostics) + /// The message we intend to send + /// A transaction provider that gives us the transaction to use with the Outbox + /// The transaction type for the Outbox + /// The type of the request we have converted into a message + /// + public void AddToOutbox( + TRequest request, + TMessage message, + IAmABoxTransactionProvider overridingTransactionProvider = null) + where TRequest : class, IRequest { CheckOutboxOutstandingLimit(); - - var written = Retry(() => { OutBox.Add(message, OutboxTimeout, overridingTransactionConnectionProvider); }); + + var written = Retry(() => { _outBox.Add(message, _outboxTimeout, overridingTransactionProvider); }); if (!written) throw new ChannelFailureException($"Could not write request {request.Id} to the outbox"); Activity.Current?.AddEvent(new ActivityEvent(ADDMESSAGETOOUTBOX, - tags: new ActivityTagsCollection {{"MessageId", message.Id}})); + tags: new ActivityTagsCollection { { "MessageId", message.Id } })); } - - internal void AddToOutbox(IEnumerable messages, IAmABoxTransactionConnectionProvider overridingTransactionConnectionProvider = null) + + public void AddToOutbox( + IEnumerable messages, + IAmABoxTransactionProvider overridingTransactionProvider = null + ) { CheckOutboxOutstandingLimit(); #pragma warning disable CS0618 - if (OutBox is IAmABulkOutboxSync box) + if (_outBox is IAmABulkOutboxSync box) #pragma warning restore CS0618 { foreach (var chunk in ChunkMessages(messages)) { var written = - Retry(() => { box.Add(chunk, OutboxTimeout, overridingTransactionConnectionProvider); }); + Retry(() => { box.Add(chunk, _outboxTimeout, overridingTransactionProvider); }); if (!written) throw new ChannelFailureException($"Could not write {chunk.Count()} messages to the outbox"); @@ -138,154 +212,322 @@ internal void AddToOutbox(IEnumerable messages, IAmABoxTransactionConne } else { - throw new InvalidOperationException($"{OutBox.GetType()} does not implement IAmABulkOutboxSync"); + throw new InvalidOperationException($"{_outBox.GetType()} does not implement IAmABulkOutboxSync"); } } - private IEnumerable> ChunkMessages(IEnumerable messages) - { - return Enumerable.Range(0, (int)Math.Ceiling((messages.Count() / (decimal)OutboxBulkChunkSize))) - .Select(i => new List(messages - .Skip(i * OutboxBulkChunkSize) - .Take(OutboxBulkChunkSize) - .ToArray())); - } - - private void CheckOutboxOutstandingLimit() - { - bool hasOutBox = (OutBox != null || AsyncOutbox != null); - if (!hasOutBox) - return; - - int maxOutStandingMessages = ProducerRegistry.GetDefaultProducer().MaxOutStandingMessages; - - s_logger.LogDebug("Outbox outstanding message count is: {OutstandingMessageCount}", _outStandingCount); - // Because a thread recalculates this, we may always be in a delay, so we check on entry for the next outstanding item - bool exceedsOutstandingMessageLimit = maxOutStandingMessages != -1 && _outStandingCount > maxOutStandingMessages; - - if (exceedsOutstandingMessageLimit) - throw new OutboxLimitReachedException($"The outbox limit of {maxOutStandingMessages} has been exceeded"); - } - - private void CheckOutstandingMessages() + /// + /// Used with RPC to call a remote service via the external bus + /// + /// The message to send + /// The type of the call + /// The type of the response + public void CallViaExternalBus(Message outMessage) + where T : class, ICall where TResponse : class, IResponse { - var now = DateTime.UtcNow; - var checkInterval = TimeSpan.FromMilliseconds(ProducerRegistry.GetDefaultProducer().MaxOutStandingCheckIntervalMilliSeconds); - - - var timeSinceLastCheck = now - _lastOutStandingMessageCheckAt; - s_logger.LogDebug("Time since last check is {SecondsSinceLastCheck} seconds.", timeSinceLastCheck.TotalSeconds); - if (timeSinceLastCheck < checkInterval) - { - s_logger.LogDebug($"Check not ready to run yet"); - return; - } - - s_logger.LogDebug("Running outstanding message check at {MessageCheckTime} after {SecondsSinceLastCheck} seconds wait", DateTime.UtcNow, timeSinceLastCheck.TotalSeconds); - //This is expensive, so use a background thread - Task.Run(() => OutstandingMessagesCheck()); + //We assume that this only occurs over a blocking producer + var producer = _producerRegistry.LookupByOrDefault(outMessage.Header.Topic); + if (producer is IAmAMessageProducerSync producerSync) + Retry(() => producerSync.Send(outMessage)); } - internal void ClearOutbox(params Guid[] posts) + /// + /// This is the clear outbox for explicit clearing of messages. + /// + /// The ids of the posts that you would like to clear + /// Thrown if there is no async outbox defined + /// Thrown if a message cannot be found + public void ClearOutbox(params Guid[] posts) { if (!HasOutbox()) throw new InvalidOperationException("No outbox defined."); // Only allow a single Clear to happen at a time - _clearSemaphoreToken.Wait(); + s_clearSemaphoreToken.Wait(); try { foreach (var messageId in posts) { - var message = OutBox.Get(messageId); + var message = _outBox.Get(messageId); if (message == null || message.Header.MessageType == MessageType.MT_NONE) throw new NullReferenceException($"Message with Id {messageId} not found in the Outbox"); - Dispatch(new[] {message}); + Dispatch(new[] { message }); } } finally { - _clearSemaphoreToken.Release(); + s_clearSemaphoreToken.Release(); } - + CheckOutstandingMessages(); } - internal async Task ClearOutboxAsync( - IEnumerable posts, + /// + /// This is the clear outbox for explicit clearing of messages. + /// + /// The ids of the posts that you would like to clear + /// Should we use the same thread in the callback + /// Allow cancellation of the operation + /// Thrown if there is no async outbox defined + /// Thrown if a message cannot be found + public async Task ClearOutboxAsync( + IEnumerable posts, bool continueOnCapturedContext = false, CancellationToken cancellationToken = default) { - if (!HasAsyncOutbox()) throw new InvalidOperationException("No async outbox defined."); - await _clearSemaphoreToken.WaitAsync(cancellationToken); + await s_clearSemaphoreToken.WaitAsync(cancellationToken); try { foreach (var messageId in posts) { - var message = await AsyncOutbox.GetAsync(messageId, OutboxTimeout, cancellationToken); + var message = await _asyncOutbox.GetAsync(messageId, _outboxTimeout, cancellationToken); if (message == null || message.Header.MessageType == MessageType.MT_NONE) throw new NullReferenceException($"Message with Id {messageId} not found in the Outbox"); - await DispatchAsync(new[] {message}, continueOnCapturedContext, cancellationToken); + await DispatchAsync(new[] { message }, continueOnCapturedContext, cancellationToken); } } finally { - _clearSemaphoreToken.Release(); + s_clearSemaphoreToken.Release(); } CheckOutstandingMessages(); } /// - /// This is the clear outbox for implicit clearing of messages. + /// This is the clear outbox for explicit clearing of messages. /// /// Maximum number to clear. /// The minimum age of messages to be cleared in milliseconds. /// Use the Async outbox and Producer /// Use bulk sending capability of the message producer, this must be paired with useAsync. /// Optional bag of arguments required by an outbox implementation to sweep - internal void ClearOutbox(int amountToClear, int minimumAge, bool useAsync, bool useBulk, Dictionary args = null) + public void ClearOutbox( + int amountToClear, + int minimumAge, + bool useAsync, + bool useBulk, + Dictionary args = null) { var span = Activity.Current; span?.AddTag("amountToClear", amountToClear); span?.AddTag("minimumAge", minimumAge); span?.AddTag("async", useAsync); span?.AddTag("bulk", useBulk); - + if (useAsync) { if (!HasAsyncOutbox()) throw new InvalidOperationException("No async outbox defined."); - - Task.Run(() => BackgroundDispatchUsingAsync(amountToClear, minimumAge, useBulk, args), CancellationToken.None); + + Task.Run(() => BackgroundDispatchUsingAsync(amountToClear, minimumAge, useBulk, args), + CancellationToken.None); } else { if (!HasOutbox()) throw new InvalidOperationException("No outbox defined."); - + Task.Run(() => BackgroundDispatchUsingSync(amountToClear, minimumAge, args)); } } - private async Task BackgroundDispatchUsingSync(int amountToClear, int minimumAge, Dictionary args) + /// + /// Configure the callbacks for the producers + /// + private void ConfigureCallbacks() + { + //Only register one, to avoid two callbacks where we support both interfaces on a producer + foreach (var producer in _producerRegistry.Producers) + { + if (!ConfigurePublisherCallbackMaybe(producer)) + ConfigureAsyncPublisherCallbackMaybe(producer); + } + } + + /// + /// If a producer supports a callback then we can use this to mark a message as dispatched in an asynchronous + /// Outbox + /// + /// The producer to add a callback for + /// + private bool ConfigureAsyncPublisherCallbackMaybe(IAmAMessageProducer producer) + { + if (producer is ISupportPublishConfirmation producerSync) + { + producerSync.OnMessagePublished += async delegate(bool success, Guid id) + { + if (success) + { + s_logger.LogInformation("Sent message: Id:{Id}", id.ToString()); + if (_asyncOutbox != null) + await RetryAsync(async ct => + await _asyncOutbox.MarkDispatchedAsync(id, DateTime.UtcNow, cancellationToken: ct)); + } + }; + return true; + } + + return false; + } + + /// + /// If a producer supports a callback then we can use this to mark a message as dispatched in a synchronous + /// Outbox + /// + /// The producer to add a callback for + private bool ConfigurePublisherCallbackMaybe(IAmAMessageProducer producer) + { + if (producer is ISupportPublishConfirmation producerSync) + { + producerSync.OnMessagePublished += delegate(bool success, Guid id) + { + if (success) + { + s_logger.LogInformation("Sent message: Id:{Id}", id.ToString()); + if (_outBox != null) + Retry(() => _outBox.MarkDispatched(id, DateTime.UtcNow)); + } + }; + return true; + } + + return false; + } + + /// + /// Do we have an async outbox defined? + /// + /// true if defined + public bool HasAsyncOutbox() + { + return _asyncOutbox != null; + } + + /// + /// Do we have an async bulk outbox defined? + /// + /// true if defined + public bool HasAsyncBulkOutbox() + { +#pragma warning disable CS0618 + return _asyncOutbox is IAmABulkOutboxAsync; +#pragma warning restore CS0618 + } + + /// + /// Do we have a synchronous outbox defined? + /// + /// true if defined + public bool HasOutbox() + { + return _outBox != null; + } + + /// + /// Do we have a synchronous bulk outbox defined? + /// + /// true if defined + public bool HasBulkOutbox() + { +#pragma warning disable CS0618 + return _outBox is IAmABulkOutboxSync; +#pragma warning restore CS0618 + } + + /// + /// Retry an action via the policy engine + /// + /// The Action to try + /// + public bool Retry(Action action) + { + var policy = _policyRegistry.Get(CommandProcessor.RETRYPOLICY); + var result = policy.ExecuteAndCapture(action); + if (result.Outcome != OutcomeType.Successful) + { + if (result.FinalException != null) + { + s_logger.LogError(result.FinalException, "Exception whilst trying to publish message"); + CheckOutstandingMessages(); + } + + return false; + } + + return true; + } + + private IEnumerable> ChunkMessages(IEnumerable messages) + { + return Enumerable.Range(0, (int)Math.Ceiling((messages.Count() / (decimal)_outboxBulkChunkSize))) + .Select(i => new List(messages + .Skip(i * _outboxBulkChunkSize) + .Take(_outboxBulkChunkSize) + .ToArray())); + } + + private void CheckOutboxOutstandingLimit() + { + bool hasOutBox = (_outBox != null || _asyncOutbox != null); + if (!hasOutBox) + return; + + int maxOutStandingMessages = _producerRegistry.GetDefaultProducer().MaxOutStandingMessages; + + s_logger.LogDebug("Outbox outstanding message count is: {OutstandingMessageCount}", _outStandingCount); + // Because a thread recalculates this, we may always be in a delay, so we check on entry for the next outstanding item + bool exceedsOutstandingMessageLimit = + maxOutStandingMessages != -1 && _outStandingCount > maxOutStandingMessages; + + if (exceedsOutstandingMessageLimit) + throw new OutboxLimitReachedException( + $"The outbox limit of {maxOutStandingMessages} has been exceeded"); + } + + private void CheckOutstandingMessages() + { + var now = DateTime.UtcNow; + var checkInterval = + TimeSpan.FromMilliseconds(_producerRegistry.GetDefaultProducer() + .MaxOutStandingCheckIntervalMilliSeconds); + + + var timeSinceLastCheck = now - _lastOutStandingMessageCheckAt; + s_logger.LogDebug("Time since last check is {SecondsSinceLastCheck} seconds.", + timeSinceLastCheck.TotalSeconds); + if (timeSinceLastCheck < checkInterval) + { + s_logger.LogDebug($"Check not ready to run yet"); + return; + } + + s_logger.LogDebug( + "Running outstanding message check at {MessageCheckTime} after {SecondsSinceLastCheck} seconds wait", + DateTime.UtcNow, timeSinceLastCheck.TotalSeconds); + //This is expensive, so use a background thread + Task.Run(() => OutstandingMessagesCheck()); + } + + + private async Task BackgroundDispatchUsingSync(int amountToClear, int minimumAge, + Dictionary args) { var span = Activity.Current; - if (await _backgroundClearSemaphoreToken.WaitAsync(TimeSpan.Zero)) + if (await s_backgroundClearSemaphoreToken.WaitAsync(TimeSpan.Zero)) { - await _clearSemaphoreToken.WaitAsync(CancellationToken.None); + await s_clearSemaphoreToken.WaitAsync(CancellationToken.None); try { - - var messages = OutBox.OutstandingMessages(minimumAge, amountToClear, args:args); + var messages = _outBox.OutstandingMessages(minimumAge, amountToClear, args: args); span?.AddEvent(new ActivityEvent(GETMESSAGESFROMOUTBOX, - tags: new ActivityTagsCollection {{"Outstanding Messages", messages.Count()}})); + tags: new ActivityTagsCollection { { "Outstanding Messages", messages.Count() } })); s_logger.LogInformation("Found {NumberOfMessages} to clear out of amount {AmountToClear}", messages.Count(), amountToClear); Dispatch(messages); @@ -300,8 +542,8 @@ private async Task BackgroundDispatchUsingSync(int amountToClear, int minimumAge finally { span?.Dispose(); - _clearSemaphoreToken.Release(); - _backgroundClearSemaphoreToken.Release(); + s_clearSemaphoreToken.Release(); + s_backgroundClearSemaphoreToken.Release(); } CheckOutstandingMessages(); @@ -313,18 +555,18 @@ private async Task BackgroundDispatchUsingSync(int amountToClear, int minimumAge s_logger.LogInformation("Skipping dispatch of messages as another thread is running"); } } - - private async Task BackgroundDispatchUsingAsync(int amountToClear, int minimumAge, bool useBulk, Dictionary args) + + private async Task BackgroundDispatchUsingAsync(int amountToClear, int minimumAge, bool useBulk, + Dictionary args) { var span = Activity.Current; - if (await _backgroundClearSemaphoreToken.WaitAsync(TimeSpan.Zero)) + if (await s_backgroundClearSemaphoreToken.WaitAsync(TimeSpan.Zero)) { - await _clearSemaphoreToken.WaitAsync(CancellationToken.None); + await s_clearSemaphoreToken.WaitAsync(CancellationToken.None); try { - var messages = - await AsyncOutbox.OutstandingMessagesAsync(minimumAge, amountToClear, args: args); + await _asyncOutbox.OutstandingMessagesAsync(minimumAge, amountToClear, args: args); span?.AddEvent(new ActivityEvent(GETMESSAGESFROMOUTBOX)); s_logger.LogInformation("Found {NumberOfMessages} to clear out of amount {AmountToClear}", @@ -350,8 +592,8 @@ private async Task BackgroundDispatchUsingAsync(int amountToClear, int minimumAg finally { span?.Dispose(); - _clearSemaphoreToken.Release(); - _backgroundClearSemaphoreToken.Release(); + s_clearSemaphoreToken.Release(); + s_backgroundClearSemaphoreToken.Release(); } CheckOutstandingMessages(); @@ -369,10 +611,14 @@ private void Dispatch(IEnumerable posts) foreach (var message in posts) { Activity.Current?.AddEvent(new ActivityEvent(DISPATCHMESSAGE, - tags: new ActivityTagsCollection {{"Topic", message.Header.Topic}, {"MessageId", message.Id}})); - s_logger.LogInformation("Decoupled invocation of message: Topic:{Topic} Id:{Id}", message.Header.Topic, message.Id.ToString()); + tags: new ActivityTagsCollection + { + { "Topic", message.Header.Topic }, { "MessageId", message.Id } + })); + s_logger.LogInformation("Decoupled invocation of message: Topic:{Topic} Id:{Id}", message.Header.Topic, + message.Id.ToString()); - var producer = ProducerRegistry.LookupByOrDefault(message.Header.Topic); + var producer = _producerRegistry.LookupByOrDefault(message.Header.Topic); if (producer is IAmAMessageProducerSync producerSync) { @@ -385,23 +631,28 @@ private void Dispatch(IEnumerable posts) { var sent = Retry(() => { producerSync.Send(message); }); if (sent) - Retry(() => OutBox.MarkDispatched(message.Id, DateTime.UtcNow)); + Retry(() => _outBox.MarkDispatched(message.Id, DateTime.UtcNow)); } } else throw new InvalidOperationException("No sync message producer defined."); } } - - private async Task DispatchAsync(IEnumerable posts, bool continueOnCapturedContext, CancellationToken cancellationToken) + + private async Task DispatchAsync(IEnumerable posts, bool continueOnCapturedContext, + CancellationToken cancellationToken) { foreach (var message in posts) { Activity.Current?.AddEvent(new ActivityEvent(DISPATCHMESSAGE, - tags: new ActivityTagsCollection {{"Topic", message.Header.Topic}, {"MessageId", message.Id}})); - s_logger.LogInformation("Decoupled invocation of message: Topic:{Topic} Id:{Id}", message.Header.Topic, message.Id.ToString()); - - var producer = ProducerRegistry.LookupByOrDefault(message.Header.Topic); + tags: new ActivityTagsCollection + { + { "Topic", message.Header.Topic }, { "MessageId", message.Id } + })); + s_logger.LogInformation("Decoupled invocation of message: Topic:{Topic} Id:{Id}", message.Header.Topic, + message.Id.ToString()); + + var producer = _producerRegistry.LookupByOrDefault(message.Header.Topic); if (producer is IAmAMessageProducerAsync producerAsync) { @@ -417,7 +668,6 @@ await producerAsync.SendAsync(message).ConfigureAwait(continueOnCapturedContext) } else { - var sent = await RetryAsync( async ct => await producerAsync.SendAsync(message).ConfigureAwait(continueOnCapturedContext), @@ -426,7 +676,9 @@ await producerAsync.SendAsync(message).ConfigureAwait(continueOnCapturedContext) .ConfigureAwait(continueOnCapturedContext); if (sent) - await RetryAsync(async ct => await AsyncOutbox.MarkDispatchedAsync(message.Id, DateTime.UtcNow, cancellationToken: cancellationToken), + await RetryAsync( + async ct => await _asyncOutbox.MarkDispatchedAsync(message.Id, DateTime.UtcNow, + cancellationToken: cancellationToken), cancellationToken: cancellationToken); } } @@ -434,7 +686,7 @@ await RetryAsync(async ct => await AsyncOutbox.MarkDispatchedAsync(message.Id, D throw new InvalidOperationException("No async message producer defined."); } } - + private async Task BulkDispatchAsync(IEnumerable posts, CancellationToken cancellationToken) { var span = Activity.Current; @@ -443,22 +695,27 @@ private async Task BulkDispatchAsync(IEnumerable posts, CancellationTok foreach (var topicBatch in messagesByTopic) { - var producer = ProducerRegistry.LookupByOrDefault(topicBatch.Key); + var producer = _producerRegistry.LookupByOrDefault(topicBatch.Key); if (producer is IAmABulkMessageProducerAsync bulkMessageProducer) { var messages = topicBatch.ToArray(); - s_logger.LogInformation("Bulk Dispatching {NumberOfMessages} for Topic {TopicName}", messages.Length, topicBatch.Key); + s_logger.LogInformation("Bulk Dispatching {NumberOfMessages} for Topic {TopicName}", + messages.Length, topicBatch.Key); span?.AddEvent(new ActivityEvent(BULKDISPATCHMESSAGE, - tags: new ActivityTagsCollection {{"Topic", topicBatch.Key}, {"Number Of Messages", messages.Length}})); + tags: new ActivityTagsCollection + { + { "Topic", topicBatch.Key }, { "Number Of Messages", messages.Length } + })); var dispatchesMessages = bulkMessageProducer.SendAsync(messages, cancellationToken); await foreach (var successfulMessage in dispatchesMessages.WithCancellation(cancellationToken)) { if (!(producer is ISupportPublishConfirmation)) { - await RetryAsync(async ct => await AsyncOutbox.MarkDispatchedAsync(successfulMessage, - DateTime.UtcNow, cancellationToken: cancellationToken), cancellationToken: cancellationToken); + await RetryAsync(async ct => await _asyncOutbox.MarkDispatchedAsync(successfulMessage, + DateTime.UtcNow, cancellationToken: cancellationToken), + cancellationToken: cancellationToken); } } } @@ -469,124 +726,46 @@ await RetryAsync(async ct => await AsyncOutbox.MarkDispatchedAsync(successfulMes } } - internal bool ConfigureAsyncPublisherCallbackMaybe(IAmAMessageProducer producer) - { - if (producer is ISupportPublishConfirmation producerSync) - { - producerSync.OnMessagePublished += async delegate(bool success, Guid id) - { - if (success) - { - s_logger.LogInformation("Sent message: Id:{Id}", id.ToString()); - if (AsyncOutbox != null) - await RetryAsync(async ct => await AsyncOutbox.MarkDispatchedAsync(id, DateTime.UtcNow, cancellationToken: ct)); - } - }; - return true; - } - - return false; - } - - internal bool ConfigurePublisherCallbackMaybe(IAmAMessageProducer producer) - { - if (producer is ISupportPublishConfirmation producerSync) - { - producerSync.OnMessagePublished += delegate(bool success, Guid id) - { - if (success) - { - s_logger.LogInformation("Sent message: Id:{Id}", id.ToString()); - if (OutBox != null) - Retry(() => OutBox.MarkDispatched(id, DateTime.UtcNow)); - } - }; - return true; - } - - return false; - } - - internal bool HasAsyncOutbox() - { - return AsyncOutbox != null; - } - internal bool HasAsyncBulkOutbox() - { -#pragma warning disable CS0618 - return AsyncOutbox is IAmABulkOutboxAsync; -#pragma warning restore CS0618 - } - - internal bool HasOutbox() - { - return OutBox != null; - } - - internal bool HasBulkOutbox() - { -#pragma warning disable CS0618 - return OutBox is IAmABulkOutboxSync; -#pragma warning restore CS0618 - } - private void OutstandingMessagesCheck() { - _checkOutstandingSemaphoreToken.Wait(); - + s_checkOutstandingSemaphoreToken.Wait(); + _lastOutStandingMessageCheckAt = DateTime.UtcNow; s_logger.LogDebug("Begin count of outstanding messages"); try { - var producer = ProducerRegistry.GetDefaultProducer(); - if (OutBox != null) + var producer = _producerRegistry.GetDefaultProducer(); + if (_outBox != null) { - _outStandingCount = OutBox + _outStandingCount = _outBox .OutstandingMessages( producer.MaxOutStandingCheckIntervalMilliSeconds, - args:producer.OutBoxBag - ) + args: producer.OutBoxBag + ) .Count(); return; } + _outStandingCount = 0; } catch (Exception ex) { //if we can't talk to the outbox, we would swallow the exception on this thread //by setting the _outstandingCount to -1, we force an exception - s_logger.LogError(ex,"Error getting outstanding message count, reset count"); + s_logger.LogError(ex, "Error getting outstanding message count, reset count"); _outStandingCount = 0; } finally { s_logger.LogDebug("Current outstanding count is {OutStandingCount}", _outStandingCount); - _checkOutstandingSemaphoreToken.Release(); - } - } - - internal bool Retry(Action send) - { - var policy = PolicyRegistry.Get(CommandProcessor.RETRYPOLICY); - var result = policy.ExecuteAndCapture(send); - if (result.Outcome != OutcomeType.Successful) - { - if (result.FinalException != null) - { - s_logger.LogError(result.FinalException,"Exception whilst trying to publish message"); - CheckOutstandingMessages(); - } - - return false; + s_checkOutstandingSemaphoreToken.Release(); } - - return true; } private async Task RetryAsync(Func send, bool continueOnCapturedContext = false, CancellationToken cancellationToken = default) { - var result = await PolicyRegistry.Get(CommandProcessor.RETRYPOLICYASYNC) + var result = await _policyRegistry.Get(CommandProcessor.RETRYPOLICYASYNC) .ExecuteAndCaptureAsync(send, cancellationToken, continueOnCapturedContext) .ConfigureAwait(continueOnCapturedContext); @@ -594,7 +773,7 @@ private async Task RetryAsync(Func send, bool con { if (result.FinalException != null) { - s_logger.LogError(result.FinalException, "Exception whilst trying to publish message" ); + s_logger.LogError(result.FinalException, "Exception whilst trying to publish message"); CheckOutstandingMessages(); } @@ -603,14 +782,5 @@ private async Task RetryAsync(Func send, bool con return true; } - - internal void CallViaExternalBus(Message outMessage) where T : class, ICall where TResponse : class, IResponse - { - //We assume that this only occurs over a blocking producer - var producer = ProducerRegistry.LookupByOrDefault(outMessage.Header.Topic); - if (producer is IAmAMessageProducerSync producerSync) - Retry(() => producerSync.Send(outMessage)); - } - } } diff --git a/src/Paramore.Brighter.Inbox.Postgres/PostgresSqlInboxConfiguration.cs b/src/Paramore.Brighter/ExternalBusType.cs similarity index 60% rename from src/Paramore.Brighter.Inbox.Postgres/PostgresSqlInboxConfiguration.cs rename to src/Paramore.Brighter/ExternalBusType.cs index 31ff311cd9..239595e877 100644 --- a/src/Paramore.Brighter.Inbox.Postgres/PostgresSqlInboxConfiguration.cs +++ b/src/Paramore.Brighter/ExternalBusType.cs @@ -1,7 +1,7 @@ #region Licence /* The MIT License (MIT) -Copyright © 2020 Ian Cooper +Copyright © 2023 Ian Cooper Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal @@ -23,26 +23,12 @@ THE SOFTWARE. */ #endregion -using Paramore.Brighter.PostgreSql; - -namespace Paramore.Brighter.Inbox.Postgres +namespace Paramore.Brighter { - public class PostgresSqlInboxConfiguration : PostgreSqlConfiguration + public enum ExternalBusType { - public PostgresSqlInboxConfiguration(string connectionString, string tableName) : base(connectionString) - { - InBoxTableName = tableName; - } - - public PostgresSqlInboxConfiguration(string tableName) : base(null) - { - InBoxTableName = tableName; - } - - /// - /// Gets the name of the outbox table. - /// - /// The name of the outbox table. - public string InBoxTableName { get; } + None = 0, + FireAndForget = 1, + RPC = 2 } } diff --git a/src/Paramore.Brighter/IAmABoxTransactionConnectionProvider.cs b/src/Paramore.Brighter/IAmABoxTransactionConnectionProvider.cs deleted file mode 100644 index 2ccc5a1359..0000000000 --- a/src/Paramore.Brighter/IAmABoxTransactionConnectionProvider.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Paramore.Brighter -{ - public interface IAmABoxTransactionConnectionProvider - { - - } -} diff --git a/src/Paramore.Brighter/IAmABoxTransactionProvider.cs b/src/Paramore.Brighter/IAmABoxTransactionProvider.cs new file mode 100644 index 0000000000..8c5a0f22f2 --- /dev/null +++ b/src/Paramore.Brighter/IAmABoxTransactionProvider.cs @@ -0,0 +1,63 @@ +using System.Data.Common; +using System.Threading; +using System.Threading.Tasks; + +namespace Paramore.Brighter +{ + /// + /// This is a marker interface to indicate that this connection provides access to an ambient transaction + /// + public interface IAmABoxTransactionProvider + { + /// + /// Close any open connection or transaction + /// + void Close(); + + /// + /// Commit any transaction that we are managing + /// + void Commit(); + + /// + /// Allows asynchronous commit of a transaction + /// + Task CommitAsync(CancellationToken cancellationToken = default); + + /// + /// Gets an existing transaction; creates a new one from the connection if it does not exist and we support + /// sharing of connections and transactions. You are responsible for committing the transaction. + /// + /// A database transaction + T GetTransaction(); + + /// + /// Gets an existing transaction; creates a new one from the connection if it does not exist and we support + /// sharing of connections and transactions. You are responsible for committing the transaction. + /// + /// A database transaction + Task GetTransactionAsync(CancellationToken cancellationToken = default); + + /// + /// Is there a transaction open? + /// + bool HasOpenTransaction { get; } + + /// + /// Is there a shared connection? (Do we maintain state of just create anew) + /// + bool IsSharedConnection { get; } + + /// + /// Rollback a transaction that we manage + /// + void Rollback(); + + /// + /// Rollback a transaction that we manage + /// + Task RollbackAsync(CancellationToken cancellationToken = default); + + } + +} diff --git a/src/Paramore.Brighter/IAmABulkOutboxAsync.cs b/src/Paramore.Brighter/IAmABulkOutboxAsync.cs index c6dbc4b993..647322853a 100644 --- a/src/Paramore.Brighter/IAmABulkOutboxAsync.cs +++ b/src/Paramore.Brighter/IAmABulkOutboxAsync.cs @@ -24,6 +24,7 @@ THE SOFTWARE. */ using System; using System.Collections.Generic; +using System.Data.Common; using System.Threading; using System.Threading.Tasks; @@ -36,9 +37,10 @@ namespace Paramore.Brighter /// We provide implementations of for various databases. Users using unsupported databases should consider a Pull /// request /// - /// + /// The type of message + /// The type of transaction used by this outbox [Obsolete("Deprecated in favour of Bulk, wil be merged into IAmAnOutboxAsync in v10")] - public interface IAmABulkOutboxAsync : IAmAnOutboxAsync where T : Message + public interface IAmABulkOutboxAsync : IAmAnOutboxAsync where T : Message { /// /// Awaitable add the specified message. @@ -46,8 +48,12 @@ public interface IAmABulkOutboxAsync : IAmAnOutboxAsync where T : Messa /// The message. /// The time allowed for the write in milliseconds; on a -1 default /// Allows the sender to cancel the request pipeline. Optional - /// The Connection Provider to use for this call + /// The Connection Provider to use for this call /// . - Task AddAsync(IEnumerable messages, int outBoxTimeout = -1, CancellationToken cancellationToken = default, IAmABoxTransactionConnectionProvider transactionConnectionProvider = null); + Task AddAsync( + IEnumerable messages, int outBoxTimeout = -1, + CancellationToken cancellationToken = default, + IAmABoxTransactionProvider transactionProvider = null + ); } } diff --git a/src/Paramore.Brighter/IAmABulkOutboxSync.cs b/src/Paramore.Brighter/IAmABulkOutboxSync.cs index 3d3c2c2c46..ffa08f7a6c 100644 --- a/src/Paramore.Brighter/IAmABulkOutboxSync.cs +++ b/src/Paramore.Brighter/IAmABulkOutboxSync.cs @@ -24,6 +24,7 @@ THE SOFTWARE. */ using System; using System.Collections.Generic; +using System.Data.Common; using System.Threading.Tasks; namespace Paramore.Brighter @@ -32,19 +33,19 @@ namespace Paramore.Brighter /// Interface IAmABulkOutbox /// In order to provide reliability for messages sent over a Task Queue we /// store the message into an OutBox to allow later replay of those messages in the event of failure. We automatically copy any posted message into the store - /// We provide implementations of for various databases. Users using unsupported databases should consider a Pull + /// We provide implementations of for various databases. Users using unsupported databases should consider a Pull /// request /// /// [Obsolete("Deprecated in favour of Bulk, wil be merged into IAmAnOutboxSync in v10")] - public interface IAmABulkOutboxSync : IAmAnOutboxSync where T : Message + public interface IAmABulkOutboxSync : IAmAnOutboxSync where T : Message { /// /// Awaitable add the specified message. /// /// The message. /// The time allowed for the write in milliseconds; on a -1 default - /// The Connection Provider to use for this call - void Add(IEnumerable messages, int outBoxTimeout = -1, IAmABoxTransactionConnectionProvider transactionConnectionProvider = null); + /// The Connection Provider to use for this call + void Add(IEnumerable messages, int outBoxTimeout = -1, IAmABoxTransactionProvider transactionProvider = null); } } diff --git a/src/Paramore.Brighter/IAmACommandProcessor.cs b/src/Paramore.Brighter/IAmACommandProcessor.cs index 7f5cd5bb39..4a59850ae6 100644 --- a/src/Paramore.Brighter/IAmACommandProcessor.cs +++ b/src/Paramore.Brighter/IAmACommandProcessor.cs @@ -42,53 +42,74 @@ public interface IAmACommandProcessor /// /// Sends the specified command. /// - /// + /// /// The command. - void Send(T command) where T : class, IRequest; + void Send(TRequest command) where TRequest : class, IRequest; /// /// Awaitably sends the specified command. /// - /// + /// /// The command. /// Should we use the calling thread's synchronization context when continuing or a default thread synchronization context. Defaults to false /// Allows the sender to cancel the request pipeline. Optional /// awaitable . - Task SendAsync(T command, bool continueOnCapturedContext = false, CancellationToken cancellationToken = default) where T : class, IRequest; + Task SendAsync(TRequest command, bool continueOnCapturedContext = false, CancellationToken cancellationToken = default) where TRequest : class, IRequest; /// /// Publishes the specified event. Throws an aggregate exception on failure of a pipeline but executes remaining /// - /// + /// /// The event. - void Publish(T @event) where T : class, IRequest; + void Publish(TRequest @event) where TRequest : class, IRequest; /// /// Publishes the specified event with async/await support. Throws an aggregate exception on failure of a pipeline but executes remaining /// - /// + /// /// The event. /// Should we use the calling thread's synchronization context when continuing or a default thread synchronization context. Defaults to false /// Allows the sender to cancel the request pipeline. Optional /// awaitable . - Task PublishAsync(T @event, bool continueOnCapturedContext = false, CancellationToken cancellationToken = default) where T : class, IRequest; + Task PublishAsync( + TRequest @event, + bool continueOnCapturedContext = false, + CancellationToken cancellationToken = default + ) where TRequest : class, IRequest; /// /// Posts the specified request. /// - /// + /// The type of the request /// The request. - void Post(T request) where T : class, IRequest; - + void Post(TRequest request) where TRequest : class, IRequest; + /// /// Posts the specified request with async/await support. /// - /// + /// The type of the request /// The request. /// Should we use the calling thread's synchronization context when continuing or a default thread synchronization context. Defaults to false /// Allows the sender to cancel the request pipeline. Optional /// awaitable . - Task PostAsync(T request, bool continueOnCapturedContext = false, CancellationToken cancellationToken = default) where T : class, IRequest; + Task PostAsync( + TRequest request, + bool continueOnCapturedContext = false, + CancellationToken cancellationToken = default + ) where TRequest : class, IRequest; + + /// + /// Adds a message into the outbox, and returns the id of the saved message. + /// Intended for use with the Outbox pattern: http://gistlabs.com/2014/05/the-outbox/ normally you include the + /// call to DepositPostBox within the scope of the transaction to write corresponding entity state to your + /// database, that you want to signal via the request to downstream consumers + /// Pass deposited Guid to + /// + /// The request to save to the outbox + /// The type of the request + /// The type of transaction used by the outbox + /// + Guid DepositPost(TRequest request) where TRequest : class, IRequest; /// /// Adds a message into the outbox, and returns the id of the saved message. @@ -98,9 +119,26 @@ public interface IAmACommandProcessor /// Pass deposited Guid to /// /// The request to save to the outbox - /// The type of the request + /// If using an Outbox, the transaction provider for the Outbox + /// The type of the request + /// The type of transaction used by the outbox /// - Guid DepositPost(T request) where T : class, IRequest; + Guid DepositPost( + TRequest request, + IAmABoxTransactionProvider transactionProvider + ) where TRequest : class, IRequest; + + /// + /// Adds a messages into the outbox, and returns the id of the saved message. + /// Intended for use with the Outbox pattern: http://gistlabs.com/2014/05/the-outbox/ normally you include the + /// call to DepositPostBox within the scope of the transaction to write corresponding entity state to your + /// database, that you want to signal via the request to downstream consumers + /// Pass deposited Guid to + /// + /// The requests to save to the outbox + /// The type of the request + /// The Id of the Message that has been deposited. + public Guid[] DepositPost(IEnumerable requests) where TRequest : class, IRequest; /// /// Adds a messages into the outbox, and returns the id of the saved message. @@ -110,9 +148,33 @@ public interface IAmACommandProcessor /// Pass deposited Guid to /// /// The requests to save to the outbox - /// The type of the request + /// If using an Outbox, the transaction provider for the Outbox + /// The type of the request + /// The type of transaction used by the outbox /// The Id of the Message that has been deposited. - public Guid[] DepositPost(IEnumerable requests) where T : class, IRequest; + public Guid[] DepositPost( + IEnumerable requests, + IAmABoxTransactionProvider transactionProvider + ) where TRequest : class, IRequest; + + /// + /// Adds a message into the outbox, and returns the id of the saved message. + /// Intended for use with the Outbox pattern: http://gistlabs.com/2014/05/the-outbox/ normally you include the + /// call to DepositPostBox within the scope of the transaction to write corresponding entity state to your + /// database, that you want to signal via the request to downstream consumers + /// Pass deposited Guid to + /// + /// The request to save to the outbox + /// Should we use the calling thread's synchronization context when continuing or a default thread synchronization context. Defaults to false + /// The Cancellation Token. + /// The type of the request + /// + Task DepositPostAsync( + TRequest request, + bool continueOnCapturedContext = false, + CancellationToken cancellationToken = default + ) where TRequest : class, IRequest; + /// /// Adds a message into the outbox, and returns the id of the saved message. @@ -122,9 +184,37 @@ public interface IAmACommandProcessor /// Pass deposited Guid to /// /// The request to save to the outbox + /// If using an Outbox, the transaction provider for the Outbox + /// Should we use the calling thread's synchronization context when continuing or a default thread synchronization context. Defaults to false + /// The Cancellation Token. /// The type of the request + /// The type of transaction used by the outbox /// - Task DepositPostAsync(T request, bool continueOnCapturedContext = false, CancellationToken cancellationToken = default) where T : class, IRequest; + Task DepositPostAsync( + T request, + IAmABoxTransactionProvider transactionProvider, + bool continueOnCapturedContext = false, + CancellationToken cancellationToken = default + ) where T : class, IRequest; + + /// + /// Adds a message into the outbox, and returns the id of the saved message. + /// Intended for use with the Outbox pattern: http://gistlabs.com/2014/05/the-outbox/ normally you include the + /// call to DepositPostBox within the scope of the transaction to write corresponding entity state to your + /// database, that you want to signal via the request to downstream consumers + /// Pass deposited Guid to + /// + /// The requests to save to the outbox + /// Should we use the calling thread's synchronization context when continuing or a default thread synchronization context. Defaults to false + /// The Cancellation Token. + /// The type of the request + /// The type of transaction used by the outbox + /// + Task DepositPostAsync( + IEnumerable requests, + bool continueOnCapturedContext = false, + CancellationToken cancellationToken = default + ) where TRequest : class, IRequest; /// /// Adds a message into the outbox, and returns the id of the saved message. @@ -134,19 +224,25 @@ public interface IAmACommandProcessor /// Pass deposited Guid to /// /// The requests to save to the outbox + /// If using an Outbox, the transaction provider for the Outbox /// Should we use the calling thread's synchronization context when continuing or a default thread synchronization context. Defaults to false /// The Cancellation Token. /// The type of the request + /// The type of transaction used by the outbox /// - Task DepositPostAsync(IEnumerable requests, bool continueOnCapturedContext = false, - CancellationToken cancellationToken = default) where T : class, IRequest; + Task DepositPostAsync( + IEnumerable requests, + IAmABoxTransactionProvider transactionProvider = null, + bool continueOnCapturedContext = false, + CancellationToken cancellationToken = default + ) where T : class, IRequest; /// - /// Flushes the message box message given by to the broker. + /// Flushes the message box message given by to the broker. /// Intended for use with the Outbox pattern: http://gistlabs.com/2014/05/the-outbox/ - /// The posts to flush + /// The ids to flush /// - void ClearOutbox(params Guid[] posts); + void ClearOutbox(params Guid[] ids); /// /// Flushes any outstanding message box message to the broker. @@ -161,7 +257,7 @@ Task DepositPostAsync(IEnumerable requests, bool continueOnCapture /// Flushes the message box message given by to the broker. /// Intended for use with the Outbox pattern: http://gistlabs.com/2014/05/the-outbox/ /// - /// The posts to flush + /// The ids to flush Task ClearOutboxAsync(IEnumerable posts, bool continueOnCapturedContext = false, CancellationToken cancellationToken = default); /// diff --git a/src/Paramore.Brighter/IAmAControlBusSender.cs b/src/Paramore.Brighter/IAmAControlBusSender.cs index 4e527488ae..69b7e28919 100644 --- a/src/Paramore.Brighter/IAmAControlBusSender.cs +++ b/src/Paramore.Brighter/IAmAControlBusSender.cs @@ -27,29 +27,26 @@ THE SOFTWARE. */ namespace Paramore.Brighter { - /// - /// Interface IAmAControlBusSender - /// This is really just a 'convenience' wrapper over a command processor to make it easy to configure two different command processors, one for normal messages the other for control messages. - /// Why? The issue arises because an application providing a lot of monitoring messages might find that the load of those control messages begins to negatively impact the throughput of normal messages. - /// To avoid this you can put control messages over a seperate broker. (There are some availability advantages here too). - /// But many IoC containers make your life hard when you do this, as you have to indicate that you want to build the MonitorHandler with one command processor and the other handlers with another - /// Wrapping the Command Processor in this class helps to alleviate that issue, by taking a dependency on a seperate interface. - /// What goes over a control bus? - /// The Control Bus is used carry the following types of messages: + // Interface IAmAControlBusSender + // This is really just a 'convenience' wrapper over a command processor to make it easy to configure two different command processors, one for normal messages the other for control messages. + // Why? The issue arises because an application providing a lot of monitoring messages might find that the load of those control messages begins to negatively impact the throughput of normal messages. + // To avoid this you can put control messages over a seperate broker. (There are some availability advantages here too). + // But many IoC containers make your life hard when you do this, as you have to indicate that you want to build the MonitorHandler with one command processor and the other handlers with another + // Wrapping the Command Processor in this class helps to alleviate that issue, by taking a dependency on a seperate interface. + // What goes over a control bus? + // The Control Bus is used carry the following types of messages: // Configuration - Allows runtime configuration of a service activator node, including stopping and starting, adding and removing of channels, control of resources allocated to channels. // Heartbeat - A ping to service activator node to determine if it is still 'alive'. The node returns a message over a private queue established by the caller.The message also displays diagnostic information on the health of the node. // Exceptions— Any exceptions generated on the node may be sent by the Control Bus to monitoring systems. // Statistics— Each service activator node broadcasts statistics about the processing of messages which can be collated by a listener to the control bus to calculate the number of messages proceses, average throughput, average time to process a message, and so on.This data is split out by message type, so we can aggregate results. - /// - /// public interface IAmAControlBusSender { /// /// Posts the specified request. /// - /// /// The request. - void Post(T request) where T : class, IRequest; + /// + void Post(TRequest request) where TRequest : class, IRequest; } /// @@ -65,12 +62,16 @@ public interface IAmAControlBusSenderAsync /// /// Posts the specified request with async/await support. /// - /// /// The request. /// Should we use the calling thread's synchronization context when continuing or a default thread synchronization context. Defaults to false /// Allows the sender to cancel the request pipeline. Optional + /// The type of the request /// awaitable . - Task PostAsync(T request, bool continueOnCapturedContext = false, CancellationToken cancellationToken = default) where T : class, IRequest; + Task PostAsync( + TRequest request, + bool continueOnCapturedContext = false, + CancellationToken cancellationToken = default + ) where TRequest : class, IRequest; } } diff --git a/src/Paramore.Brighter/IAmAControlBusSenderFactory.cs b/src/Paramore.Brighter/IAmAControlBusSenderFactory.cs index 0ffbb1a756..0e96a8066b 100644 --- a/src/Paramore.Brighter/IAmAControlBusSenderFactory.cs +++ b/src/Paramore.Brighter/IAmAControlBusSenderFactory.cs @@ -36,9 +36,10 @@ public interface IAmAControlBusSenderFactory { /// /// Creates the specified configuration. /// - /// The gateway to the control bus /// The outbox to record outbound messages on the control bus + /// The list of producers to send with /// IAmAControlBusSender. - IAmAControlBusSender Create(IAmAnOutbox outbox, IAmAProducerRegistry producerRegistry); + IAmAControlBusSender Create(IAmAnOutbox outbox, IAmAProducerRegistry producerRegistry) + where T: Message; } } diff --git a/src/Paramore.Brighter/IAmAMessageRecoverer.cs b/src/Paramore.Brighter/IAmAMessageRecoverer.cs index dc0c177933..ca29dbe22e 100644 --- a/src/Paramore.Brighter/IAmAMessageRecoverer.cs +++ b/src/Paramore.Brighter/IAmAMessageRecoverer.cs @@ -8,6 +8,10 @@ namespace Paramore.Brighter /// public interface IAmAMessageRecoverer { - void Repost(List messageIds, IAmAnOutboxSync outBox, IAmAMessageProducerSync messageProducerSync); + void Repost( + List messageIds, + IAmAnOutboxSync outBox, + IAmAMessageProducerSync messageProducerSync + ) where T : Message; } } diff --git a/src/Paramore.Brighter/IAmARelationalDatabaseConfiguration.cs b/src/Paramore.Brighter/IAmARelationalDatabaseConfiguration.cs new file mode 100644 index 0000000000..d53d640044 --- /dev/null +++ b/src/Paramore.Brighter/IAmARelationalDatabaseConfiguration.cs @@ -0,0 +1,33 @@ +namespace Paramore.Brighter +{ + public interface IAmARelationalDatabaseConfiguration + { + /// + /// Is the message payload binary, or a UTF-8 string. Default is false or UTF-8 + /// + bool BinaryMessagePayload { get; } + + /// + /// Gets the connection string. + /// + /// The connection string. + string ConnectionString { get; } + + /// + /// Gets the name of the inbox table. + /// + /// The name of the inbox table. + string InBoxTableName { get; } + + /// + /// Gets the name of the outbox table. + /// + /// The name of the outbox table. + string OutBoxTableName { get; } + + /// + /// Gets the name of the queue table. + /// + string QueueStoreTable { get; } + } +} diff --git a/src/Paramore.Brighter/IAmARelationalDbConnectionProvider.cs b/src/Paramore.Brighter/IAmARelationalDbConnectionProvider.cs new file mode 100644 index 0000000000..e9ee5b64b3 --- /dev/null +++ b/src/Paramore.Brighter/IAmARelationalDbConnectionProvider.cs @@ -0,0 +1,27 @@ +using System.Data.Common; +using System.Threading; +using System.Threading.Tasks; + +namespace Paramore.Brighter +{ + /// + /// Providers an abstraction over a relational database connection; implementations may allow connection and + /// transaction sharing by holding state, thus a Close method is provided. + /// + public interface IAmARelationalDbConnectionProvider + { + /// + /// Gets a existing Connection; creates a new one if it does not exist + /// The connection is not opened, you need to open it yourself. + /// + /// A database connection + DbConnection GetConnection(); + + /// + /// Gets a existing Connection; creates a new one if it does not exist + /// The connection is not opened, you need to open it yourself. + /// + /// A database connection + Task GetConnectionAsync(CancellationToken cancellationToken); + } +} diff --git a/src/Paramore.Brighter/IAmATransactionConnectionProvider.cs b/src/Paramore.Brighter/IAmATransactionConnectionProvider.cs new file mode 100644 index 0000000000..b0ac47934e --- /dev/null +++ b/src/Paramore.Brighter/IAmATransactionConnectionProvider.cs @@ -0,0 +1,6 @@ +using System.Data.Common; + +namespace Paramore.Brighter +{ + public interface IAmATransactionConnectionProvider : IAmARelationalDbConnectionProvider, IAmABoxTransactionProvider { } +} diff --git a/src/Paramore.Brighter/IAmAnExternalBusService.cs b/src/Paramore.Brighter/IAmAnExternalBusService.cs new file mode 100644 index 0000000000..82f3197fef --- /dev/null +++ b/src/Paramore.Brighter/IAmAnExternalBusService.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Paramore.Brighter +{ + public interface IAmAnExternalBusService : IDisposable + { + /// + /// Used with RPC to call a remote service via the external bus + /// + /// The message to send + /// The type of the call + /// The type of the response + void CallViaExternalBus(Message outMessage) + where T : class, ICall where TResponse : class, IResponse; + + /// + /// This is the clear outbox for explicit clearing of messages. + /// + /// The ids of the posts that you would like to clear + /// Thrown if there is no async outbox defined + /// Thrown if a message cannot be found + void ClearOutbox(params Guid[] posts); + + /// + /// This is the clear outbox for explicit clearing of messages. + /// + /// The ids of the posts that you would like to clear + /// Should we use the same thread in the callback + /// Allow cancellation of the operation + /// Thrown if there is no async outbox defined + /// Thrown if a message cannot be found + Task ClearOutboxAsync(IEnumerable posts, bool continueOnCapturedContext, + CancellationToken cancellationToken); + + /// + /// This is the clear outbox for explicit clearing of messages. + /// + /// Maximum number to clear. + /// The minimum age of messages to be cleared in milliseconds. + /// Use the Async outbox and Producer + /// Use bulk sending capability of the message producer, this must be paired with useAsync. + /// Optional bag of arguments required by an outbox implementation to sweep + void ClearOutbox(int amountToClear, int minimumAge, bool useAsync, bool useBulk, + Dictionary args = null); + + /// + /// Do we have an async outbox defined? + /// + /// true if defined + bool HasAsyncOutbox(); + + /// + /// Do we have an async bulk outbox defined? + /// + /// true if defined + bool HasAsyncBulkOutbox(); + + /// + /// Do we have a synchronous outbox defined? + /// + /// true if defined + bool HasOutbox(); + + /// + /// Do we have a synchronous bulk outbox defined? + /// + /// true if defined + bool HasBulkOutbox(); + + /// + /// Retry an action via the policy engine + /// + /// The Action to try + /// + bool Retry(Action action); + } +} diff --git a/src/Paramore.Brighter/IAmAnOutbox.cs b/src/Paramore.Brighter/IAmAnOutbox.cs index f618018919..7333ced091 100644 --- a/src/Paramore.Brighter/IAmAnOutbox.cs +++ b/src/Paramore.Brighter/IAmAnOutbox.cs @@ -6,8 +6,7 @@ /// store the message into an OutBox to allow later replay of those messages in the event of failure. We automatically copy any posted message into the store /// We provide implementations of for various databases. Users using other databases should consider a Pull Request /// - /// - public interface IAmAnOutbox where T : Message + public interface IAmAnOutbox { } diff --git a/src/Paramore.Brighter/IAmAnOutboxAsync.cs b/src/Paramore.Brighter/IAmAnOutboxAsync.cs index 7cc15c52c9..d8b4f6930b 100644 --- a/src/Paramore.Brighter/IAmAnOutboxAsync.cs +++ b/src/Paramore.Brighter/IAmAnOutboxAsync.cs @@ -24,6 +24,7 @@ THE SOFTWARE. */ using System; using System.Collections.Generic; +using System.Data.Common; using System.Threading; using System.Threading.Tasks; @@ -36,8 +37,9 @@ namespace Paramore.Brighter /// We provide implementations of for various databases. Users using unsupported databases should consider a Pull /// request ///