From 7bcaec835c98333efb5d62949e5fb8ee21178aa0 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Mon, 20 Mar 2023 08:01:04 +0000 Subject: [PATCH 01/89] First pass with Sqlite; errors --- .../SqliteOutboxBuilder.cs | 37 ++++-- .../SqliteOutboxSync.cs | 88 ++++++++++--- .../SqliteConfiguration.cs | 31 ++--- .../RelationDatabaseOutbox.cs | 7 +- .../RelationalDatabaseOutboxConfiguration.cs | 49 ++++++++ ...iting_A_Message_To_A_Binary_Body_Outbox.cs | 117 ++++++++++++++++++ .../SqliteTestHelper.cs | 8 +- 7 files changed, 283 insertions(+), 54 deletions(-) create mode 100644 src/Paramore.Brighter/RelationalDatabaseOutboxConfiguration.cs create mode 100644 tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_Writing_A_Message_To_A_Binary_Body_Outbox.cs diff --git a/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutboxBuilder.cs b/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutboxBuilder.cs index 3aa0cec188..441d8ca120 100644 --- a/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutboxBuilder.cs +++ b/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutboxBuilder.cs @@ -29,7 +29,7 @@ namespace Paramore.Brighter.Outbox.Sqlite /// public class SqliteOutboxBuilder { - const string OutboxDdl = @"CREATE TABLE {0} + const string TextOutboxDdl = @"CREATE TABLE {0} ( [MessageId] uniqueidentifier NOT NULL, [Topic] nvarchar(255) NULL, @@ -43,17 +43,34 @@ [ContentType] NVARCHAR(128) NULL, [Body] ntext NULL, CONSTRAINT[PK_MessageId] PRIMARY KEY([MessageId]) );"; + + const string BinaryOutboxDdl = @"CREATE TABLE {0} + ( + [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] binary 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); } /// @@ -63,7 +80,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/SqliteOutboxSync.cs b/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutboxSync.cs index ed3b6fa3a7..fbf17294f4 100644 --- a/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutboxSync.cs +++ b/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutboxSync.cs @@ -26,6 +26,7 @@ THE SOFTWARE. */ using System; using System.Collections.Generic; using System.Data; +using System.IO; using System.Linq; using System.Text.Json; using System.Threading; @@ -40,7 +41,8 @@ namespace Paramore.Brighter.Outbox.Sqlite /// /// Implements an outbox using Sqlite as a backing store /// - public class SqliteOutboxSync : RelationDatabaseOutboxSync + public class SqliteOutboxSync : RelationDatabaseOutboxSync { private static readonly ILogger s_logger = ApplicationLogging.CreateLogger(); @@ -66,15 +68,18 @@ public SqliteOutboxSync(SqliteConfiguration configuration, ISqliteConnectionProv /// 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 SqliteOutboxSync(SqliteConfiguration configuration) : this(configuration, + new SqliteConnectionProvider(configuration)) { } - protected override void WriteToStore(IAmABoxTransactionConnectionProvider transactionConnectionProvider, Func commandFunc, + protected override void WriteToStore(IAmABoxTransactionConnectionProvider transactionConnectionProvider, + Func commandFunc, Action loggingAction) { var connectionProvider = _connectionProvider; - if (transactionConnectionProvider != null && transactionConnectionProvider is ISqliteTransactionConnectionProvider provider) + if (transactionConnectionProvider != null && + transactionConnectionProvider is ISqliteTransactionConnectionProvider provider) connectionProvider = provider; var connection = connectionProvider.GetConnection(); @@ -109,11 +114,14 @@ protected override void WriteToStore(IAmABoxTransactionConnectionProvider transa } } - protected override async Task WriteToStoreAsync(IAmABoxTransactionConnectionProvider transactionConnectionProvider, Func commandFunc, + protected override async Task WriteToStoreAsync( + IAmABoxTransactionConnectionProvider transactionConnectionProvider, + Func commandFunc, Action loggingAction, CancellationToken cancellationToken) { var connectionProvider = _connectionProvider; - if (transactionConnectionProvider != null && transactionConnectionProvider is ISqliteTransactionConnectionProvider provider) + if (transactionConnectionProvider != null && + transactionConnectionProvider is ISqliteTransactionConnectionProvider provider) connectionProvider = provider; var connection = await connectionProvider.GetConnectionAsync(cancellationToken); @@ -148,7 +156,8 @@ protected override async Task WriteToStoreAsync(IAmABoxTransactionConnectionProv } } - protected override T ReadFromStore(Func commandFunc, Func resultFunc) + protected override T ReadFromStore(Func commandFunc, + Func resultFunc) { var connection = _connectionProvider.GetConnection(); @@ -170,7 +179,8 @@ protected override T ReadFromStore(Func comm } } - 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); @@ -191,7 +201,7 @@ protected override async Task ReadFromStoreAsync(Func MapFunctionAsync(SqliteDataReader dr, CancellationToken cancellationToken) + protected override async Task MapFunctionAsync(SqliteDataReader dr, + CancellationToken cancellationToken) { using (dr) { @@ -270,18 +293,21 @@ protected override IEnumerable MapListFunction(SqliteDataReader dr) { messages.Add(MapAMessage(dr)); } + dr.Close(); return messages; } - protected override async Task> MapListFunctionAsync(SqliteDataReader dr, CancellationToken cancellationToken) + protected override async Task> MapListFunctionAsync(SqliteDataReader dr, + CancellationToken cancellationToken) { var messages = new List(); while (await dr.ReadAsync(cancellationToken)) { messages.Add(MapAMessage(dr)); } + dr.Close(); return messages; @@ -329,11 +355,33 @@ private Message MapAMessage(IDataReader dr) } } - var body = new MessageBody(dr.GetString(dr.GetOrdinal("Body"))); + var body = _configuration.BinaryMessagePayload + ? new MessageBody(GetBodyAsBytes(dr)) + : new MessageBody(dr.GetString(dr.GetOrdinal("Body"))); return new Message(header, body); } + private byte[] GetBodyAsBytes(IDataReader dr) + { + var i = dr.GetOrdinal("Body"); + using var payload = new MemoryStream(); + + var bufferSize = 4096; + var buffer = new byte[bufferSize]; + var bytesRead = dr.GetBytes(i, 0, buffer, 0, buffer.Length); + + payload.Write(buffer); + + while (bytesRead == buffer.Length) + { + bytesRead = dr.GetBytes(i, bytesRead, buffer, 0, buffer.Length); + payload.Write(buffer); + } + + return payload.ToArray(); + } + private static string GetTopic(IDataReader dr) { return dr.GetString(dr.GetOrdinal("Topic")); @@ -371,7 +419,8 @@ 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); + var dictionaryBag = + JsonSerializer.Deserialize>(headerBag, JsonSerialisationOptions.Options); return dictionaryBag; } @@ -392,6 +441,5 @@ private static DateTime GetTimeStamp(IDataReader dr) : dr.GetDateTime(ordinal); return timeStamp; } - } } diff --git a/src/Paramore.Brighter.Sqlite/SqliteConfiguration.cs b/src/Paramore.Brighter.Sqlite/SqliteConfiguration.cs index edf0a8afca..cb1c865463 100644 --- a/src/Paramore.Brighter.Sqlite/SqliteConfiguration.cs +++ b/src/Paramore.Brighter.Sqlite/SqliteConfiguration.cs @@ -24,7 +24,7 @@ THE SOFTWARE. */ namespace Paramore.Brighter.Sqlite { - public class SqliteConfiguration + public class SqliteConfiguration : RelationalDatabaseOutboxConfiguration { /// /// Initializes a new instance of the class. @@ -33,35 +33,22 @@ public class SqliteConfiguration /// 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) + /// Is the message payload binary, or a UTF-8 string, default is false or UTF-8 + public SqliteConfiguration( + string connectionString, + string outBoxTableName = null, + string inboxTableName = null, + string queueStoreTable = null, + bool binaryMessagePayload = false) + : base(connectionString, outBoxTableName, queueStoreTable, binaryMessagePayload) { - 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/RelationDatabaseOutbox.cs b/src/Paramore.Brighter/RelationDatabaseOutbox.cs index 19f3337d4b..102eb6cb5b 100644 --- a/src/Paramore.Brighter/RelationDatabaseOutbox.cs +++ b/src/Paramore.Brighter/RelationDatabaseOutbox.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Data; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -10,7 +11,11 @@ namespace Paramore.Brighter public abstract class RelationDatabaseOutboxSync : IAmAnOutboxSync, - IAmAnOutboxAsync + IAmAnOutboxAsync + where TConnection : IDbConnection, new() + where TCommand : IDbCommand, new() + where TDataReader: IDataReader + where TParameter : IDbDataParameter, new() { private readonly IRelationDatabaseOutboxQueries _queries; private readonly ILogger _logger; diff --git a/src/Paramore.Brighter/RelationalDatabaseOutboxConfiguration.cs b/src/Paramore.Brighter/RelationalDatabaseOutboxConfiguration.cs new file mode 100644 index 0000000000..7d75b8d38d --- /dev/null +++ b/src/Paramore.Brighter/RelationalDatabaseOutboxConfiguration.cs @@ -0,0 +1,49 @@ +namespace Paramore.Brighter +{ + public class RelationalDatabaseOutboxConfiguration + { + + /// + /// 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 + protected RelationalDatabaseOutboxConfiguration( + string connectionString, + string outBoxTableName = null, + string queueStoreTable = null, + bool binaryMessagePayload = false + ) + { + OutBoxTableName = outBoxTableName; + 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 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/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..c805329734 --- /dev/null +++ b/tests/Paramore.Brighter.Sqlite.Tests/Outbox/When_Writing_A_Message_To_A_Binary_Body_Outbox.cs @@ -0,0 +1,117 @@ +#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 SqliteOutboxSync _sqlOutboxSync; + 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(); + _sqlOutboxSync = new SqliteOutboxSync(new SqliteConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableName_Messages, binaryMessagePayload: true)); + 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"); + 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)); + _sqlOutboxSync.Add(_messageEarliest); + } + + [Fact] + public void When_Writing_A_Message_To_The_Outbox() + { + _storedMessage = _sqlOutboxSync.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.Should().Be(_messageEarliest.Header.TimeStamp); + _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); + _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/SqliteTestHelper.cs b/tests/Paramore.Brighter.Sqlite.Tests/SqliteTestHelper.cs index c881cd3640..7f86f9ec56 100644 --- a/tests/Paramore.Brighter.Sqlite.Tests/SqliteTestHelper.cs +++ b/tests/Paramore.Brighter.Sqlite.Tests/SqliteTestHelper.cs @@ -8,6 +8,7 @@ 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"; @@ -15,6 +16,11 @@ public class SqliteTestHelper private string connectionStringPath; private string connectionStringPathDir; + public SqliteTestHelper(bool binaryMessagePayload = false) + { + _binaryMessagePayload = binaryMessagePayload; + } + public void SetupCommandDb() { connectionStringPath = GetUniqueTestDbPathAndCreateDir(); @@ -26,7 +32,7 @@ public void SetupMessageDb() { connectionStringPath = GetUniqueTestDbPathAndCreateDir(); ConnectionString = $"DataSource=\"{connectionStringPath}\""; - CreateDatabaseWithTable(ConnectionString, SqliteOutboxBuilder.GetDDL(TableName_Messages)); + CreateDatabaseWithTable(ConnectionString, SqliteOutboxBuilder.GetDDL(TableName_Messages, hasBinaryMessagePayload: _binaryMessagePayload)); } private string GetUniqueTestDbPathAndCreateDir() From 39c67f185a727ffca2eb62f3a78a837244c28222 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Mon, 20 Mar 2023 08:01:27 +0000 Subject: [PATCH 02/89] First pass at binary payload with Sqlite --- Docker/dynamodb/shared-local-instance.db | Bin 0 -> 552960 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 Docker/dynamodb/shared-local-instance.db diff --git a/Docker/dynamodb/shared-local-instance.db b/Docker/dynamodb/shared-local-instance.db new file mode 100644 index 0000000000000000000000000000000000000000..881b750a6eb237392c9abed45ae60446e8761956 GIT binary patch literal 552960 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 literal 0 HcmV?d00001 From 25b9411d0f2f35cc5be5f8aa1b61216bdcd994e1 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Mon, 20 Mar 2023 08:19:29 +0000 Subject: [PATCH 03/89] Fix issues with SQL Parameter naming --- src/Paramore.Brighter.Outbox.Sqlite/SqliteQueries.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Paramore.Brighter.Outbox.Sqlite/SqliteQueries.cs b/src/Paramore.Brighter.Outbox.Sqlite/SqliteQueries.cs index 02f0b08e0e..8105fc1c97 100644 --- a/src/Paramore.Brighter.Outbox.Sqlite/SqliteQueries.cs +++ b/src/Paramore.Brighter.Outbox.Sqlite/SqliteQueries.cs @@ -7,9 +7,9 @@ public class SqliteQueries : IRelationDatabaseOutboxQueries 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 MarkDispatchedCommand { get; } = "UPDATE {0} SET Dispatched = @DispatchedAt WHERE MessageId = @MessageId"; + 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 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} )"; } From c295e8620dc4d17e4009f1955566b8827ecac6cb Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Mon, 20 Mar 2023 18:09:49 +0000 Subject: [PATCH 04/89] Problem with Sqlite --- src/Paramore.Brighter.Outbox.Sqlite/SqliteOutboxSync.cs | 3 +-- src/Paramore.Brighter.Outbox.Sqlite/SqliteQueries.cs | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutboxSync.cs b/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutboxSync.cs index fbf17294f4..86039d490d 100644 --- a/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutboxSync.cs +++ b/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutboxSync.cs @@ -27,7 +27,6 @@ THE SOFTWARE. */ using System.Collections.Generic; using System.Data; using System.IO; -using System.Linq; using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -227,7 +226,7 @@ protected override SqliteParameter[] CreatePagedOutstandingParameters(double mil protected override SqliteParameter CreateSqlParameter(string parameterName, object value) { - return new SqliteParameter(parameterName, value ?? DBNull.Value); + return new SqliteParameter("@" + parameterName, value ?? DBNull.Value); } protected override SqliteParameter[] InitAddDbParameters(Message message, int? position = null) diff --git a/src/Paramore.Brighter.Outbox.Sqlite/SqliteQueries.cs b/src/Paramore.Brighter.Outbox.Sqlite/SqliteQueries.cs index 8105fc1c97..d441ff4af7 100644 --- a/src/Paramore.Brighter.Outbox.Sqlite/SqliteQueries.cs +++ b/src/Paramore.Brighter.Outbox.Sqlite/SqliteQueries.cs @@ -7,9 +7,9 @@ public class SqliteQueries : IRelationDatabaseOutboxQueries 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 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 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} )"; } From e079ae256c55302339ddd05f617bc3aed234c64c Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Mon, 20 Mar 2023 18:28:56 +0000 Subject: [PATCH 05/89] Try to get Sqlite tests running --- .github/workflows/ci.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2df039c8e6..d1b0964613 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -389,6 +389,23 @@ jobs: aws sns list-topics - name: SQS Tests run: dotnet test ./tests/Paramore.Brighter.AWS.Tests/Paramore.Brighter.AWS.Tests.csproj --filter "Fragile!=CI" --configuration Release --logger "console;verbosity=normal" --blame -v n + + sqlite-ci: + runs-on: ubuntu-latest + timeout-minutes: 5 + needs: [ build ] + + steps: + - uses: actions/checkout@v3 + - name: Setup dotnet 6 + uses: actions/setup-dotnet@v3 + with: + dotnet-version: | + 6.0.x + - name: Install dependencies + run: dotnet restore + - name: Sqlite Tests + run: dotnet test ./tests/Paramore.Brighter.Sqlite.Tests/Paramore.Brighter.Sqlite.Tests.csproj --filter "Fragile!=CI" --configuration Release --logger "console;verbosity=normal" --blame -v n azure-ci: runs-on: ubuntu-latest From bce3a459da6704fe78624a563974d28d96c4b96d Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Tue, 21 Mar 2023 09:03:28 +0000 Subject: [PATCH 06/89] Erros with Sqlite --- src/Paramore.Brighter.Outbox.Sqlite/SqliteOutboxSync.cs | 2 +- src/Paramore.Brighter.Outbox.Sqlite/SqliteQueries.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutboxSync.cs b/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutboxSync.cs index 86039d490d..2e02e0fee6 100644 --- a/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutboxSync.cs +++ b/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutboxSync.cs @@ -226,7 +226,7 @@ protected override SqliteParameter[] CreatePagedOutstandingParameters(double mil protected override SqliteParameter CreateSqlParameter(string parameterName, object value) { - return new SqliteParameter("@" + parameterName, value ?? DBNull.Value); + return new SqliteParameter(parameterName, value ?? DBNull.Value); } protected override SqliteParameter[] InitAddDbParameters(Message message, int? position = null) diff --git a/src/Paramore.Brighter.Outbox.Sqlite/SqliteQueries.cs b/src/Paramore.Brighter.Outbox.Sqlite/SqliteQueries.cs index d441ff4af7..02f0b08e0e 100644 --- a/src/Paramore.Brighter.Outbox.Sqlite/SqliteQueries.cs +++ b/src/Paramore.Brighter.Outbox.Sqlite/SqliteQueries.cs @@ -7,8 +7,8 @@ public class SqliteQueries : IRelationDatabaseOutboxQueries 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 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 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} )"; From 450f9bb05b252e1a21929c1230f16bb87af9ce00 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Wed, 22 Mar 2023 18:52:15 +0000 Subject: [PATCH 07/89] Fix Sqlite Inbox --- .../SqliteOutboxBuilder.cs | 21 ++++++++++--------- .../SqliteOutboxSync.cs | 5 ++--- .../SqliteQueries.cs | 6 +++--- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutboxBuilder.cs b/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutboxBuilder.cs index 3aa0cec188..14ed93aee3 100644 --- a/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutboxBuilder.cs +++ b/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutboxBuilder.cs @@ -26,21 +26,22 @@ namespace Paramore.Brighter.Outbox.Sqlite { /// /// Provide SQL statement helpers for creation of an Outbox + /// Note that due to a case-related bug in Microsoft.Data.Core when comparing GUIDs in Sqlite, we use COLLATE NOCASE for the MessageId /// public class SqliteOutboxBuilder { const string OutboxDdl = @"CREATE TABLE {0} ( - [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, + [MessageId] TEXT NOT NULL COLLATE NOCASE, + [Topic] TEXT NULL, + [MessageType] TEXT NULL, + [Timestamp] TEXT NULL, + [CorrelationId] TEXT NULL, + [ReplyTo] TEXT NULL, + [ContentType] TEXT NULL, + [Dispatched] TEXT NULL, + [HeaderBag] TEXT NULL, + [Body] TEXT NULL, CONSTRAINT[PK_MessageId] PRIMARY KEY([MessageId]) );"; diff --git a/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutboxSync.cs b/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutboxSync.cs index ed3b6fa3a7..833bb01052 100644 --- a/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutboxSync.cs +++ b/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutboxSync.cs @@ -26,7 +26,6 @@ THE SOFTWARE. */ using System; using System.Collections.Generic; using System.Data; -using System.Linq; using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -230,8 +229,8 @@ protected override SqliteParameter[] InitAddDbParameters(Message message, int? p new SqliteParameter($"@{prefix}Topic", SqliteType.Text) { Value = message.Header.Topic }, new SqliteParameter($"@{prefix}Timestamp", SqliteType.Text) { Value = message.Header.TimeStamp.ToString("s") }, new SqliteParameter($"@{prefix}CorrelationId", SqliteType.Text) { Value = message.Header.CorrelationId }, - new SqliteParameter($"@{prefix}ReplyTo", message.Header.ReplyTo), - new SqliteParameter($"@{prefix}ContentType", message.Header.ContentType), + new SqliteParameter($"@{prefix}ReplyTo", SqliteType.Text) {Value = message.Header.ReplyTo}, + new SqliteParameter($"@{prefix}ContentType", SqliteType.Text) {Value = message.Header.ContentType}, new SqliteParameter($"@{prefix}HeaderBag", SqliteType.Text) { Value = bagJson }, new SqliteParameter($"@{prefix}Body", SqliteType.Text) { Value = message.Body.Value } }; diff --git a/src/Paramore.Brighter.Outbox.Sqlite/SqliteQueries.cs b/src/Paramore.Brighter.Outbox.Sqlite/SqliteQueries.cs index 02f0b08e0e..c54b6e760e 100644 --- a/src/Paramore.Brighter.Outbox.Sqlite/SqliteQueries.cs +++ b/src/Paramore.Brighter.Outbox.Sqlite/SqliteQueries.cs @@ -2,9 +2,9 @@ { public class SqliteQueries : 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 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 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 MarkDispatchedCommand { get; } = "UPDATE {0} SET Dispatched = @DispatchedAt WHERE MessageId = @MessageId"; From 29a71a07b1836bc86aefd8a3645b69bf2d5df218 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Fri, 24 Mar 2023 14:00:38 +0000 Subject: [PATCH 08/89] Support binary payloads in Sqlite --- .../SqliteOutboxBuilder.cs | 15 +----------- .../SqliteOutboxSync.cs | 24 ++++++------------- 2 files changed, 8 insertions(+), 31 deletions(-) diff --git a/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutboxBuilder.cs b/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutboxBuilder.cs index b6726b82b5..963d0120c2 100644 --- a/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutboxBuilder.cs +++ b/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutboxBuilder.cs @@ -47,18 +47,6 @@ CONSTRAINT[PK_MessageId] PRIMARY KEY([MessageId]) const string BinaryOutboxDdl = @"CREATE TABLE {0} ( -<<<<<<< HEAD - [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] binary NULL, -======= [MessageId] TEXT NOT NULL COLLATE NOCASE, [Topic] TEXT NULL, [MessageType] TEXT NULL, @@ -68,8 +56,7 @@ [ContentType] NVARCHAR(128) NULL, [ContentType] TEXT NULL, [Dispatched] TEXT NULL, [HeaderBag] TEXT NULL, - [Body] TEXT NULL, ->>>>>>> master + [Body] BLOB NULL, CONSTRAINT[PK_MessageId] PRIMARY KEY([MessageId]) );"; diff --git a/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutboxSync.cs b/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutboxSync.cs index 38c76e776d..b75582e28c 100644 --- a/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutboxSync.cs +++ b/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutboxSync.cs @@ -26,6 +26,7 @@ THE SOFTWARE. */ using System; using System.Collections.Generic; using System.Data; +using System.IO; using System.Linq; using System.Text.Json; using System.Threading; @@ -355,30 +356,19 @@ private Message MapAMessage(IDataReader dr) } var body = _configuration.BinaryMessagePayload - ? new MessageBody(GetBodyAsBytes(dr)) + ? new MessageBody(GetBodyAsBytes((SqliteDataReader)dr)) : new MessageBody(dr.GetString(dr.GetOrdinal("Body"))); return new Message(header, body); } - private byte[] GetBodyAsBytes(IDataReader dr) + private byte[] GetBodyAsBytes(SqliteDataReader dr) { var i = dr.GetOrdinal("Body"); - using var payload = new MemoryStream(); - - var bufferSize = 4096; - var buffer = new byte[bufferSize]; - var bytesRead = dr.GetBytes(i, 0, buffer, 0, buffer.Length); - - payload.Write(buffer); - - while (bytesRead == buffer.Length) - { - bytesRead = dr.GetBytes(i, bytesRead, buffer, 0, buffer.Length); - payload.Write(buffer); - } - - return payload.ToArray(); + var body = dr.GetStream(i); + var buffer = new byte[body.Length]; + body.Read(buffer); + return buffer; } private static string GetTopic(IDataReader dr) From d1cabbd7e593185914029231a8658d15abd525bd Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Mon, 3 Apr 2023 19:47:37 +0100 Subject: [PATCH 09/89] Fix MySQL to support binary; Remove Sync suffix from Outbox name --- .../MySqlConfiguration.cs | 31 ++--- .../MySqlConnectionProvider.cs | 2 +- .../MsSqlOutbox.cs | 2 +- .../MySqlOutboxBuilder.cs | 34 +++-- .../MySqlOutboxSync.cs | 120 ++++++++++++++---- .../ServiceCollectionExtensions.cs | 8 +- .../ServiceCollectionExtensions.cs | 4 +- .../SqliteOutboxSync.cs | 14 +- .../SqliteConfiguration.cs | 1 + .../RelationDatabaseOutbox.cs | 4 +- .../MySqlTestHelper.cs | 19 ++- .../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 | 81 ++++++++++++ ...ng_a_message_to_the_message_store_async.cs | 8 +- .../When_writing_a_message_to_the_outbox.cs | 8 +- .../When_writing_messages_to_the_outbox.cs | 16 +-- ...en_writing_messages_to_the_outbox_async.cs | 16 +-- .../Outbox/SQlOutboxMigrationTests.cs | 8 +- .../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 | 8 +- .../When_Writing_A_Message_To_The_Outbox.cs | 8 +- ...n_Writing_A_Message_To_The_Outbox_Async.cs | 8 +- .../When_Writing_Messages_To_The_Outbox.cs | 16 +-- ...en_Writing_Messages_To_The_Outbox_Async.cs | 16 +-- ..._are_received_and_Dispatched_bulk_Async.cs | 4 +- .../SqliteTestHelper.cs | 28 ++-- 40 files changed, 399 insertions(+), 239 deletions(-) create mode 100644 tests/Paramore.Brighter.MySQL.Tests/Outbox/When_writing_a_message_to_a_binary_body_outbox.cs diff --git a/src/Paramore.Brighter.MySql/MySqlConfiguration.cs b/src/Paramore.Brighter.MySql/MySqlConfiguration.cs index cca6c8d97c..b356b0a45b 100644 --- a/src/Paramore.Brighter.MySql/MySqlConfiguration.cs +++ b/src/Paramore.Brighter.MySql/MySqlConfiguration.cs @@ -24,7 +24,7 @@ THE SOFTWARE. */ namespace Paramore.Brighter.MySql { - public class MySqlConfiguration + public class MySqlConfiguration : RelationalDatabaseOutboxConfiguration { /// /// Initializes a new instance of the class. @@ -33,25 +33,18 @@ public class MySqlConfiguration /// 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) + /// Is the message payload binary, or a UTF-8 string, default is false or UTF-8 + public MySqlConfiguration( + string connectionString, + string outBoxTableName = null, + string inboxTableName = null, + string queueStoreTable = null, + bool binaryMessagePayload = false + ) + : base(connectionString, outBoxTableName, queueStoreTable, binaryMessagePayload) { - 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. @@ -59,9 +52,5 @@ public MySqlConfiguration(string connectionString, string outBoxTableName = null /// 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 d72f1ab721..f5b5acb434 100644 --- a/src/Paramore.Brighter.MySql/MySqlConnectionProvider.cs +++ b/src/Paramore.Brighter.MySql/MySqlConnectionProvider.cs @@ -39,7 +39,7 @@ public class MySqlConnectionProvider : IMySqlConnectionProvider /// Initialise a new instance of Sqlte Connection provider from a connection string /// /// Ms Sql Configuration - public MySqlConnectionProvider(MySqlConfiguration configuration) + public MySqlConnectionProvider(RelationalDatabaseOutboxConfiguration configuration) { _connectionString = configuration.ConnectionString; } diff --git a/src/Paramore.Brighter.Outbox.MsSql/MsSqlOutbox.cs b/src/Paramore.Brighter.Outbox.MsSql/MsSqlOutbox.cs index 84ed6610e8..686ddc6f6c 100644 --- a/src/Paramore.Brighter.Outbox.MsSql/MsSqlOutbox.cs +++ b/src/Paramore.Brighter.Outbox.MsSql/MsSqlOutbox.cs @@ -40,7 +40,7 @@ namespace Paramore.Brighter.Outbox.MsSql /// Class MsSqlOutbox. /// public class MsSqlOutbox : - RelationDatabaseOutboxSync + RelationDatabaseOutbox { private const int MsSqlDuplicateKeyError_UniqueIndexViolation = 2601; private const int MsSqlDuplicateKeyError_UniqueConstraintViolation = 2627; diff --git a/src/Paramore.Brighter.Outbox.MySql/MySqlOutboxBuilder.cs b/src/Paramore.Brighter.Outbox.MySql/MySqlOutboxBuilder.cs index 0341cb209d..f4f10c4f18 100644 --- a/src/Paramore.Brighter.Outbox.MySql/MySqlOutboxBuilder.cs +++ b/src/Paramore.Brighter.Outbox.MySql/MySqlOutboxBuilder.cs @@ -27,12 +27,12 @@ THE SOFTWARE. */ 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 , @@ -49,18 +49,37 @@ public class MySqlOutboxBuilder PRIMARY KEY (`MessageId`) ) ENGINE = InnoDB;"; + 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 , + `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 = @"SHOW TABLES LIKE '{0}'; "; /// /// 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); + + return string.Format(hasBinaryMessagePayload ? BinaryOutboxDdl : TextOutboxDdl, outboxTableName); } /// @@ -74,7 +93,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/MySqlOutboxSync.cs b/src/Paramore.Brighter.Outbox.MySql/MySqlOutboxSync.cs index 985b94bed0..d36f2485d9 100644 --- a/src/Paramore.Brighter.Outbox.MySql/MySqlOutboxSync.cs +++ b/src/Paramore.Brighter.Outbox.MySql/MySqlOutboxSync.cs @@ -39,37 +39,41 @@ namespace Paramore.Brighter.Outbox.MySql /// /// Class MySqlOutbox. /// - 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()) + public MySqlOutbox(MySqlConfiguration configuration, IMySqlConnectionProvider connectionProvider) : base( + configuration.OutBoxTableName, new MySqlQueries(), ApplicationLogging.CreateLogger()) { _configuration = configuration; _connectionProvider = connectionProvider; ContinueOnCapturedContext = false; } - public MySqlOutboxSync(MySqlConfiguration configuration) : this(configuration, new MySqlConnectionProvider(configuration)) + public MySqlOutbox(MySqlConfiguration configuration) : this(configuration, + new MySqlConnectionProvider(configuration)) { } - protected override void WriteToStore(IAmABoxTransactionConnectionProvider transactionConnectionProvider, Func commandFunc, + protected override void WriteToStore(IAmABoxTransactionConnectionProvider transactionConnectionProvider, + Func commandFunc, Action loggingAction) { var connectionProvider = _connectionProvider; - if (transactionConnectionProvider != null && transactionConnectionProvider is IMySqlTransactionConnectionProvider provider) + if (transactionConnectionProvider != null && + transactionConnectionProvider is IMySqlTransactionConnectionProvider provider) connectionProvider = provider; var connection = connectionProvider.GetConnection(); if (connection.State != ConnectionState.Open) - connection.Open(); + connection.Open(); using (var command = commandFunc.Invoke(connection)) { try @@ -99,14 +103,18 @@ protected override void WriteToStore(IAmABoxTransactionConnectionProvider transa } } - protected override async Task WriteToStoreAsync(IAmABoxTransactionConnectionProvider transactionConnectionProvider, Func commandFunc, + protected override async Task WriteToStoreAsync( + IAmABoxTransactionConnectionProvider transactionConnectionProvider, + Func commandFunc, Action loggingAction, CancellationToken cancellationToken) { var connectionProvider = _connectionProvider; - if (transactionConnectionProvider != null && transactionConnectionProvider is IMySqlTransactionConnectionProvider provider) + if (transactionConnectionProvider != null && + transactionConnectionProvider is IMySqlTransactionConnectionProvider provider) connectionProvider = provider; - 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); @@ -139,7 +147,8 @@ protected override async Task WriteToStoreAsync(IAmABoxTransactionConnectionProv } } - protected override T ReadFromStore(Func commandFunc, Func resultFunc) + protected override T ReadFromStore(Func commandFunc, + Func resultFunc) { var connection = _connectionProvider.GetConnection(); @@ -161,7 +170,8 @@ protected override T ReadFromStore(Func comman } } - 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); @@ -199,7 +209,7 @@ protected override MySqlParameter CreateSqlParameter(string parameterName, objec { return new MySqlParameter { ParameterName = parameterName, Value = value }; } - + protected override MySqlParameter[] InitAddDbParameters(Message message, int? position = null) { @@ -207,19 +217,57 @@ protected override MySqlParameter[] InitAddDbParameters(Message message, int? po var bagJson = JsonSerializer.Serialize(message.Header.Bag, JsonSerialisationOptions.Options); return new[] { - new MySqlParameter { ParameterName = $"@{prefix}MessageId", DbType = DbType.String, Value = message.Id.ToString() }, - new MySqlParameter { ParameterName = $"@{prefix}MessageType", DbType = DbType.String, Value = message.Header.MessageType.ToString() }, - new MySqlParameter { ParameterName = $"@{prefix}Topic", DbType = DbType.String, Value = message.Header.Topic, }, - new MySqlParameter { ParameterName = $"@{prefix}Timestamp", DbType = DbType.DateTime2, Value = message.Header.TimeStamp.ToUniversalTime() }, //always store in UTC, as this is how we query messages - new MySqlParameter { ParameterName = $"@{prefix}CorrelationId", DbType = DbType.String, Value = message.Header.CorrelationId.ToString() }, - new MySqlParameter { ParameterName = $"@{prefix}ReplyTo", DbType = DbType.String, Value = message.Header.ReplyTo }, - new MySqlParameter { ParameterName = $"@{prefix}ContentType", DbType = DbType.String, Value = message.Header.ContentType }, + new MySqlParameter + { + ParameterName = $"@{prefix}MessageId", DbType = DbType.String, Value = message.Id.ToString() + }, + new MySqlParameter + { + ParameterName = $"@{prefix}MessageType", + DbType = DbType.String, + Value = message.Header.MessageType.ToString() + }, + new MySqlParameter + { + ParameterName = $"@{prefix}Topic", DbType = DbType.String, Value = message.Header.Topic, + }, + new MySqlParameter + { + ParameterName = $"@{prefix}Timestamp", + DbType = DbType.DateTime2, + Value = message.Header.TimeStamp.ToUniversalTime() + }, //always store in UTC, as this is how we query messages + new MySqlParameter + { + ParameterName = $"@{prefix}CorrelationId", + DbType = DbType.String, + Value = message.Header.CorrelationId.ToString() + }, + new MySqlParameter + { + ParameterName = $"@{prefix}ReplyTo", DbType = DbType.String, Value = message.Header.ReplyTo + }, + new MySqlParameter + { + ParameterName = $"@{prefix}ContentType", + DbType = DbType.String, + Value = message.Header.ContentType + }, new MySqlParameter { ParameterName = $"@{prefix}HeaderBag", DbType = DbType.String, Value = bagJson }, - new MySqlParameter { ParameterName = $"@{prefix}Body", DbType = DbType.String, Value = message.Body.Value } + _configuration.BinaryMessagePayload + ? new MySqlParameter + { + ParameterName = $"@{prefix}Body", DbType = DbType.Binary, Value = message.Body.Value + } + : new MySqlParameter + { + ParameterName = $"@{prefix}Body", DbType = DbType.String, Value = message.Body.Value + } }; } - - protected override MySqlParameter[] CreatePagedOutstandingParameters(double milliSecondsSinceAdded, int pageSize, int pageNumber) + + protected override MySqlParameter[] CreatePagedOutstandingParameters(double milliSecondsSinceAdded, + int pageSize, int pageNumber) { var offset = (pageNumber - 1) * pageSize; var parameters = new MySqlParameter[3]; @@ -229,7 +277,7 @@ protected override MySqlParameter[] CreatePagedOutstandingParameters(double mill return parameters; } - + protected override Message MapFunction(MySqlDataReader dr) { if (dr.Read()) @@ -257,18 +305,21 @@ protected override IEnumerable MapListFunction(MySqlDataReader dr) { messages.Add(MapAMessage(dr)); } + dr.Close(); return messages; } - protected override async Task> MapListFunctionAsync(MySqlDataReader dr, CancellationToken cancellationToken) + protected override async Task> MapListFunctionAsync(MySqlDataReader dr, + CancellationToken cancellationToken) { var messages = new List(); while (await dr.ReadAsync(cancellationToken)) { messages.Add(MapAMessage(dr)); } + dr.Close(); return messages; @@ -315,11 +366,23 @@ private Message MapAMessage(IDataReader dr) } } - var body = new MessageBody(dr.GetString(dr.GetOrdinal("Body"))); + var body = _configuration.BinaryMessagePayload + ? new MessageBody(GetBodyAsBytes((MySqlDataReader)dr)) + : new MessageBody(dr.GetString(dr.GetOrdinal("Body"))); return new Message(header, body); } + private byte[] GetBodyAsBytes(MySqlDataReader dr) + { + var i = dr.GetOrdinal("Body"); + var body = dr.GetStream(i); + long bodyLength = body.Length; + var buffer = new byte[bodyLength]; + body.Read(buffer,0, (int)bodyLength); + return buffer; + } + private static string GetTopic(IDataReader dr) { return dr.GetString(dr.GetOrdinal("Topic")); @@ -357,7 +420,8 @@ 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); + var dictionaryBag = + JsonSerializer.Deserialize>(headerBag, JsonSerialisationOptions.Options); return dictionaryBag; } diff --git a/src/Paramore.Brighter.Outbox.MySql/ServiceCollectionExtensions.cs b/src/Paramore.Brighter.Outbox.MySql/ServiceCollectionExtensions.cs index c162c07b46..6866bd12c2 100644 --- a/src/Paramore.Brighter.Outbox.MySql/ServiceCollectionExtensions.cs +++ b/src/Paramore.Brighter.Outbox.MySql/ServiceCollectionExtensions.cs @@ -23,9 +23,9 @@ public static class ServiceCollectionExtensions /// -- 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) + this IBrighterBuilder brighterBuilder, RelationalDatabaseOutboxConfiguration configuration, Type connectionProvider, ServiceLifetime serviceLifetime = ServiceLifetime.Singleton) { - brighterBuilder.Services.AddSingleton(configuration); + brighterBuilder.Services.AddSingleton(configuration); brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IMySqlConnectionProvider), connectionProvider, serviceLifetime)); brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmAnOutboxSync), BuildMySqlOutboxOutbox, serviceLifetime)); @@ -53,12 +53,12 @@ public static IBrighterBuilder UseMySqTransactionConnectionProvider( return brighterBuilder; } - private static MySqlOutboxSync BuildMySqlOutboxOutbox(IServiceProvider provider) + private static MySqlOutbox BuildMySqlOutboxOutbox(IServiceProvider provider) { var config = provider.GetService(); var connectionProvider = provider.GetService(); - return new MySqlOutboxSync(config, connectionProvider); + return new MySqlOutbox(config, connectionProvider); } } } diff --git a/src/Paramore.Brighter.Outbox.Sqlite/ServiceCollectionExtensions.cs b/src/Paramore.Brighter.Outbox.Sqlite/ServiceCollectionExtensions.cs index a2649dbbb6..350245a0ad 100644 --- a/src/Paramore.Brighter.Outbox.Sqlite/ServiceCollectionExtensions.cs +++ b/src/Paramore.Brighter.Outbox.Sqlite/ServiceCollectionExtensions.cs @@ -53,12 +53,12 @@ public static IBrighterBuilder UseSqliteTransactionConnectionProvider( return brighterBuilder; } - private static SqliteOutboxSync BuildSqliteOutbox(IServiceProvider provider) + private static SqliteOutbox BuildSqliteOutbox(IServiceProvider provider) { var config = provider.GetService(); var connectionProvider = provider.GetService(); - return new SqliteOutboxSync(config, connectionProvider); + return new SqliteOutbox(config, connectionProvider); } } } diff --git a/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutboxSync.cs b/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutboxSync.cs index b75582e28c..e8c203495c 100644 --- a/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutboxSync.cs +++ b/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutboxSync.cs @@ -41,10 +41,10 @@ namespace Paramore.Brighter.Outbox.Sqlite /// /// Implements an outbox using Sqlite as a backing store /// - public class SqliteOutboxSync : RelationDatabaseOutboxSync { - 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; @@ -52,12 +52,12 @@ public class SqliteOutboxSync : RelationDatabaseOutboxSync - /// 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(SqliteConfiguration configuration, ISqliteConnectionProvider connectionProvider) : base( + configuration.OutBoxTableName, new SqliteQueries(), ApplicationLogging.CreateLogger()) { _configuration = configuration; ContinueOnCapturedContext = false; @@ -65,10 +65,10 @@ 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, + public SqliteOutbox(SqliteConfiguration configuration) : this(configuration, new SqliteConnectionProvider(configuration)) { } diff --git a/src/Paramore.Brighter.Sqlite/SqliteConfiguration.cs b/src/Paramore.Brighter.Sqlite/SqliteConfiguration.cs index cb1c865463..b80f07707b 100644 --- a/src/Paramore.Brighter.Sqlite/SqliteConfiguration.cs +++ b/src/Paramore.Brighter.Sqlite/SqliteConfiguration.cs @@ -42,6 +42,7 @@ public SqliteConfiguration( bool binaryMessagePayload = false) : base(connectionString, outBoxTableName, queueStoreTable, binaryMessagePayload) { + InBoxTableName = inboxTableName; } diff --git a/src/Paramore.Brighter/RelationDatabaseOutbox.cs b/src/Paramore.Brighter/RelationDatabaseOutbox.cs index 102eb6cb5b..2d24a60330 100644 --- a/src/Paramore.Brighter/RelationDatabaseOutbox.cs +++ b/src/Paramore.Brighter/RelationDatabaseOutbox.cs @@ -10,7 +10,7 @@ namespace Paramore.Brighter { public abstract class - RelationDatabaseOutboxSync : IAmAnOutboxSync, + RelationDatabaseOutbox : IAmAnOutboxSync, IAmAnOutboxAsync where TConnection : IDbConnection, new() where TCommand : IDbCommand, new() @@ -21,7 +21,7 @@ public abstract class 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; diff --git a/tests/Paramore.Brighter.MySQL.Tests/MySqlTestHelper.cs b/tests/Paramore.Brighter.MySQL.Tests/MySqlTestHelper.cs index 2fa7c781c8..512c458e2f 100644 --- a/tests/Paramore.Brighter.MySQL.Tests/MySqlTestHelper.cs +++ b/tests/Paramore.Brighter.MySQL.Tests/MySqlTestHelper.cs @@ -9,11 +9,13 @@ namespace Paramore.Brighter.MySQL.Tests { public class MySqlTestHelper { + private readonly bool _binaryMessagePayload; private string _tableName; private MySqlSettings _mysqlSettings; - public MySqlTestHelper() + public MySqlTestHelper(bool binaryMessagePayload = false) { + _binaryMessagePayload = binaryMessagePayload; var builder = new ConfigurationBuilder().AddEnvironmentVariables(); var configuration = builder.Build(); @@ -23,7 +25,7 @@ public MySqlTestHelper() _tableName = $"test_{Guid.NewGuid()}"; } - public void CreateDatabase() + public void CreateDatabase() { using (var connection = new MySqlConnection(_mysqlSettings.TestsMasterConnectionString)) { @@ -48,9 +50,11 @@ public void SetupCommandDb() CreateInboxTable(); } - public MySqlInboxConfiguration InboxConfiguration => new MySqlInboxConfiguration(_mysqlSettings.TestsBrighterConnectionString, _tableName); + public MySqlInboxConfiguration InboxConfiguration => + new(_mysqlSettings.TestsBrighterConnectionString, _tableName); - public MySqlConfiguration OutboxConfiguration => new MySqlConfiguration(_mysqlSettings.TestsBrighterConnectionString, _tableName); + public MySqlConfiguration OutboxConfiguration => + new(_mysqlSettings.TestsBrighterConnectionString, _tableName, binaryMessagePayload: _binaryMessagePayload); public void CleanUpDb() { @@ -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..5aceefec00 --- /dev/null +++ b/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_writing_a_message_to_a_binary_body_outbox.cs @@ -0,0 +1,81 @@ +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"); + 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 })); + _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.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.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); + _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..bc9c5982c2 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,9 +66,9 @@ 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); 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..64dccffaae 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", @@ -69,13 +69,13 @@ 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); 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..ac99baf3ab 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,7 +36,7 @@ namespace Paramore.Brighter.MySQL.Tests.Outbox public class MySqlOutboxWritngMessagesTests { private readonly MySqlTestHelper _mySqlTestHelper; - private readonly MySqlOutboxSync _mySqlOutboxSync; + private readonly MySqlOutbox _mySqlOutbox; private readonly Message _messageEarliest; private readonly Message _message2; private readonly Message _messageLatest; @@ -46,7 +46,7 @@ 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")); @@ -57,11 +57,11 @@ public MySqlOutboxWritngMessagesTests() [Fact] public void When_Writing_Messages_To_The_Outbox() { - _mySqlOutboxSync.Add(_messageEarliest); - _mySqlOutboxSync.Add(_message2); - _mySqlOutboxSync.Add(_messageLatest); + _mySqlOutbox.Add(_messageEarliest); + _mySqlOutbox.Add(_message2); + _mySqlOutbox.Add(_messageLatest); - _retrievedMessages = _mySqlOutboxSync.Get(); + _retrievedMessages = _mySqlOutbox.Get(); //should read first message last from the outbox _retrievedMessages.Last().Id.Should().Be(_messageEarliest.Id); @@ -75,8 +75,8 @@ public void When_Writing_Messages_To_The_Outbox() public void When_Writing_Messages_To_The_Outbox_Bulk() { var messages = new List { _messageEarliest, _message2, _messageLatest }; - _mySqlOutboxSync.Add(messages); - _retrievedMessages = _mySqlOutboxSync.Get(); + _mySqlOutbox.Add(messages); + _retrievedMessages = _mySqlOutbox.Get(); //should read first message last from the outbox _retrievedMessages.Last().Id.Should().Be(_messageEarliest.Id); 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..8e93c8cc63 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,7 +37,7 @@ namespace Paramore.Brighter.MySQL.Tests.Outbox public class MySqlOutboxWritingMessagesAsyncTests : IDisposable { private readonly MySqlTestHelper _mySqlTestHelper; - private readonly MySqlOutboxSync _mySqlOutboxSync; + private readonly MySqlOutbox _mySqlOutbox; private Message _message2; private Message _messageEarliest; private Message _messageLatest; @@ -47,7 +47,7 @@ public MySqlOutboxWritingMessagesAsyncTests() { _mySqlTestHelper = new MySqlTestHelper(); _mySqlTestHelper.SetupMessageDb(); - _mySqlOutboxSync = new MySqlOutboxSync(_mySqlTestHelper.OutboxConfiguration); + _mySqlOutbox = new MySqlOutbox(_mySqlTestHelper.OutboxConfiguration); } [Fact] @@ -55,7 +55,7 @@ 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); @@ -69,9 +69,9 @@ 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); @@ -84,13 +84,13 @@ public async Task When_Writing_Messages_To_The_Outbox_Async_Bulk() 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); + if(addMessagesToOutbox) await _mySqlOutbox.AddAsync(_messageEarliest); _message2 = new Message(new MessageHeader(Guid.NewGuid(), "Test2", MessageType.MT_COMMAND, DateTime.UtcNow.AddHours(-2)), new MessageBody("Body2")); - if(addMessagesToOutbox) await _mySqlOutboxSync.AddAsync(_message2); + if(addMessagesToOutbox) await _mySqlOutbox.AddAsync(_message2); _messageLatest = new Message(new MessageHeader(Guid.NewGuid(), "Test3", MessageType.MT_COMMAND, DateTime.UtcNow.AddHours(-1)), new MessageBody("Body3")); - if(addMessagesToOutbox) await _mySqlOutboxSync.AddAsync(_messageLatest); + if(addMessagesToOutbox) await _mySqlOutbox.AddAsync(_messageLatest); return new List { _messageEarliest, _message2, _messageLatest }; } diff --git a/tests/Paramore.Brighter.Sqlite.Tests/Outbox/SQlOutboxMigrationTests.cs b/tests/Paramore.Brighter.Sqlite.Tests/Outbox/SQlOutboxMigrationTests.cs index 07bb4e7153..84894b1742 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 SqliteConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableNameMessages)); _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.TableNameMessages); 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); 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..a3524147c6 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(new SqliteConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableNameMessages)); _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..d66229457d 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(new SqliteConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableNameMessages)); _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..12e51f572e 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(new SqliteConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableNameMessages)); _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..2673d86692 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(new SqliteConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableNameMessages)); _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..d9d1dfc4d3 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(new SqliteConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableNameMessages)); _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..abff2cbea3 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(new SqliteConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableNameMessages)); _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..08c20a73da 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(new SqliteConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableNameMessages)); _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 index c805329734..67efb6c72a 100644 --- 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 @@ -35,7 +35,7 @@ namespace Paramore.Brighter.Sqlite.Tests.Outbox public class SqliteOutboxWritingBinaryMessageTests : 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"; @@ -53,7 +53,7 @@ public SqliteOutboxWritingBinaryMessageTests() { _sqliteTestHelper = new SqliteTestHelper(binaryMessagePayload: true); _sqliteTestHelper.SetupMessageDb(); - _sqlOutboxSync = new SqliteOutboxSync(new SqliteConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableName_Messages, binaryMessagePayload: true)); + _sqlOutbox = new SqliteOutbox(new SqliteConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableNameMessages, binaryMessagePayload: true)); var messageHeader = new MessageHeader( messageId:Guid.NewGuid(), topic: "test_topic", @@ -74,13 +74,13 @@ public SqliteOutboxWritingBinaryMessageTests() var bytes = System.Text.Encoding.UTF8.GetBytes("message body"); _messageEarliest = new Message(messageHeader, new MessageBody(bytes, contentType:"application/octet-stream", CharacterEncoding.Raw)); - _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.Bytes.Should().Equal(_messageEarliest.Body.Bytes); var bodyAsString = Encoding.UTF8.GetString(_storedMessage.Body.Bytes); 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..f22df4b0ef 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(new SqliteConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableNameMessages)); var messageHeader = new MessageHeader( messageId:Guid.NewGuid(), topic: "test_topic", @@ -70,13 +70,13 @@ 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 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..02deb26b99 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,7 +52,7 @@ public SqliteOutboxWritingMessageAsyncTests() { _sqliteTestHelper = new SqliteTestHelper(); _sqliteTestHelper.SetupMessageDb(); - _sqlOutboxSync = new SqliteOutboxSync(new SqliteConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableName_Messages)); + _sqlOutbox = new SqliteOutbox(new SqliteConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableNameMessages)); var messageHeader = new MessageHeader( Guid.NewGuid(), @@ -71,9 +71,9 @@ 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); 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..04288ca26b 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,7 +36,7 @@ namespace Paramore.Brighter.Sqlite.Tests.Outbox public class SqlOutboxWritngMessagesTests { 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 SqlOutboxWritngMessagesTests() { _sqliteTestHelper = new SqliteTestHelper(); _sqliteTestHelper.SetupMessageDb(); - _sqlOutboxSync = new SqliteOutboxSync(new SqliteConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableName_Messages)); + _sqlOutbox = new SqliteOutbox(new SqliteConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableNameMessages)); _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,11 +56,11 @@ public SqlOutboxWritngMessagesTests() [Fact] public void When_Writing_Messages_To_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(); //should read first message last from the outbox @@ -74,9 +74,9 @@ 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 {_messageEarliest, _message2, _messageLatest}); - _retrievedMessages = _sqlOutboxSync.Get(); + _retrievedMessages = _sqlOutbox.Get(); //should read first message last from the outbox _retrievedMessages.Last().Id.Should().Be(_messageEarliest.Id); 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..c39c073ada 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,7 +37,7 @@ namespace Paramore.Brighter.Sqlite.Tests.Outbox public class SqlOutboxWritngMessagesAsyncTests : IDisposable { private readonly SqliteTestHelper _sqliteTestHelper; - private readonly SqliteOutboxSync _sSqlOutboxSync; + private readonly SqliteOutbox _sSqlOutbox; private Message _message2; private Message _messageEarliest; private Message _messageLatest; @@ -47,7 +47,7 @@ public SqlOutboxWritngMessagesAsyncTests() { _sqliteTestHelper = new SqliteTestHelper(); _sqliteTestHelper.SetupMessageDb(); - _sSqlOutboxSync = new SqliteOutboxSync(new SqliteConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableName_Messages)); + _sSqlOutbox = new SqliteOutbox(new SqliteConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableNameMessages)); } [Fact] @@ -55,7 +55,7 @@ public async Task When_Writing_Messages_To_The_Outbox_Async() { await SetUpMessagesAsync(); - _retrievedMessages = await _sSqlOutboxSync.GetAsync(); + _retrievedMessages = await _sSqlOutbox.GetAsync(); //should read first message last from the outbox _retrievedMessages.Last().Id.Should().Be(_messageEarliest.Id); @@ -69,9 +69,9 @@ 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 _sSqlOutbox.AddAsync(messages); - _retrievedMessages = await _sSqlOutboxSync.GetAsync(); + _retrievedMessages = await _sSqlOutbox.GetAsync(); //should read first message last from the outbox _retrievedMessages.Last().Id.Should().Be(_messageEarliest.Id); @@ -86,13 +86,13 @@ public async Task When_Writing_Messages_To_The_Outbox_Async_Bulk() 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); + if(addMessagesToOutbox) await _sSqlOutbox.AddAsync(_messageEarliest); _message2 = new Message(new MessageHeader(Guid.NewGuid(), "Test2", MessageType.MT_COMMAND, DateTime.UtcNow.AddHours(-2)), new MessageBody("Body2")); - if(addMessagesToOutbox) await _sSqlOutboxSync.AddAsync(_message2); + if(addMessagesToOutbox) await _sSqlOutbox.AddAsync(_message2); _messageLatest = new Message(new MessageHeader(Guid.NewGuid(), "Test3", MessageType.MT_COMMAND, DateTime.UtcNow.AddHours(-1)), new MessageBody("Body3")); - if(addMessagesToOutbox) await _sSqlOutboxSync.AddAsync(_messageLatest); + if(addMessagesToOutbox) await _sSqlOutbox.AddAsync(_messageLatest); return new List { _messageEarliest, _message2, _messageLatest }; } 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..3a319c9686 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,13 +18,13 @@ 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(new SqliteConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableNameMessages)); _message = new Message(new MessageHeader(Guid.NewGuid(), _Topic1, MessageType.MT_COMMAND), new MessageBody("message body")); diff --git a/tests/Paramore.Brighter.Sqlite.Tests/SqliteTestHelper.cs b/tests/Paramore.Brighter.Sqlite.Tests/SqliteTestHelper.cs index 7f86f9ec56..0f72a9022e 100644 --- a/tests/Paramore.Brighter.Sqlite.Tests/SqliteTestHelper.cs +++ b/tests/Paramore.Brighter.Sqlite.Tests/SqliteTestHelper.cs @@ -11,10 +11,10 @@ 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 TableName = "test_commands"; + public readonly string TableNameMessages = "test_messages"; + private string _connectionStringPath; + private string _connectionStringPathDir; public SqliteTestHelper(bool binaryMessagePayload = false) { @@ -23,33 +23,33 @@ public SqliteTestHelper(bool binaryMessagePayload = false) public void SetupCommandDb() { - connectionStringPath = GetUniqueTestDbPathAndCreateDir(); - ConnectionString = $"DataSource=\"{connectionStringPath}\""; + _connectionStringPath = GetUniqueTestDbPathAndCreateDir(); + ConnectionString = $"DataSource=\"{_connectionStringPath}\""; CreateDatabaseWithTable(ConnectionString, SqliteInboxBuilder.GetDDL(TableName)); } public void SetupMessageDb() { - connectionStringPath = GetUniqueTestDbPathAndCreateDir(); - ConnectionString = $"DataSource=\"{connectionStringPath}\""; - CreateDatabaseWithTable(ConnectionString, SqliteOutboxBuilder.GetDDL(TableName_Messages, hasBinaryMessagePayload: _binaryMessagePayload)); + _connectionStringPath = GetUniqueTestDbPathAndCreateDir(); + ConnectionString = $"DataSource=\"{_connectionStringPath}\""; + CreateDatabaseWithTable(ConnectionString, SqliteOutboxBuilder.GetDDL(TableNameMessages, 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 6f4bf2b263b65f3474290bd3ef42c12241b2b442 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Tue, 4 Apr 2023 08:37:19 +0100 Subject: [PATCH 10/89] Ensure bytes are written raw, not converted to a string, which is lossy --- src/Paramore.Brighter.Outbox.MySql/MySqlOutboxSync.cs | 2 +- .../When_writing_a_message_to_a_binary_body_outbox.cs | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Paramore.Brighter.Outbox.MySql/MySqlOutboxSync.cs b/src/Paramore.Brighter.Outbox.MySql/MySqlOutboxSync.cs index d36f2485d9..18ca15d6af 100644 --- a/src/Paramore.Brighter.Outbox.MySql/MySqlOutboxSync.cs +++ b/src/Paramore.Brighter.Outbox.MySql/MySqlOutboxSync.cs @@ -257,7 +257,7 @@ protected override MySqlParameter[] InitAddDbParameters(Message message, int? po _configuration.BinaryMessagePayload ? new MySqlParameter { - ParameterName = $"@{prefix}Body", DbType = DbType.Binary, Value = message.Body.Value + ParameterName = $"@{prefix}Body", DbType = DbType.Binary, Value = message.Body.Bytes } : new MySqlParameter { 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 index 5aceefec00..b8954294f5 100644 --- 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 @@ -43,7 +43,10 @@ public MySqlOutboxWritingBinaryMessageTests() messageHeader.Bag.Add(_key4, _value4); messageHeader.Bag.Add(_key5, _value5); - _messageEarliest = new Message(messageHeader, new MessageBody(new byte[] { 1, 2, 3, 4, 5 })); + _messageEarliest = new Message( + messageHeader, + new MessageBody(new byte[] { 1, 2, 3, 4, 5 }, "application/octet-stream", CharacterEncoding.Raw ) + ); _mySqlOutbox.Add(_messageEarliest); } @@ -53,7 +56,8 @@ 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.Value.Should().Be(_messageEarliest.Body.Value); + _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); From d6a892d7c60360b29ea7dc65aa63cdd9afa6f846 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Wed, 5 Apr 2023 17:49:58 +0100 Subject: [PATCH 11/89] Run SQL Server tests on GA --- .../GreetingsSender.Web/Program.cs | 2 +- .../ASBTaskQueue/GreetingsWorker/Program.cs | 2 +- .../Orders.API/Program.cs | 2 +- .../Extensions/BrighterExtensions.cs | 2 +- .../Orders.Worker/Program.cs | 2 +- .../MsSqlConfiguration.cs | 33 ++---- .../SqlOutboxBuilder.cs | 31 +++++- .../MySqlOutboxBuilder.cs | 3 +- .../MsSqlTestHelper.cs | 48 +++++--- ..._message_to_a_binary_body_message_store.cs | 104 ++++++++++++++++++ 10 files changed, 184 insertions(+), 45 deletions(-) create mode 100644 tests/Paramore.Brighter.MSSQL.Tests/Outbox/When_writing_a_message_to_a_binary_body_message_store.cs diff --git a/samples/ASBTaskQueue/GreetingsSender.Web/Program.cs b/samples/ASBTaskQueue/GreetingsSender.Web/Program.cs index 8363e6b58c..ee57d2da39 100644 --- a/samples/ASBTaskQueue/GreetingsSender.Web/Program.cs +++ b/samples/ASBTaskQueue/GreetingsSender.Web/Program.cs @@ -36,7 +36,7 @@ var asbConnection = new ServiceBusVisualStudioCredentialClientProvider(asbEndpoint); -var outboxConfig = new MsSqlConfiguration(dbConnString, "BrighterOutbox"); +var outboxConfig = new MsSqlConfiguration(dbConnString, outBoxTableName: "BrighterOutbox"); builder.Services .AddBrighter(opt => diff --git a/samples/ASBTaskQueue/GreetingsWorker/Program.cs b/samples/ASBTaskQueue/GreetingsWorker/Program.cs index 0134887a1f..aa5c99d6cb 100644 --- a/samples/ASBTaskQueue/GreetingsWorker/Program.cs +++ b/samples/ASBTaskQueue/GreetingsWorker/Program.cs @@ -69,7 +69,7 @@ o.UseSqlServer(dbConnString); }); -var outboxConfig = new MsSqlConfiguration(dbConnString, "BrighterOutbox"); +var outboxConfig = new MsSqlConfiguration(dbConnString, outBoxTableName: "BrighterOutbox"); //TODO: add your ASB qualified name here var clientProvider = new ServiceBusVisualStudioCredentialClientProvider(".servicebus.windows.net"); diff --git a/samples/WebApiWithWorkerAndSweeper/Orders.API/Program.cs b/samples/WebApiWithWorkerAndSweeper/Orders.API/Program.cs index a264fb3554..7522f99c4e 100644 --- a/samples/WebApiWithWorkerAndSweeper/Orders.API/Program.cs +++ b/samples/WebApiWithWorkerAndSweeper/Orders.API/Program.cs @@ -31,7 +31,7 @@ var asbConnection = new ServiceBusVisualStudioCredentialClientProvider(asbEndpoint); -var outboxConfig = new MsSqlConfiguration(dbConnString, "BrighterOutbox"); +var outboxConfig = new MsSqlConfiguration(dbConnString, outBoxTableName: "BrighterOutbox"); builder.Services .AddBrighter(opt => diff --git a/samples/WebApiWithWorkerAndSweeper/Orders.Sweeper/Extensions/BrighterExtensions.cs b/samples/WebApiWithWorkerAndSweeper/Orders.Sweeper/Extensions/BrighterExtensions.cs index c068b38202..f1193eb697 100644 --- a/samples/WebApiWithWorkerAndSweeper/Orders.Sweeper/Extensions/BrighterExtensions.cs +++ b/samples/WebApiWithWorkerAndSweeper/Orders.Sweeper/Extensions/BrighterExtensions.cs @@ -32,7 +32,7 @@ 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); + var outboxSettings = new MsSqlConfiguration(boxSettings.ConnectionString, outBoxTableName: boxSettings.OutboxTableName); Type outboxType; if (boxSettings.UseMsi) diff --git a/samples/WebApiWithWorkerAndSweeper/Orders.Worker/Program.cs b/samples/WebApiWithWorkerAndSweeper/Orders.Worker/Program.cs index d546ebc93b..74ac4ce6f2 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 MsSqlConfiguration(dbConnString, outBoxTableName: "BrighterOutbox"); //TODO: add your ASB qualified name here var clientProvider = new ServiceBusVisualStudioCredentialClientProvider(".servicebus.windows.net"); diff --git a/src/Paramore.Brighter.MsSql/MsSqlConfiguration.cs b/src/Paramore.Brighter.MsSql/MsSqlConfiguration.cs index 596629d17c..7d9c14aa9a 100644 --- a/src/Paramore.Brighter.MsSql/MsSqlConfiguration.cs +++ b/src/Paramore.Brighter.MsSql/MsSqlConfiguration.cs @@ -1,44 +1,31 @@ namespace Paramore.Brighter.MsSql { - public class MsSqlConfiguration + public class MsSqlConfiguration : RelationalDatabaseOutboxConfiguration { /// /// 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. + /// 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) + public MsSqlConfiguration( + string connectionString, + string outBoxTableName = null, + string inboxTableName = null, + string queueStoreTable = null, + bool binaryMessagePayload = false) + : base(connectionString, outBoxTableName, queueStoreTable, binaryMessagePayload) { - 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.Outbox.MsSql/SqlOutboxBuilder.cs b/src/Paramore.Brighter.Outbox.MsSql/SqlOutboxBuilder.cs index d1ac1dc065..2d7e1f5ea6 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,7 +31,7 @@ namespace Paramore.Brighter.Outbox.MsSql /// public class SqlOutboxBuilder { - const string OutboxDdl = @" + const string TextOutboxDdl = @" CREATE TABLE {0} ( [Id] [BIGINT] NOT NULL IDENTITY , @@ -47,16 +49,39 @@ 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, + [Dispatched] DATETIME NULL, + [HeaderBag] NTEXT NULL , + [Body] VARBINARY(MAX) NULL , + PRIMARY KEY ( [Id] ) + ); + "; + + private const string OutboxExistsSQL = @"SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = N'{0}'"; /// /// 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/MySqlOutboxBuilder.cs b/src/Paramore.Brighter.Outbox.MySql/MySqlOutboxBuilder.cs index f4f10c4f18..b35a6e50e3 100644 --- a/src/Paramore.Brighter.Outbox.MySql/MySqlOutboxBuilder.cs +++ b/src/Paramore.Brighter.Outbox.MySql/MySqlOutboxBuilder.cs @@ -23,6 +23,7 @@ THE SOFTWARE. */ #endregion +using System; using System.ComponentModel; namespace Paramore.Brighter.Outbox.MySql @@ -77,7 +78,7 @@ PRIMARY KEY (`MessageId`) 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"); + throw new ArgumentNullException(outboxTableName, $"You must provide a tablename for the OutBox table"); return string.Format(hasBinaryMessagePayload ? BinaryOutboxDdl : TextOutboxDdl, outboxTableName); } diff --git a/tests/Paramore.Brighter.MSSQL.Tests/MsSqlTestHelper.cs b/tests/Paramore.Brighter.MSSQL.Tests/MsSqlTestHelper.cs index b7d4691e6a..ff5cfd7202 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 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,21 @@ [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 MsSqlTestHelper() + public MsSqlTestHelper(bool binaryMessagePayload = false) { + _binaryMessagePayload = binaryMessagePayload; var builder = new ConfigurationBuilder().AddEnvironmentVariables(); var configuration = builder.Build(); @@ -34,13 +47,15 @@ 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 MsSqlSqlAuthConnectionProvider(new MsSqlConfiguration(_sqlSettings.TestsBrighterConnectionString)); + _masterConnectionProvider = + new MsSqlSqlAuthConnectionProvider(new MsSqlConfiguration(_sqlSettings.TestsMasterConnectionString)); } - public void CreateDatabase() - { + public void CreateDatabase() + { using (var connection = _masterConnectionProvider.GetConnection()) { connection.Open(); @@ -74,17 +89,22 @@ public void SetupQueueDb() CreateQueueTable(); } - public MsSqlConfiguration InboxConfiguration => new MsSqlConfiguration(_sqlSettings.TestsBrighterConnectionString, inboxTableName: _tableName); + public MsSqlConfiguration InboxConfiguration => + new MsSqlConfiguration(_sqlSettings.TestsBrighterConnectionString, inboxTableName: _tableName); - public MsSqlConfiguration OutboxConfiguration => new MsSqlConfiguration(_sqlSettings.TestsBrighterConnectionString, outBoxTableName: _tableName); + public MsSqlConfiguration OutboxConfiguration => new MsSqlConfiguration( + _sqlSettings.TestsBrighterConnectionString, outBoxTableName: _tableName, + binaryMessagePayload: _binaryMessagePayload); + + public MsSqlConfiguration QueueConfiguration => + new MsSqlConfiguration(_sqlSettings.TestsBrighterConnectionString, queueStoreTable: _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()) @@ -92,8 +112,10 @@ private void CreateQueueTable() command.CommandText = createTableSql; command.ExecuteNonQuery(); } + connection.Close(); } + public void CleanUpDb() { using (var connection = _connectionProvider.GetConnection()) @@ -116,7 +138,7 @@ 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()) 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..f8714967a1 --- /dev/null +++ b/tests/Paramore.Brighter.MSSQL.Tests/Outbox/When_writing_a_message_to_a_binary_body_message_store.cs @@ -0,0 +1,104 @@ +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"); + _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, 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.Should().Be(_message.Header.TimeStamp); + _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); + + + //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(); + } + } +} From ff513c48a70d6689fb54f161829cec9e17e48fe0 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Thu, 6 Apr 2023 10:06:34 +0100 Subject: [PATCH 12/89] Binary support for MSSQL; some naming clean up for Outboxes. Ensure that we encode --- .github/workflows/ci.yml | 2 +- docker-compose-mssql.yaml | 6 +- .../MsSqlOutbox.cs | 74 ++++++++++++++----- .../{MySqlOutboxSync.cs => MySqlOutbox.cs} | 7 +- ...reSqlOutboxSync.cs => PostgreSqlOutbox.cs} | 8 +- .../ServiceCollectionExtensions.cs | 4 +- .../{SqliteOutboxSync.cs => SqliteOutbox.cs} | 9 +-- .../MsSqlTestHelper.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 | 12 +-- ...n_There_Is_No_Message_In_The_Sql_Outbox.cs | 6 +- .../When_Writing_A_Message_To_The_Outbox.cs | 8 +- .../When_Writing_Messages_To_The_Outbox.cs | 16 ++-- 14 files changed, 111 insertions(+), 73 deletions(-) rename src/Paramore.Brighter.Outbox.MySql/{MySqlOutboxSync.cs => MySqlOutbox.cs} (98%) rename src/Paramore.Brighter.Outbox.PostgreSql/{PostgreSqlOutboxSync.cs => PostgreSqlOutbox.cs} (99%) rename src/Paramore.Brighter.Outbox.Sqlite/{SqliteOutboxSync.cs => SqliteOutbox.cs} (98%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b7e4347841..391d41c524 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@v3 - name: Setup dotnet 6 diff --git a/docker-compose-mssql.yaml b/docker-compose-mssql.yaml index 25fe426b3f..808a5b59f3 100644 --- a/docker-compose-mssql.yaml +++ b/docker-compose-mssql.yaml @@ -6,12 +6,12 @@ services: sqlserver: image: mcr.microsoft.com/mssql/server ports: - - "1433:1433" + - "11433:1433" volumes: - sqlserver-data:/var/opt/mssql environment: - - ACCEPT_EULA=Y - - SA_PASSWORD=Password123! + - 'ACCEPT_EULA=Y' + - 'SA_PASSWORD=Password123!' volumes: sqlserver-data: diff --git a/src/Paramore.Brighter.Outbox.MsSql/MsSqlOutbox.cs b/src/Paramore.Brighter.Outbox.MsSql/MsSqlOutbox.cs index 686ddc6f6c..80f883da9c 100644 --- a/src/Paramore.Brighter.Outbox.MsSql/MsSqlOutbox.cs +++ b/src/Paramore.Brighter.Outbox.MsSql/MsSqlOutbox.cs @@ -202,9 +202,9 @@ protected override SqlCommand CreateCommand(SqlConnection connection, string sql protected override SqlParameter[] CreatePagedOutstandingParameters(double milliSecondsSinceAdded, int pageSize, int pageNumber) { var parameters = new SqlParameter[3]; - parameters[0] = CreateSqlParameter("PageNumber", pageNumber); - parameters[1] = CreateSqlParameter("PageSize", pageSize); - parameters[2] = CreateSqlParameter("OutstandingSince", milliSecondsSinceAdded); + parameters[0] = new SqlParameter{ParameterName ="PageNumber", Value=(object)pageNumber ?? DBNull.Value}; + parameters[1] = new SqlParameter{ParameterName ="PageSize", Value = (object)pageSize ?? DBNull.Value}; + parameters[2] = new SqlParameter{ParameterName ="OutstandingSince", Value = (object)milliSecondsSinceAdded ?? DBNull.Value}; return parameters; } @@ -213,7 +213,7 @@ protected override SqlParameter[] CreatePagedOutstandingParameters(double milliS protected override SqlParameter CreateSqlParameter(string parameterName, object value) { - return new SqlParameter(parameterName, value ?? DBNull.Value); + return new SqlParameter{ ParameterName = parameterName, Value = value ?? DBNull.Value}; } protected override SqlParameter[] InitAddDbParameters(Message message, int? position = null) @@ -222,15 +222,46 @@ protected override SqlParameter[] InitAddDbParameters(Message message, int? posi var bagJson = JsonSerializer.Serialize(message.Header.Bag, JsonSerialisationOptions.Options); return new[] { - CreateSqlParameter($"{prefix}MessageId", message.Id), - CreateSqlParameter($"{prefix}MessageType", message.Header.MessageType.ToString()), - CreateSqlParameter($"{prefix}Topic", message.Header.Topic), - CreateSqlParameter($"{prefix}Timestamp", message.Header.TimeStamp.ToUniversalTime()), //always store in UTC, as this is how we query messages - CreateSqlParameter($"{prefix}CorrelationId", message.Header.CorrelationId), - CreateSqlParameter($"{prefix}ReplyTo", message.Header.ReplyTo), - CreateSqlParameter($"{prefix}ContentType", message.Header.ContentType), - CreateSqlParameter($"{prefix}HeaderBag", bagJson), - CreateSqlParameter($"{prefix}Body", message.Body?.Value) + new SqlParameter + { + ParameterName= $"{prefix}MessageId", Value = (object)message.Id ?? DBNull.Value + }, + new SqlParameter + { + ParameterName=$"{prefix}MessageType", Value = (object)message.Header.MessageType.ToString() ?? DBNull.Value + }, + new SqlParameter + { + ParameterName = $"{prefix}Topic", Value = (object)message.Header.Topic ?? DBNull.Value + }, + new SqlParameter{ParameterName =$"{prefix}Timestamp", Value = (object)message.Header.TimeStamp.ToUniversalTime() ?? DBNull.Value}, //always store in UTC, as this is how we query messages + new SqlParameter + { + ParameterName = $"{prefix}CorrelationId", Value = (object)message.Header.CorrelationId ?? DBNull.Value + }, + new SqlParameter + { + ParameterName = $"{prefix}ReplyTo", Value = (object)message.Header.ReplyTo ?? DBNull.Value + }, + new SqlParameter + { + ParameterName = $"{prefix}ContentType", Value = (object)message.Header.ContentType ?? DBNull.Value + }, + new SqlParameter + { + ParameterName = $"{prefix}HeaderBag", Value = (object)bagJson ?? DBNull.Value + }, + _configuration.BinaryMessagePayload + ? new SqlParameter + { + ParameterName = $"{prefix}Body", + Value = (object)message.Body?.Bytes ?? DBNull.Value + } + : new SqlParameter + { + ParameterName = $"{prefix}Body", + Value = (object)message.Body?.Value ?? DBNull.Value + } }; } @@ -379,11 +410,20 @@ private Message MapAMessage(SqlDataReader dr) 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(dr.GetString(dr.GetOrdinal("Body")), "application/json", CharacterEncoding.UTF8); return new Message(header, body); } + + private byte[] GetBodyAsBytes(SqlDataReader dr) + { + var i = dr.GetOrdinal("Body"); + var body = dr.GetStream(i); + long bodyLength = body.Length; + var buffer = new byte[bodyLength]; + body.Read(buffer,0, (int)bodyLength); + return buffer; + } } } diff --git a/src/Paramore.Brighter.Outbox.MySql/MySqlOutboxSync.cs b/src/Paramore.Brighter.Outbox.MySql/MySqlOutbox.cs similarity index 98% rename from src/Paramore.Brighter.Outbox.MySql/MySqlOutboxSync.cs rename to src/Paramore.Brighter.Outbox.MySql/MySqlOutbox.cs index 18ca15d6af..00cdae4552 100644 --- a/src/Paramore.Brighter.Outbox.MySql/MySqlOutboxSync.cs +++ b/src/Paramore.Brighter.Outbox.MySql/MySqlOutbox.cs @@ -39,8 +39,7 @@ namespace Paramore.Brighter.Outbox.MySql /// /// Class MySqlOutbox. /// - public class - MySqlOutbox : RelationDatabaseOutbox + public class MySqlOutbox : RelationDatabaseOutbox { private static readonly ILogger s_logger = ApplicationLogging.CreateLogger(); @@ -367,8 +366,8 @@ private Message MapAMessage(IDataReader dr) } var body = _configuration.BinaryMessagePayload - ? new MessageBody(GetBodyAsBytes((MySqlDataReader)dr)) - : new MessageBody(dr.GetString(dr.GetOrdinal("Body"))); + ? new MessageBody(GetBodyAsBytes((MySqlDataReader)dr), "application/octet-stream", CharacterEncoding.Raw) + : new MessageBody(dr.GetString(dr.GetOrdinal("Body")), "application/json", CharacterEncoding.UTF8); return new Message(header, body); } diff --git a/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutboxSync.cs b/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutbox.cs similarity index 99% rename from src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutboxSync.cs rename to src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutbox.cs index c4d505ba1d..6d143bdfce 100644 --- a/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutboxSync.cs +++ b/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutbox.cs @@ -36,11 +36,11 @@ THE SOFTWARE. */ namespace Paramore.Brighter.Outbox.PostgreSql { - public class PostgreSqlOutboxSync : IAmABulkOutboxSync + public class PostgreSqlOutbox : IAmABulkOutboxSync { private readonly PostgreSqlOutboxConfiguration _configuration; private readonly IPostgreSqlConnectionProvider _connectionProvider; - private static readonly ILogger s_logger = ApplicationLogging.CreateLogger(); + private static readonly ILogger s_logger = ApplicationLogging.CreateLogger(); private const string _deleteMessageCommand = "DELETE FROM {0} WHERE MessageId IN ({1})"; @@ -53,10 +53,10 @@ public bool ContinueOnCapturedContext } /// - /// Initialises a new instance of class. + /// Initialises a new instance of class. /// /// PostgreSql Outbox Configuration. - public PostgreSqlOutboxSync(PostgreSqlOutboxConfiguration configuration, IPostgreSqlConnectionProvider connectionProvider = null) + public PostgreSqlOutbox(PostgreSqlOutboxConfiguration configuration, IPostgreSqlConnectionProvider connectionProvider = null) { _configuration = configuration; _connectionProvider = connectionProvider ?? new PostgreSqlNpgsqlConnectionProvider(configuration); diff --git a/src/Paramore.Brighter.Outbox.PostgreSql/ServiceCollectionExtensions.cs b/src/Paramore.Brighter.Outbox.PostgreSql/ServiceCollectionExtensions.cs index 16637e5e10..d44e8f250d 100644 --- a/src/Paramore.Brighter.Outbox.PostgreSql/ServiceCollectionExtensions.cs +++ b/src/Paramore.Brighter.Outbox.PostgreSql/ServiceCollectionExtensions.cs @@ -59,12 +59,12 @@ public static IBrighterBuilder UsePostgreSqlTransactionConnectionProvider( return brighterBuilder; } - private static PostgreSqlOutboxSync BuildPostgreSqlOutboxSync(IServiceProvider provider) + private static PostgreSqlOutbox BuildPostgreSqlOutboxSync(IServiceProvider provider) { var config = provider.GetService(); var connectionProvider = provider.GetService(); - return new PostgreSqlOutboxSync(config, connectionProvider); + return new PostgreSqlOutbox(config, connectionProvider); } } } diff --git a/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutboxSync.cs b/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutbox.cs similarity index 98% rename from src/Paramore.Brighter.Outbox.Sqlite/SqliteOutboxSync.cs rename to src/Paramore.Brighter.Outbox.Sqlite/SqliteOutbox.cs index e8c203495c..01e0078ba2 100644 --- a/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutboxSync.cs +++ b/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutbox.cs @@ -26,8 +26,6 @@ THE SOFTWARE. */ using System; using System.Collections.Generic; using System.Data; -using System.IO; -using System.Linq; using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -355,9 +353,10 @@ private Message MapAMessage(IDataReader dr) } } - var body = _configuration.BinaryMessagePayload - ? new MessageBody(GetBodyAsBytes((SqliteDataReader)dr)) - : 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); } diff --git a/tests/Paramore.Brighter.MSSQL.Tests/MsSqlTestHelper.cs b/tests/Paramore.Brighter.MSSQL.Tests/MsSqlTestHelper.cs index ff5cfd7202..2d2206346c 100644 --- a/tests/Paramore.Brighter.MSSQL.Tests/MsSqlTestHelper.cs +++ b/tests/Paramore.Brighter.MSSQL.Tests/MsSqlTestHelper.cs @@ -169,9 +169,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.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..9edecb624d 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.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,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..265616e1e6 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.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.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..0d03a1c2a7 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 @@ -40,28 +40,28 @@ public class PostgreSqlOutboxRangeRequestTests : IDisposable private readonly string _TopicFirstMessage = "test_topic"; 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); + _sqlOutbox = new PostgreSqlOutbox(_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.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.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..1fb4dcb276 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.OutboxConfiguration); _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_The_Outbox.cs b/tests/Paramore.Brighter.PostgresSQL.Tests/Outbox/When_Writing_A_Message_To_The_Outbox.cs index ae86b66ebb..057be6df6c 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.OutboxConfiguration); var messageHeader = new MessageHeader( messageId:Guid.NewGuid(), topic: "test_topic", @@ -71,13 +71,13 @@ 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); 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..3ab12ff56f 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 @@ -40,14 +40,14 @@ public class SqlOutboxWritngMessagesTests : IDisposable private readonly Message _message2; private readonly Message _messageLatest; private IEnumerable _retrievedMessages; - private readonly PostgreSqlOutboxSync _sqlOutboxSync; + private readonly PostgreSqlOutbox _sqlOutbox; public SqlOutboxWritngMessagesTests() { _postgresSqlTestHelper = new PostgresSqlTestHelper(); _postgresSqlTestHelper.SetupMessageDb(); - _sqlOutboxSync = new PostgreSqlOutboxSync(_postgresSqlTestHelper.OutboxConfiguration); + _sqlOutbox = new PostgreSqlOutbox(_postgresSqlTestHelper.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,11 +59,11 @@ public SqlOutboxWritngMessagesTests() [Fact] public void When_Writing_Messages_To_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(); //should read first message last from the outbox _retrievedMessages.Last().Id.Should().Be(_messageEarliest.Id); @@ -76,8 +76,8 @@ 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{_messageEarliest, _message2, _messageLatest}); + _retrievedMessages = _sqlOutbox.Get(); //should read first message last from the outbox _retrievedMessages.Last().Id.Should().Be(_messageEarliest.Id); From d963a14e0c77881da59b2abe4f545867a45e70c3 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Thu, 6 Apr 2023 11:31:50 +0100 Subject: [PATCH 13/89] remove obsolete sql data types --- src/Paramore.Brighter.Inbox.MsSql/SqlInboxBuilder.cs | 6 +++--- src/Paramore.Brighter.Outbox.MsSql/SqlOutboxBuilder.cs | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Paramore.Brighter.Inbox.MsSql/SqlInboxBuilder.cs b/src/Paramore.Brighter.Inbox.MsSql/SqlInboxBuilder.cs index e1d7fea8e8..4e4343dc06 100644 --- a/src/Paramore.Brighter.Inbox.MsSql/SqlInboxBuilder.cs +++ b/src/Paramore.Brighter.Inbox.MsSql/SqlInboxBuilder.cs @@ -29,13 +29,13 @@ 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] ) @@ -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.Outbox.MsSql/SqlOutboxBuilder.cs b/src/Paramore.Brighter.Outbox.MsSql/SqlOutboxBuilder.cs index 2d7e1f5ea6..8eaf7489ce 100644 --- a/src/Paramore.Brighter.Outbox.MsSql/SqlOutboxBuilder.cs +++ b/src/Paramore.Brighter.Outbox.MsSql/SqlOutboxBuilder.cs @@ -43,8 +43,8 @@ [MessageType] NVARCHAR(32) NULL , [ReplyTo] NVARCHAR(255) NULL, [ContentType] NVARCHAR(128) NULL, [Dispatched] DATETIME NULL, - [HeaderBag] NTEXT NULL , - [Body] NTEXT NULL , + [HeaderBag] NVARCHAR(MAX) NULL , + [Body] NVARCHAR(MAX) NULL , PRIMARY KEY ( [Id] ) ); "; @@ -61,7 +61,7 @@ [MessageType] NVARCHAR(32) NULL , [ReplyTo] NVARCHAR(255) NULL, [ContentType] NVARCHAR(128) NULL, [Dispatched] DATETIME NULL, - [HeaderBag] NTEXT NULL , + [HeaderBag] NVARCHAR(MAX) NULL , [Body] VARBINARY(MAX) NULL , PRIMARY KEY ( [Id] ) ); From 3a9ebd74ea072849eb905d4731175bd3c28fc79d Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Tue, 11 Apr 2023 09:52:20 +0100 Subject: [PATCH 14/89] Fix missing partition key header --- .../MsSqlOutbox.cs | 83 +++++++++++-------- .../MsSqlQueries.cs | 4 +- .../SqlOutboxBuilder.cs | 28 ++++--- .../RelationDatabaseOutbox.cs | 2 +- ..._message_to_a_binary_body_message_store.cs | 4 +- 5 files changed, 68 insertions(+), 53 deletions(-) diff --git a/src/Paramore.Brighter.Outbox.MsSql/MsSqlOutbox.cs b/src/Paramore.Brighter.Outbox.MsSql/MsSqlOutbox.cs index 80f883da9c..30390ab8ef 100644 --- a/src/Paramore.Brighter.Outbox.MsSql/MsSqlOutbox.cs +++ b/src/Paramore.Brighter.Outbox.MsSql/MsSqlOutbox.cs @@ -248,6 +248,10 @@ protected override SqlParameter[] InitAddDbParameters(Message message, int? posi ParameterName = $"{prefix}ContentType", Value = (object)message.Header.ContentType ?? DBNull.Value }, new SqlParameter + { + ParameterName = $"{prefix}PartitionKey", Value = (object)message.Header.PartitionKey ?? DBNull.Value + }, + new SqlParameter { ParameterName = $"{prefix}HeaderBag", Value = (object)bagJson ?? DBNull.Value }, @@ -280,8 +284,8 @@ private string GetContentType(SqlDataReader dr) var ordinal = dr.GetOrdinal("ContentType"); if (dr.IsDBNull(ordinal)) return null; - var replyTo = dr.GetString(ordinal); - return replyTo; + var contentType = dr.GetString(ordinal); + return contentType; } private string GetReplyTo(SqlDataReader dr) @@ -318,6 +322,25 @@ private static DateTime GetTimeStamp(SqlDataReader dr) : dr.GetDateTime(ordinal); return timeStamp; } + + private string GetPartitionKey(SqlDataReader dr) + { + var ordinal = dr.GetOrdinal("PartitionKey"); + if (dr.IsDBNull(ordinal)) return null; + + var partitionKey = dr.GetString(ordinal); + return partitionKey; + } + + private byte[] GetBodyAsBytes(SqlDataReader dr) + { + var i = dr.GetOrdinal("Body"); + var body = dr.GetStream(i); + long bodyLength = body.Length; + var buffer = new byte[bodyLength]; + body.Read(buffer,0, (int)bodyLength); + return buffer; + } #endregion @@ -379,32 +402,30 @@ 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]); } } @@ -415,15 +436,5 @@ private Message MapAMessage(SqlDataReader dr) : new MessageBody(dr.GetString(dr.GetOrdinal("Body")), "application/json", CharacterEncoding.UTF8); return new Message(header, body); } - - private byte[] GetBodyAsBytes(SqlDataReader dr) - { - var i = dr.GetOrdinal("Body"); - var body = dr.GetStream(i); - long bodyLength = body.Length; - var buffer = new byte[bodyLength]; - body.Read(buffer,0, (int)bodyLength); - return buffer; - } } } diff --git a/src/Paramore.Brighter.Outbox.MsSql/MsSqlQueries.cs b/src/Paramore.Brighter.Outbox.MsSql/MsSqlQueries.cs index 4951d7176d..5da2d389fd 100644 --- a/src/Paramore.Brighter.Outbox.MsSql/MsSqlQueries.cs +++ b/src/Paramore.Brighter.Outbox.MsSql/MsSqlQueries.cs @@ -5,8 +5,8 @@ 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"; diff --git a/src/Paramore.Brighter.Outbox.MsSql/SqlOutboxBuilder.cs b/src/Paramore.Brighter.Outbox.MsSql/SqlOutboxBuilder.cs index 8eaf7489ce..5e189e7896 100644 --- a/src/Paramore.Brighter.Outbox.MsSql/SqlOutboxBuilder.cs +++ b/src/Paramore.Brighter.Outbox.MsSql/SqlOutboxBuilder.cs @@ -35,16 +35,17 @@ public class SqlOutboxBuilder 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 , + [HeaderBag] NVARCHAR(MAX) NULL, + [Body] NVARCHAR(MAX) NULL, PRIMARY KEY ( [Id] ) ); "; @@ -52,17 +53,18 @@ 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 , + [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] NVARCHAR(MAX) NULL , - [Body] VARBINARY(MAX) NULL , + [HeaderBag] NVARCHAR(MAX) NULL, + [Body] VARBINARY(MAX) NULL, PRIMARY KEY ( [Id] ) ); "; diff --git a/src/Paramore.Brighter/RelationDatabaseOutbox.cs b/src/Paramore.Brighter/RelationDatabaseOutbox.cs index 2d24a60330..3bec3befea 100644 --- a/src/Paramore.Brighter/RelationDatabaseOutbox.cs +++ b/src/Paramore.Brighter/RelationDatabaseOutbox.cs @@ -448,7 +448,7 @@ protected abstract Task> MapListFunctionAsync(TDataReader d 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/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 index f8714967a1..63c4b22515 100644 --- 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 @@ -39,7 +39,8 @@ public SqlBinaryPayloadOutboxWritingMessageTests() delayedMilliseconds: 5, correlationId: Guid.NewGuid(), replyTo: "ReplyAddress", - contentType: "application/octet-stream"); + contentType: "application/octet-stream", + partitionKey: "123456789"); _messageHeader.Bag.Add(_key1, _value1); _messageHeader.Bag.Add(_key2, _value2); _messageHeader.Bag.Add(_key3, _value3); @@ -81,6 +82,7 @@ private void AssertMessage() _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 From cdae9693d3f85306199496c77bf8c0eb47cfb98a Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Tue, 11 Apr 2023 10:36:00 +0100 Subject: [PATCH 15/89] Sqlite should support partition key --- .../MsSqlOutbox.cs | 142 +++++++++++------- .../SqliteOutbox.cs | 119 +++++++++++---- .../SqliteOutboxBuilder.cs | 2 + .../SqliteQueries.cs | 4 +- ...iting_A_Message_To_A_Binary_Body_Outbox.cs | 4 +- 5 files changed, 178 insertions(+), 93 deletions(-) diff --git a/src/Paramore.Brighter.Outbox.MsSql/MsSqlOutbox.cs b/src/Paramore.Brighter.Outbox.MsSql/MsSqlOutbox.cs index 30390ab8ef..dbdb8c3549 100644 --- a/src/Paramore.Brighter.Outbox.MsSql/MsSqlOutbox.cs +++ b/src/Paramore.Brighter.Outbox.MsSql/MsSqlOutbox.cs @@ -46,7 +46,7 @@ public class MsSqlOutbox : private const int MsSqlDuplicateKeyError_UniqueConstraintViolation = 2627; private readonly MsSqlConfiguration _configuration; private readonly IMsSqlConnectionProvider _connectionProvider; - + /// /// Initializes a new instance of the class. /// @@ -64,12 +64,17 @@ public MsSqlOutbox(MsSqlConfiguration configuration, IMsSqlConnectionProvider co /// Initializes a new instance of the class. /// /// The configuration. - public MsSqlOutbox(MsSqlConfiguration configuration) : this(configuration, new MsSqlSqlAuthConnectionProvider(configuration)) { } + public MsSqlOutbox(MsSqlConfiguration configuration) : this(configuration, + new MsSqlSqlAuthConnectionProvider(configuration)) + { + } - protected override void WriteToStore(IAmABoxTransactionConnectionProvider transactionConnectionProvider, Func commandFunc, Action loggingAction) + protected override void WriteToStore(IAmABoxTransactionConnectionProvider transactionConnectionProvider, + Func commandFunc, Action loggingAction) { var connectionProvider = _connectionProvider; - if (transactionConnectionProvider != null && transactionConnectionProvider is IMsSqlTransactionConnectionProvider provider) + if (transactionConnectionProvider != null && + transactionConnectionProvider is IMsSqlTransactionConnectionProvider provider) connectionProvider = provider; var connection = connectionProvider.GetConnection(); @@ -105,13 +110,17 @@ protected override void WriteToStore(IAmABoxTransactionConnectionProvider transa } } - protected override async Task WriteToStoreAsync(IAmABoxTransactionConnectionProvider transactionConnectionProvider, Func commandFunc, Action loggingAction, CancellationToken cancellationToken) + protected override async Task WriteToStoreAsync( + IAmABoxTransactionConnectionProvider transactionConnectionProvider, + Func commandFunc, Action loggingAction, CancellationToken cancellationToken) { var connectionProvider = _connectionProvider; - if (transactionConnectionProvider != null && transactionConnectionProvider is IMsSqlTransactionConnectionProvider provider) + if (transactionConnectionProvider != null && + transactionConnectionProvider is IMsSqlTransactionConnectionProvider provider) connectionProvider = provider; - 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); @@ -144,7 +153,8 @@ protected override async Task WriteToStoreAsync(IAmABoxTransactionConnectionProv } } - protected override T ReadFromStore(Func commandFunc, Func resultFunc) + protected override T ReadFromStore(Func commandFunc, + Func resultFunc) { var connection = _connectionProvider.GetConnection(); @@ -166,7 +176,8 @@ protected override T ReadFromStore(Func commandFun } } - 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); @@ -188,7 +199,8 @@ protected override async Task ReadFromStoreAsync(Func 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(SqlDataReader dr) => + (MessageType)Enum.Parse(typeof(MessageType), dr.GetString(dr.GetOrdinal("MessageType"))); private static Guid GetMessageId(SqlDataReader dr) => dr.GetGuid(dr.GetOrdinal("MessageId")); private string GetContentType(SqlDataReader dr) { var ordinal = dr.GetOrdinal("ContentType"); - if (dr.IsDBNull(ordinal)) return null; - + if (dr.IsDBNull(ordinal)) return null; + var contentType = dr.GetString(ordinal); return contentType; } private string GetReplyTo(SqlDataReader 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) { 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) { var ordinal = dr.GetOrdinal("CorrelationId"); - if (dr.IsDBNull(ordinal)) return null; - + if (dr.IsDBNull(ordinal)) return null; + var correlationId = dr.GetGuid(ordinal); return correlationId; } @@ -322,12 +341,12 @@ private static DateTime GetTimeStamp(SqlDataReader dr) : dr.GetDateTime(ordinal); return timeStamp; } - + private string GetPartitionKey(SqlDataReader dr) { var ordinal = dr.GetOrdinal("PartitionKey"); - if (dr.IsDBNull(ordinal)) return null; - + if (dr.IsDBNull(ordinal)) return null; + var partitionKey = dr.GetString(ordinal); return partitionKey; } @@ -338,13 +357,14 @@ private byte[] GetBodyAsBytes(SqlDataReader dr) var body = dr.GetStream(i); long bodyLength = body.Length; var buffer = new byte[bodyLength]; - body.Read(buffer,0, (int)bodyLength); + body.Read(buffer, 0, (int)bodyLength); return buffer; } #endregion #region DataReader Operators + protected override Message MapFunction(SqlDataReader dr) { Message message = null; @@ -352,11 +372,12 @@ protected override Message MapFunction(SqlDataReader dr) { message = MapAMessage(dr); } + dr.Close(); return message ?? new Message(); } - + protected override async Task MapFunctionAsync(SqlDataReader dr, CancellationToken cancellationToken) { Message message = null; @@ -364,6 +385,7 @@ protected override async Task MapFunctionAsync(SqlDataReader dr, Cancel { message = MapAMessage(dr); } + dr.Close(); return message ?? new Message(); @@ -376,22 +398,26 @@ protected override IEnumerable MapListFunction(SqlDataReader dr) { messages.Add(MapAMessage(dr)); } + dr.Close(); return messages; } - protected override async Task> MapListFunctionAsync(SqlDataReader dr, CancellationToken cancellationToken) + protected override async Task> MapListFunctionAsync(SqlDataReader 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) @@ -414,7 +440,7 @@ private Message MapAMessage(SqlDataReader dr) messageType: messageType, timeStamp: timeStamp, handledCount: 0, - delayedMilliseconds: 0, + delayedMilliseconds: 0, correlationId: correlationId, replyTo: replyTo, contentType: contentType, diff --git a/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutbox.cs b/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutbox.cs index 01e0078ba2..2c6d39a779 100644 --- a/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutbox.cs +++ b/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutbox.cs @@ -234,26 +234,65 @@ protected override SqliteParameter[] InitAddDbParameters(Message message, int? p var bagJson = JsonSerializer.Serialize(message.Header.Bag, JsonSerialisationOptions.Options); return new[] { - new SqliteParameter($"@{prefix}MessageId", SqliteType.Text) { Value = message.Id.ToString() }, - new SqliteParameter($"@{prefix}MessageType", SqliteType.Text) + new SqliteParameter { + ParameterName = $"@{prefix}MessageId", + SqliteType = SqliteType.Text, + Value = message.Id.ToString() + }, + new SqliteParameter + { + ParameterName = $"@{prefix}MessageType", + SqliteType = SqliteType.Text, Value = message.Header.MessageType.ToString() }, - new SqliteParameter($"@{prefix}Topic", SqliteType.Text) { Value = message.Header.Topic }, - new SqliteParameter($"@{prefix}Timestamp", SqliteType.Text) + new SqliteParameter + { + ParameterName = $"@{prefix}Topic", SqliteType = SqliteType.Text, Value = message.Header.Topic + }, + new SqliteParameter { + ParameterName = $"@{prefix}Timestamp", + SqliteType = SqliteType.Text, Value = message.Header.TimeStamp.ToString("s") }, - new SqliteParameter($"@{prefix}CorrelationId", SqliteType.Text) + new SqliteParameter { + ParameterName = $"@{prefix}CorrelationId", + SqliteType = SqliteType.Text, Value = message.Header.CorrelationId }, - new SqliteParameter($"@{prefix}ReplyTo", SqliteType.Text) {Value = message.Header.ReplyTo}, - new SqliteParameter($"@{prefix}ContentType", SqliteType.Text) {Value = message.Header.ContentType}, - new SqliteParameter($"@{prefix}HeaderBag", SqliteType.Text) { Value = bagJson }, + new SqliteParameter + { + ParameterName = $"@{prefix}ReplyTo", + SqliteType = SqliteType.Text, + Value = message.Header.ReplyTo + }, + new SqliteParameter + { + ParameterName = $"@{prefix}ContentType", + SqliteType = SqliteType.Text, + Value = message.Header.ContentType + }, + new SqliteParameter + { + ParameterName = $"@{prefix}PartitionKey", + SqliteType = SqliteType.Text, + Value = message.Header.PartitionKey + }, + new SqliteParameter + { + ParameterName = $"@{prefix}HeaderBag", SqliteType = SqliteType.Text, Value = bagJson + }, _configuration.BinaryMessagePayload - ? new SqliteParameter($"@{prefix}Body", SqliteType.Blob) { Value = message.Body.Bytes } - : new SqliteParameter($"@{prefix}Body", SqliteType.Text) { Value = message.Body.Value } + ? new SqliteParameter + { + ParameterName = $"@{prefix}Body", SqliteType = SqliteType.Blob, Value = message.Body.Bytes + } + : new SqliteParameter + { + ParameterName = $"@{prefix}Body", SqliteType = SqliteType.Text, Value = message.Body.Value + } }; } @@ -331,6 +370,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, @@ -341,7 +381,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) @@ -354,13 +395,15 @@ private Message MapAMessage(IDataReader dr) } var body = _configuration.BinaryMessagePayload - ? new MessageBody(GetBodyAsBytes((SqliteDataReader)dr), "application/octet-stream", CharacterEncoding.Raw) + ? 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 byte[] GetBodyAsBytes(SqliteDataReader dr) { var i = dr.GetOrdinal("Body"); @@ -370,9 +413,32 @@ private byte[] GetBodyAsBytes(SqliteDataReader dr) return buffer; } - private static string GetTopic(IDataReader dr) + private static Dictionary GetContextBag(IDataReader dr) { - return dr.GetString(dr.GetOrdinal("Topic")); + 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) @@ -385,15 +451,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"); @@ -403,23 +470,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) { diff --git a/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutboxBuilder.cs b/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutboxBuilder.cs index 963d0120c2..0d7a168ef8 100644 --- a/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutboxBuilder.cs +++ b/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutboxBuilder.cs @@ -39,6 +39,7 @@ public class SqliteOutboxBuilder [CorrelationId] TEXT NULL, [ReplyTo] TEXT NULL, [ContentType] TEXT NULL, + [PartitionKey] TEXT NULL, [Dispatched] TEXT NULL, [HeaderBag] TEXT NULL, [Body] TEXT NULL, @@ -54,6 +55,7 @@ CONSTRAINT[PK_MessageId] PRIMARY KEY([MessageId]) [CorrelationId] TEXT NULL, [ReplyTo] TEXT NULL, [ContentType] TEXT NULL, + [PartitionKey] TEXT NULL, [Dispatched] TEXT NULL, [HeaderBag] TEXT NULL, [Body] BLOB NULL, diff --git a/src/Paramore.Brighter.Outbox.Sqlite/SqliteQueries.cs b/src/Paramore.Brighter.Outbox.Sqlite/SqliteQueries.cs index c54b6e760e..634adef8b3 100644 --- a/src/Paramore.Brighter.Outbox.Sqlite/SqliteQueries.cs +++ b/src/Paramore.Brighter.Outbox.Sqlite/SqliteQueries.cs @@ -5,8 +5,8 @@ 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 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"; 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 index 67efb6c72a..8c7fd6fdbc 100644 --- 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 @@ -63,7 +63,8 @@ public SqliteOutboxWritingBinaryMessageTests() delayedMilliseconds:5, correlationId: Guid.NewGuid(), replyTo: "ReplyTo", - contentType: "application/octet-stream"); + contentType: "application/octet-stream", + partitionKey: "123456789"); messageHeader.Bag.Add(_key1, _value1); messageHeader.Bag.Add(_key2, _value2); messageHeader.Bag.Add(_key3, _value3); @@ -94,6 +95,7 @@ public void When_Writing_A_Message_To_The_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 From c39127cdfe0a2aab1fed59fa91046c29b43b1e64 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Wed, 19 Apr 2023 08:12:27 +0100 Subject: [PATCH 16/89] sqlite supports writing kafka partition key --- .../Outbox/When_Writing_A_Message_To_The_Outbox.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 f22df4b0ef..503b05dbad 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 @@ -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); @@ -88,6 +89,7 @@ public void When_Writing_A_Message_To_The_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 From 18b01b1b6da8c3eeaa296bde812be1420d9dad7b Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Thu, 20 Apr 2023 10:19:59 +0100 Subject: [PATCH 17/89] Move Postgres to new format; add binary and partitionkey support --- .../PostgresSqlInbox.cs | 57 +- .../PostgresSqlInboxConfiguration.cs | 13 +- .../MySqlOutbox.cs | 87 ++- .../MySqlOutboxBuilder.cs | 6 +- .../MySqlQueries.cs | 4 +- ...guration.cs => PostgreSqlConfiguration.cs} | 38 +- .../PostgreSqlOutbox.cs | 713 ++++++------------ ...oxBuilder.cs => PostgreSqlOutboxBulder.cs} | 44 +- .../PostgreSqlQueries.cs | 16 + .../ServiceCollectionExtensions.cs | 6 +- .../PostgreSqlConfiguration.cs | 12 - .../PostgreSqlNpgsqlConnectionProvider.cs | 2 +- ..._writing_a_message_to_the_message_store.cs | 4 +- ...iting_a_message_to_a_binary_body_outbox.cs | 5 +- .../When_writing_a_message_to_the_outbox.cs | 4 +- .../When_Removing_Messages_From_The_Outbox.cs | 2 +- ...en_The_Message_Is_Already_In_The_Outbox.cs | 2 +- ...es_In_The_Outbox_And_A_Range_Is_Fetched.cs | 2 +- ...n_There_Is_No_Message_In_The_Sql_Outbox.cs | 2 +- ...iting_A_Message_To_A_Binary_Body_Outbox.cs | 88 +++ .../When_Writing_A_Message_To_The_Outbox.cs | 7 +- .../When_Writing_Messages_To_The_Outbox.cs | 2 +- .../PostgresSqlTestHelper.cs | 13 +- 23 files changed, 505 insertions(+), 624 deletions(-) rename src/Paramore.Brighter.Outbox.PostgreSql/{PostgreSqlOutboxConfiguration.cs => PostgreSqlConfiguration.cs} (53%) rename src/Paramore.Brighter.Outbox.PostgreSql/{PostgreSqlOutboxBuilder.cs => PostgreSqlOutboxBulder.cs} (62%) create mode 100644 src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlQueries.cs delete mode 100644 src/Paramore.Brighter.PostgreSql/PostgreSqlConfiguration.cs create mode 100644 tests/Paramore.Brighter.PostgresSQL.Tests/Outbox/When_Writing_A_Message_To_A_Binary_Body_Outbox.cs diff --git a/src/Paramore.Brighter.Inbox.Postgres/PostgresSqlInbox.cs b/src/Paramore.Brighter.Inbox.Postgres/PostgresSqlInbox.cs index cef7880f17..943865fcee 100644 --- a/src/Paramore.Brighter.Inbox.Postgres/PostgresSqlInbox.cs +++ b/src/Paramore.Brighter.Inbox.Postgres/PostgresSqlInbox.cs @@ -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,29 +183,11 @@ 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; } @@ -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/PostgresSqlInboxConfiguration.cs b/src/Paramore.Brighter.Inbox.Postgres/PostgresSqlInboxConfiguration.cs index 31ff311cd9..e36fffdb7d 100644 --- a/src/Paramore.Brighter.Inbox.Postgres/PostgresSqlInboxConfiguration.cs +++ b/src/Paramore.Brighter.Inbox.Postgres/PostgresSqlInboxConfiguration.cs @@ -27,17 +27,18 @@ THE SOFTWARE. */ namespace Paramore.Brighter.Inbox.Postgres { - public class PostgresSqlInboxConfiguration : PostgreSqlConfiguration + public class PostgresSqlInboxConfiguration { - public PostgresSqlInboxConfiguration(string connectionString, string tableName) : base(connectionString) + public PostgresSqlInboxConfiguration(string connectionString, string tableName) { + ConnectionString = connectionString; InBoxTableName = tableName; } - public PostgresSqlInboxConfiguration(string tableName) : base(null) - { - InBoxTableName = tableName; - } + /// + /// The connection string to the PostgresSql database + /// + public string ConnectionString { get; } /// /// Gets the name of the outbox table. diff --git a/src/Paramore.Brighter.Outbox.MySql/MySqlOutbox.cs b/src/Paramore.Brighter.Outbox.MySql/MySqlOutbox.cs index 00cdae4552..66faafe2e9 100644 --- a/src/Paramore.Brighter.Outbox.MySql/MySqlOutbox.cs +++ b/src/Paramore.Brighter.Outbox.MySql/MySqlOutbox.cs @@ -105,7 +105,8 @@ protected override void WriteToStore(IAmABoxTransactionConnectionProvider transa protected override async Task WriteToStoreAsync( IAmABoxTransactionConnectionProvider transactionConnectionProvider, Func commandFunc, - Action loggingAction, CancellationToken cancellationToken) + Action loggingAction, + CancellationToken cancellationToken) { var connectionProvider = _connectionProvider; if (transactionConnectionProvider != null && @@ -146,7 +147,9 @@ protected override async Task WriteToStoreAsync( } } - protected override T ReadFromStore(Func commandFunc, + protected override T ReadFromStore( + Func commandFunc, Func resultFunc) { var connection = _connectionProvider.GetConnection(); @@ -169,8 +172,10 @@ protected override T ReadFromStore(Func comman } } - 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); @@ -192,7 +197,10 @@ protected override async Task ReadFromStoreAsync(Func MapListFunction(MySqlDataReader dr) return messages; } - protected override async Task> MapListFunctionAsync(MySqlDataReader dr, + protected override async Task> MapListFunctionAsync( + MySqlDataReader dr, CancellationToken cancellationToken) { var messages = new List(); @@ -343,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, @@ -353,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) @@ -366,7 +383,8 @@ private Message MapAMessage(IDataReader dr) } var body = _configuration.BinaryMessagePayload - ? new MessageBody(GetBodyAsBytes((MySqlDataReader)dr), "application/octet-stream", CharacterEncoding.Raw) + ? new MessageBody(GetBodyAsBytes((MySqlDataReader)dr), "application/octet-stream", + CharacterEncoding.Raw) : new MessageBody(dr.GetString(dr.GetOrdinal("Body")), "application/json", CharacterEncoding.UTF8); return new Message(header, body); @@ -378,13 +396,35 @@ private byte[] GetBodyAsBytes(MySqlDataReader dr) var body = dr.GetStream(i); long bodyLength = body.Length; var buffer = new byte[bodyLength]; - body.Read(buffer,0, (int)bodyLength); + body.Read(buffer, 0, (int)bodyLength); return buffer; } - private static string GetTopic(IDataReader dr) + private static Dictionary GetContextBag(IDataReader dr) { - return dr.GetString(dr.GetOrdinal("Topic")); + 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) @@ -397,13 +437,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) @@ -415,22 +455,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 b35a6e50e3..c39512f50e 100644 --- a/src/Paramore.Brighter.Outbox.MySql/MySqlOutboxBuilder.cs +++ b/src/Paramore.Brighter.Outbox.MySql/MySqlOutboxBuilder.cs @@ -40,7 +40,8 @@ public class MySqlOutboxBuilder `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 , @@ -58,9 +59,10 @@ PRIMARY KEY (`MessageId`) `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 , + `Body` BLOB NOT NULL , `Created` TIMESTAMP(3) NOT NULL DEFAULT NOW(3), `CreatedID` INT(11) NOT NULL AUTO_INCREMENT, UNIQUE(`CreatedID`), diff --git a/src/Paramore.Brighter.Outbox.MySql/MySqlQueries.cs b/src/Paramore.Brighter.Outbox.MySql/MySqlQueries.cs index 880265e3a4..bf5a885af5 100644 --- a/src/Paramore.Brighter.Outbox.MySql/MySqlQueries.cs +++ b/src/Paramore.Brighter.Outbox.MySql/MySqlQueries.cs @@ -5,8 +5,8 @@ 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 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"; diff --git a/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutboxConfiguration.cs b/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlConfiguration.cs similarity index 53% rename from src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutboxConfiguration.cs rename to src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlConfiguration.cs index 73a7a44c3c..d54f59302c 100644 --- a/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutboxConfiguration.cs +++ b/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlConfiguration.cs @@ -22,35 +22,33 @@ THE SOFTWARE. */ #endregion -using Paramore.Brighter.PostgreSql; - namespace Paramore.Brighter.Outbox.PostgreSql { - public class PostgreSqlOutboxConfiguration : PostgreSqlConfiguration + public class PostgreSqlConfiguration : RelationalDatabaseOutboxConfiguration { /// - /// 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 + /// Initialises a new instance of /// - /// Name of the OutBox table - public PostgreSqlOutboxConfiguration(string outBoxTablename) : base(null) + /// The connection string to the database + /// The name of the outbox within the table + /// The name of the inbox table + /// A store for messages to be written + /// Do we store the payload as text or binary + public PostgreSqlConfiguration( + string connectionString, + string outBoxTableName = null, + string inboxTableName = null, + string queueStoreTable = null, + bool binaryMessagePayload = false) + : base(connectionString, outBoxTableName, queueStoreTable, binaryMessagePayload) { - OutboxTableName = outBoxTablename; + InBoxTableName = inboxTableName; } /// - /// Gets the name of the outbox table. + /// Gets the name of the inbox table. /// - /// The name of the outbox table. - public string OutboxTableName { get; } + /// The name of the inbox table. + public string InBoxTableName { get; private set; } } } diff --git a/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutbox.cs b/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutbox.cs index 6d143bdfce..9c930b72d7 100644 --- a/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutbox.cs +++ b/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutbox.cs @@ -26,8 +26,9 @@ THE SOFTWARE. */ using System; using System.Collections.Generic; using System.Data; -using System.Linq; using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Npgsql; using NpgsqlTypes; @@ -36,354 +37,99 @@ THE SOFTWARE. */ namespace Paramore.Brighter.Outbox.PostgreSql { - public class PostgreSqlOutbox : IAmABulkOutboxSync + public class + PostgreSqlOutbox : RelationDatabaseOutbox { - 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(); - } + private readonly PostgreSqlConfiguration _configuration; + private readonly IPostgreSqlConnectionProvider _connectionProvider; + - /// - /// Initialises a new instance of class. - /// - /// PostgreSql Outbox Configuration. - public PostgreSqlOutbox(PostgreSqlOutboxConfiguration configuration, IPostgreSqlConnectionProvider connectionProvider = null) + public PostgreSqlOutbox( + PostgreSqlConfiguration configuration, + IPostgreSqlConnectionProvider connectionProvider) : base( + configuration.OutBoxTableName, new PostgreSqlQueries(), ApplicationLogging.CreateLogger()) { _configuration = configuration; - _connectionProvider = connectionProvider ?? new PostgreSqlNpgsqlConnectionProvider(configuration); - _outboxTableName = configuration.OutboxTableName; + _connectionProvider = connectionProvider; } - /// - /// 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); + public PostgreSqlOutbox(PostgreSqlConfiguration configuration) + : this(configuration, new PostgreSqlNpgsqlConnectionProvider(configuration)) + { } - 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) + protected override void WriteToStore(IAmABoxTransactionConnectionProvider transactionConnectionProvider, + Func commandFunc, + Action loggingAction) { - var connectionProvider = GetConnectionProvider(transactionConnectionProvider); - var connection = GetOpenConnection(connectionProvider); + var connectionProvider = _connectionProvider; + if (transactionConnectionProvider != null && + transactionConnectionProvider is IPostgreSqlConnectionProvider provider) + connectionProvider = provider; - try + var connection = connectionProvider.GetConnection(); + + if (connection.State != ConnectionState.Open) + connection.Open(); + using (var command = commandFunc.Invoke(connection)) { - using (var command = InitBulkAddDbCommand(connection, messages.ToList())) + try { - if (connectionProvider.HasOpenTransaction) + if (transactionConnectionProvider != null && 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)) + catch (PostgresException sqlException) { - var messages = new List(); - - using (var dbDataReader = command.ExecuteReader()) + if (sqlException.SqlState == PostgresErrorCodes.UniqueViolation) { - while (dbDataReader.Read()) - { - messages.Add(MapAMessage(dbDataReader)); - } + loggingAction.Invoke(); + return; } - 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(); + throw; } - } - 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)) + finally { - var messages = new List(); - - using (var dbDataReader = command.ExecuteReader()) - { - while (dbDataReader.Read()) - { - messages.Add(MapAMessage(dbDataReader)); - } - } - - return messages; + if (!connectionProvider.IsSharedConnection) + connection.Dispose(); + else if (!connectionProvider.HasOpenTransaction) + connection.Close(); } } - 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) + protected override async Task WriteToStoreAsync( + IAmABoxTransactionConnectionProvider transactionConnectionProvider, + Func commandFunc, + Action loggingAction, + CancellationToken cancellationToken) { var connectionProvider = _connectionProvider; - if (transactionConnectionProvider != null && transactionConnectionProvider is IPostgreSqlConnectionProvider provider) + if (transactionConnectionProvider != null && + transactionConnectionProvider is IPostgreSqlConnectionProvider provider) connectionProvider = provider; - var connection = connectionProvider.GetConnection(); + var connection = await connectionProvider.GetConnectionAsync(cancellationToken) + .ConfigureAwait(ContinueOnCapturedContext); if (connection.State != ConnectionState.Open) - connection.Open(); + await connection.OpenAsync(cancellationToken); using (var command = commandFunc.Invoke(connection)) { try { if (transactionConnectionProvider != null && connectionProvider.HasOpenTransaction) command.Transaction = connectionProvider.GetTransaction(); - command.ExecuteNonQuery(); + await command.ExecuteNonQueryAsync(cancellationToken); } catch (PostgresException sqlException) { if (sqlException.SqlState == PostgresErrorCodes.UniqueViolation) { - loggingAction.Invoke(); + s_logger.LogWarning( + "PostgresSqlOutbox: A duplicate was detected in the batch"); return; } @@ -394,205 +140,210 @@ private void WriteToStore(IAmABoxTransactionConnectionProvider transactionConnec if (!connectionProvider.IsSharedConnection) connection.Dispose(); else if (!connectionProvider.HasOpenTransaction) - connection.Close(); + await connection.CloseAsync(); } } } - private IPostgreSqlConnectionProvider GetConnectionProvider(IAmABoxTransactionConnectionProvider transactionConnectionProvider = null) + protected override T ReadFromStore(Func commandFunc, + Func resultFunc) { - var connectionProvider = _connectionProvider ?? new PostgreSqlNpgsqlConnectionProvider(_configuration); + var connection = _connectionProvider.GetConnection(); - if (transactionConnectionProvider != null) + if (connection.State != ConnectionState.Open) + connection.Open(); + using (var command = commandFunc.Invoke(connection)) { - if (transactionConnectionProvider is IPostgreSqlTransactionConnectionProvider provider) - connectionProvider = provider; - else - throw new Exception($"{nameof(transactionConnectionProvider)} does not implement interface {nameof(IPostgreSqlTransactionConnectionProvider)}."); + try + { + return resultFunc.Invoke(command.ExecuteReader()); + } + finally + { + if (!_connectionProvider.IsSharedConnection) + connection.Dispose(); + else if (!_connectionProvider.HasOpenTransaction) + connection.Close(); + } } - - return connectionProvider; } - private NpgsqlConnection GetOpenConnection(IPostgreSqlConnectionProvider connectionProvider) + protected override async Task ReadFromStoreAsync(Func commandFunc, + Func> resultFunc, CancellationToken cancellationToken) { - NpgsqlConnection connection = connectionProvider.GetConnection(); + var connection = await _connectionProvider.GetConnectionAsync(cancellationToken); 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); + await connection.OpenAsync(cancellationToken); + using (var command = commandFunc.Invoke(connection)) + { + try + { + return await resultFunc.Invoke(await command.ExecuteReaderAsync(cancellationToken)); + } + finally + { + if (!_connectionProvider.IsSharedConnection) + connection.Dispose(); + else if (!_connectionProvider.HasOpenTransaction) + connection.Close(); + } + } } - private NpgsqlCommand InitPagedDispatchedCommand(NpgsqlConnection connection, double millisecondsDispatchedSince, - int pageSize, int pageNumber) + protected override NpgsqlCommand CreateCommand( + NpgsqlConnection connection, + string sqlText, + int outBoxTimeout, + params NpgsqlParameter[] parameters) { 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.CommandTimeout = outBoxTimeout < 0 ? 0 : outBoxTimeout; + command.CommandText = sqlText; command.Parameters.AddRange(parameters); return command; } - private NpgsqlCommand InitPagedReadCommand(NpgsqlConnection connection, int pageSize, int pageNumber) + protected override NpgsqlParameter[] CreatePagedOutstandingParameters(double milliSecondsSinceAdded, + 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); + var offset = (pageNumber - 1) * pageSize; + var parameters = new NpgsqlParameter[3]; + parameters[0] = CreateSqlParameter("OffsetValue", offset); + parameters[1] = CreateSqlParameter("PageSize", pageSize); + parameters[2] = CreateSqlParameter("OutstandingSince", milliSecondsSinceAdded); - return command; + return parameters; } - private NpgsqlCommand InitPagedOutstandingCommand(NpgsqlConnection connection, double milliSecondsSinceAdded, int pageSize, - int pageNumber) + protected override NpgsqlParameter CreateSqlParameter(string parameterName, object value) { - 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; + return new NpgsqlParameter { ParameterName = parameterName, Value = value }; } - private NpgsqlParameter[] InitAddDbParameters(Message message, int? position = null) + protected override 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[] + return new[] { - 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) + 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 + } }; } - private NpgsqlCommand InitMarkDispatchedCommand(NpgsqlConnection connection, Guid messageId, - DateTime? dispatchedAt) + protected override Message MapFunction(NpgsqlDataReader dr) { - 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 + if (dr.Read()) { - 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(); + return MapAMessage(dr); } + + return new Message(); } - private NpgsqlCommand InitAddDbCommand(NpgsqlConnection connection, NpgsqlParameter[] parameters) + protected override async Task MapFunctionAsync(NpgsqlDataReader dr, + CancellationToken cancellationToken) { - 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); + if (await dr.ReadAsync(cancellationToken)) + { + return MapAMessage(dr); + } - return command; + return new Message(); } - - private NpgsqlCommand InitBulkAddDbCommand(NpgsqlConnection connection, List messages) + + protected override IEnumerable MapListFunction(NpgsqlDataReader dr) { - var messageParams = new List(); - var parameters = new List(); - - for (int i = 0; i < messages.Count; i++) + var messages = new List(); + while (dr.Read()) { - 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)); - + messages.Add(MapAMessage(dr)); } - 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; + dr.Close(); + + return messages; } - private Message MapFunction(IDataReader reader) + protected override async Task> MapListFunctionAsync( + NpgsqlDataReader dr, + CancellationToken cancellationToken) { - if (reader.Read()) + var messages = new List(); + while (await dr.ReadAsync(cancellationToken)) { - return MapAMessage(reader); + messages.Add(MapAMessage(dr)); } - return new Message(); + await dr.CloseAsync(); + + return messages; } - private Message MapAMessage(IDataReader dr) + public Message MapAMessage(IDataReader dr) { var id = GetMessageId(dr); var messageType = GetMessageType(dr); @@ -602,6 +353,7 @@ private Message MapAMessage(IDataReader dr) var correlationId = GetCorrelationId(dr); var replyTo = GetReplyTo(dr); var contentType = GetContentType(dr); + var partitionKey = GetPartitionKey(dr); var header = new MessageHeader( messageId: id, @@ -612,7 +364,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) @@ -623,11 +376,42 @@ private Message MapAMessage(IDataReader dr) } } - var body = new MessageBody(dr.GetString(dr.GetOrdinal("Body"))); + 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(IDataReader dr) + { + var ordinal = dr.GetOrdinal("ContentType"); + 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 string GetTopic(IDataReader dr) { return dr.GetString(dr.GetOrdinal("Topic")); @@ -643,14 +427,13 @@ private static Guid GetMessageId(IDataReader dr) return dr.GetGuid(dr.GetOrdinal("MessageId")); } - private string GetContentType(IDataReader dr) + private string GetPartitionKey(IDataReader dr) { - var ordinal = dr.GetOrdinal("ContentType"); - if (dr.IsDBNull(ordinal)) - return null; + 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) @@ -663,24 +446,6 @@ 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) - { - 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"); diff --git a/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutboxBuilder.cs b/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutboxBulder.cs similarity index 62% rename from src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutboxBuilder.cs rename to src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutboxBulder.cs index 3a1d587a49..c5b001b7a7 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 * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = N'{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) { - return string.Format(OutboxDdl, outboxTableName); + return binaryMessagePayload ? string.Format(BinaryOutboxDdl, outboxTableName) : string.Format(TextOutboxDdl, outboxTableName); } /// diff --git a/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlQueries.cs b/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlQueries.cs new file mode 100644 index 0000000000..0938d6b985 --- /dev/null +++ b/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlQueries.cs @@ -0,0 +1,16 @@ +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 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 < (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} )"; + public string DeleteMessagesCommand { get; }= "DELETE FROM {0} WHERE MessageId IN ( {1} )"; + } +} diff --git a/src/Paramore.Brighter.Outbox.PostgreSql/ServiceCollectionExtensions.cs b/src/Paramore.Brighter.Outbox.PostgreSql/ServiceCollectionExtensions.cs index d44e8f250d..d16e07b1eb 100644 --- a/src/Paramore.Brighter.Outbox.PostgreSql/ServiceCollectionExtensions.cs +++ b/src/Paramore.Brighter.Outbox.PostgreSql/ServiceCollectionExtensions.cs @@ -8,7 +8,7 @@ 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) + this IBrighterBuilder brighterBuilder, PostgreSqlConfiguration configuration, Type connectionProvider = null, ServiceLifetime serviceLifetime = ServiceLifetime.Singleton) { if (brighterBuilder is null) throw new ArgumentNullException($"{nameof(brighterBuilder)} cannot be null.", nameof(brighterBuilder)); @@ -16,7 +16,7 @@ public static IBrighterBuilder UsePostgreSqlOutbox( if (configuration is null) throw new ArgumentNullException($"{nameof(configuration)} cannot be null.", nameof(configuration)); - brighterBuilder.Services.AddSingleton(configuration); + brighterBuilder.Services.AddSingleton(configuration); if (connectionProvider is object) { @@ -61,7 +61,7 @@ public static IBrighterBuilder UsePostgreSqlTransactionConnectionProvider( private static PostgreSqlOutbox BuildPostgreSqlOutboxSync(IServiceProvider provider) { - var config = provider.GetService(); + var config = provider.GetService(); var connectionProvider = provider.GetService(); return new PostgreSqlOutbox(config, connectionProvider); 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/PostgreSqlNpgsqlConnectionProvider.cs b/src/Paramore.Brighter.PostgreSql/PostgreSqlNpgsqlConnectionProvider.cs index 0eb773f027..b12acd221e 100644 --- a/src/Paramore.Brighter.PostgreSql/PostgreSqlNpgsqlConnectionProvider.cs +++ b/src/Paramore.Brighter.PostgreSql/PostgreSqlNpgsqlConnectionProvider.cs @@ -9,7 +9,7 @@ public class PostgreSqlNpgsqlConnectionProvider : IPostgreSqlConnectionProvider { private readonly string _connectionString; - public PostgreSqlNpgsqlConnectionProvider(PostgreSqlConfiguration configuration) + public PostgreSqlNpgsqlConnectionProvider(RelationalDatabaseOutboxConfiguration configuration) { if (string.IsNullOrWhiteSpace(configuration?.ConnectionString)) throw new ArgumentNullException(nameof(configuration.ConnectionString)); 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..72688b3072 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); @@ -108,6 +109,7 @@ private void AssertMessage() _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.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 index b8954294f5..bc3b480f58 100644 --- 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 @@ -36,7 +36,8 @@ public MySqlOutboxWritingBinaryMessageTests() delayedMilliseconds: 5, correlationId: new Guid(), replyTo: "ReplyTo", - contentType: "application/octet-stream"); + contentType: "application/octet-stream", + partitionKey: Guid.NewGuid().ToString()); messageHeader.Bag.Add(_key1, _value1); messageHeader.Bag.Add(_key2, _value2); messageHeader.Bag.Add(_key3, _value3); @@ -67,7 +68,7 @@ public void When_writing_a_message_to_a_binary_body_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.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 64dccffaae..f3a8f16689 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 @@ -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); @@ -88,6 +89,7 @@ public void When_Writing_A_Message_To_The_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.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 9edecb624d..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 @@ -47,7 +47,7 @@ public SqlOutboxDeletingMessagesTests() _postgresSqlTestHelper = new PostgresSqlTestHelper(); _postgresSqlTestHelper.SetupMessageDb(); - _sqlOutbox = new PostgreSqlOutbox(_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")); 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 265616e1e6..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 @@ -43,7 +43,7 @@ public PostgreSqlOutboxMessageAlreadyExistsTests() _postgresSqlTestHelper = new PostgresSqlTestHelper(); _postgresSqlTestHelper.SetupMessageDb(); - _sqlOutbox = new PostgreSqlOutbox(_postgresSqlTestHelper.OutboxConfiguration); + _sqlOutbox = new PostgreSqlOutbox(_postgresSqlTestHelper.Configuration); _messageEarliest = new Message(new MessageHeader(Guid.NewGuid(), "test_topic", MessageType.MT_DOCUMENT), new MessageBody("message body")); _sqlOutbox.Add(_messageEarliest); } 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 0d03a1c2a7..fb537a91c6 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 @@ -47,7 +47,7 @@ public PostgreSqlOutboxRangeRequestTests() _postgresSqlTestHelper = new PostgresSqlTestHelper(); _postgresSqlTestHelper.SetupMessageDb(); - _sqlOutbox = new PostgreSqlOutbox(_postgresSqlTestHelper.OutboxConfiguration); + _sqlOutbox = new PostgreSqlOutbox(_postgresSqlTestHelper.Configuration); 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")); 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 1fb4dcb276..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 @@ -43,7 +43,7 @@ public PostgreSqlOutboxEmptyStoreTests() _postgresSqlTestHelper = new PostgresSqlTestHelper(); _postgresSqlTestHelper.SetupMessageDb(); - _sqlOutbox = new PostgreSqlOutbox(_postgresSqlTestHelper.OutboxConfiguration); + _sqlOutbox = new PostgreSqlOutbox(_postgresSqlTestHelper.Configuration); _messageEarliest = new Message(new MessageHeader(Guid.NewGuid(), "test_topic", MessageType.MT_DOCUMENT), new MessageBody("message body")); } 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..99355f3057 --- /dev/null +++ b/tests/Paramore.Brighter.PostgresSQL.Tests/Outbox/When_Writing_A_Message_To_A_Binary_Body_Outbox.cs @@ -0,0 +1,88 @@ +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.Should().Be(_messageEarliest.Header.TimeStamp); + _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 057be6df6c..8ad44ceedd 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 @@ -53,7 +53,7 @@ public SqlOutboxWritingMessageTests() _postgresSqlTestHelper = new PostgresSqlTestHelper(); _postgresSqlTestHelper.SetupMessageDb(); - _sqlOutbox = new PostgreSqlOutbox(_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); @@ -90,7 +91,7 @@ public void When_Writing_A_Message_To_The_PostgreSql_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 3ab12ff56f..6f1f11f825 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 @@ -47,7 +47,7 @@ public SqlOutboxWritngMessagesTests() _postgresSqlTestHelper = new PostgresSqlTestHelper(); _postgresSqlTestHelper.SetupMessageDb(); - _sqlOutbox = new PostgreSqlOutbox(_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")); diff --git a/tests/Paramore.Brighter.PostgresSQL.Tests/PostgresSqlTestHelper.cs b/tests/Paramore.Brighter.PostgresSQL.Tests/PostgresSqlTestHelper.cs index a13f50bf1a..ddea1d937f 100644 --- a/tests/Paramore.Brighter.PostgresSQL.Tests/PostgresSqlTestHelper.cs +++ b/tests/Paramore.Brighter.PostgresSQL.Tests/PostgresSqlTestHelper.cs @@ -10,13 +10,15 @@ 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 PostgresSqlTestHelper(bool binaryMessagePayload = false) { + _binaryMessagePayload = binaryMessagePayload; var builder = new ConfigurationBuilder().AddEnvironmentVariables(); var configuration = builder.Build(); @@ -26,9 +28,12 @@ public PostgresSqlTestHelper() _tableName = $"test_{Guid.NewGuid():N}"; } - public PostgreSqlOutboxConfiguration OutboxConfiguration => new PostgreSqlOutboxConfiguration(_postgreSqlSettings.TestsBrighterConnectionString, _tableName); + public PostgreSqlConfiguration Configuration + => new PostgreSqlConfiguration(_postgreSqlSettings.TestsBrighterConnectionString, + outBoxTableName: _tableName, binaryMessagePayload: _binaryMessagePayload); - public PostgresSqlInboxConfiguration InboxConfiguration => new PostgresSqlInboxConfiguration(_postgreSqlSettings.TestsBrighterConnectionString, _tableName); + public PostgresSqlInboxConfiguration InboxConfiguration + => new PostgresSqlInboxConfiguration(_postgreSqlSettings.TestsBrighterConnectionString, _tableName); public void SetupMessageDb() @@ -92,7 +97,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()) From 0c5d25407d0105f3516f438125d1d6cf247e1ec8 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Mon, 24 Apr 2023 21:16:16 +0100 Subject: [PATCH 18/89] ensure consistency of headers --- .../RmqMessagePublisher.cs | 2 +- .../MessagePump.cs | 2 +- src/Paramore.Brighter/Message.cs | 74 +++++++++++-------- ...layed_message_via_the_messaging_gateway.cs | 2 +- 4 files changed, 46 insertions(+), 34 deletions(-) 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.ServiceActivator/MessagePump.cs b/src/Paramore.Brighter.ServiceActivator/MessagePump.cs index 45589e5bad..d605235396 100644 --- a/src/Paramore.Brighter.ServiceActivator/MessagePump.cs +++ b/src/Paramore.Brighter.ServiceActivator/MessagePump.cs @@ -314,7 +314,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/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/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 From ae98f43bdcf1774f8df3ee8d06c2822d93418211 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Sat, 29 Apr 2023 19:55:00 +0100 Subject: [PATCH 19/89] Fix unix timestamps --- .../KafkaDefaultMessageHeaderBuilder.cs | 14 +++++- .../KafkaMessageCreator.cs | 2 +- .../UnixTimestamp.cs | 48 ------------------- src/Paramore.Brighter/MessageHeader.cs | 15 +----- .../When_posting_a_message.cs | 15 +++++- 5 files changed, 29 insertions(+), 65 deletions(-) delete mode 100644 src/Paramore.Brighter.MessagingGateway.Kafka/UnixTimestamp.cs 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/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/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); } From e74983ba98e8228527049f19bedfee1d57a7e3ff Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Sun, 30 Apr 2023 17:34:14 +0100 Subject: [PATCH 20/89] Fix issue with order of paged read --- .../PostgreSqlQueries.cs | 2 +- ...es_In_The_Outbox_And_A_Range_Is_Fetched.cs | 20 ++++++++++--------- .../When_Writing_A_Message_To_The_Outbox.cs | 3 ++- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlQueries.cs b/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlQueries.cs index 0938d6b985..31d7736ade 100644 --- a/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlQueries.cs +++ b/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlQueries.cs @@ -3,7 +3,7 @@ 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 DESC) AS NUMBER, * FROM {0}) AS TBL WHERE 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 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 < (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}"; 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 fb537a91c6..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,8 +37,9 @@ 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 PostgreSqlOutbox _sqlOutbox; @@ -48,14 +49,15 @@ public PostgreSqlOutboxRangeRequestTests() _postgresSqlTestHelper.SetupMessageDb(); _sqlOutbox = new PostgreSqlOutbox(_postgresSqlTestHelper.Configuration); - 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); + 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); - _sqlOutbox.Add(message1); + _sqlOutbox.Add(messageTwo); Task.Delay(100); - _sqlOutbox.Add(message2); + _sqlOutbox.Add(messageThree); } [Fact] @@ -66,7 +68,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(_topicLastMessage); //should not fetch null messages _messages.Should().NotBeNull(); } 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 8ad44ceedd..599ccd6331 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 @@ -85,7 +85,8 @@ public void When_Writing_A_Message_To_The_PostgreSql_Outbox() //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.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); From f8dd8c134bb04736d24d980f064ce1f3f4af7924 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Sun, 30 Apr 2023 19:07:36 +0100 Subject: [PATCH 21/89] Fix tests broken now that headers don't auto-round times. --- .../Outbox/When_writing_a_message_to_a_binary_body_outbox.cs | 3 ++- .../When_writing_a_message_to_the_message_store_async.cs | 3 ++- .../Outbox/When_writing_a_message_to_the_outbox.cs | 3 ++- .../Outbox/When_Writing_A_Message_To_A_Binary_Body_Outbox.cs | 3 ++- 4 files changed, 8 insertions(+), 4 deletions(-) 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 index bc3b480f58..fc9796519f 100644 --- 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 @@ -62,7 +62,8 @@ public void When_writing_a_message_to_a_binary_body_outbox() //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.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); 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 bc9c5982c2..257deb48b2 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 @@ -75,7 +75,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(_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); 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 f3a8f16689..59e3aea1f4 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 @@ -83,7 +83,8 @@ public void When_Writing_A_Message_To_The_Outbox() //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.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); 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 index 99355f3057..9fe15ddb04 100644 --- 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 @@ -59,7 +59,8 @@ public void When_Writing_A_Message_To_A_Binary_Body_Outbox() //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.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); From a2b68a90110e9b2839331885a7fa06b1cbf7ad26 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Sun, 30 Apr 2023 21:47:19 +0100 Subject: [PATCH 22/89] Fix up sql --- .../MsSqlOutbox.cs | 45 +++++++++++++++---- src/Paramore.Brighter/MessageBody.cs | 15 ++++++- ..._message_to_a_binary_body_message_store.cs | 5 ++- ..._writing_a_message_to_the_message_store.cs | 10 ++--- ...ng_a_message_to_the_message_store_async.cs | 3 +- ...iting_a_message_to_a_binary_body_outbox.cs | 4 +- ...ng_a_message_to_the_message_store_async.cs | 4 +- .../When_writing_a_message_to_the_outbox.cs | 4 +- ...iting_A_Message_To_A_Binary_Body_Outbox.cs | 4 +- .../When_Writing_A_Message_To_The_Outbox.cs | 4 +- 10 files changed, 69 insertions(+), 29 deletions(-) diff --git a/src/Paramore.Brighter.Outbox.MsSql/MsSqlOutbox.cs b/src/Paramore.Brighter.Outbox.MsSql/MsSqlOutbox.cs index dbdb8c3549..8d59ace03a 100644 --- a/src/Paramore.Brighter.Outbox.MsSql/MsSqlOutbox.cs +++ b/src/Paramore.Brighter.Outbox.MsSql/MsSqlOutbox.cs @@ -239,49 +239,68 @@ protected override SqlParameter[] InitAddDbParameters(Message message, int? posi var bagJson = JsonSerializer.Serialize(message.Header.Bag, JsonSerialisationOptions.Options); return new[] { - new SqlParameter { ParameterName = $"{prefix}MessageId", Value = (object)message.Id ?? DBNull.Value }, + new SqlParameter + { + ParameterName = $"{prefix}MessageId", + DbType = DbType.Guid, + Value = (object)message.Id ?? DBNull.Value + }, new SqlParameter { ParameterName = $"{prefix}MessageType", + DbType = DbType.String, Value = (object)message.Header.MessageType.ToString() ?? DBNull.Value }, new SqlParameter { - ParameterName = $"{prefix}Topic", Value = (object)message.Header.Topic ?? DBNull.Value + ParameterName = $"{prefix}Topic", + DbType = DbType.String, + Value = (object)message.Header.Topic ?? DBNull.Value }, new SqlParameter { ParameterName = $"{prefix}Timestamp", + DbType = DbType.DateTime, Value = (object)message.Header.TimeStamp.ToUniversalTime() ?? DBNull.Value }, //always store in UTC, as this is how we query messages new SqlParameter { ParameterName = $"{prefix}CorrelationId", + DbType = DbType.Guid, Value = (object)message.Header.CorrelationId ?? DBNull.Value }, new SqlParameter { - ParameterName = $"{prefix}ReplyTo", Value = (object)message.Header.ReplyTo ?? DBNull.Value + ParameterName = $"{prefix}ReplyTo", + DbType = DbType.String, + Value = (object)message.Header.ReplyTo ?? DBNull.Value }, new SqlParameter { ParameterName = $"{prefix}ContentType", + DbType = DbType.String, Value = (object)message.Header.ContentType ?? DBNull.Value }, new SqlParameter { ParameterName = $"{prefix}PartitionKey", + DbType = DbType.String, Value = (object)message.Header.PartitionKey ?? DBNull.Value }, - new SqlParameter { ParameterName = $"{prefix}HeaderBag", Value = (object)bagJson ?? DBNull.Value }, + new SqlParameter { ParameterName = $"{prefix}HeaderBag", + Value = (object)bagJson ?? DBNull.Value }, _configuration.BinaryMessagePayload ? new SqlParameter { - ParameterName = $"{prefix}Body", Value = (object)message.Body?.Bytes ?? DBNull.Value + ParameterName = $"{prefix}Body", + DbType = DbType.Binary, + Value = (object)message.Body?.Bytes ?? DBNull.Value } : new SqlParameter { - ParameterName = $"{prefix}Body", Value = (object)message.Body?.Value ?? DBNull.Value + ParameterName = $"{prefix}Body", + DbType = DbType.String, + Value = (object)message.Body?.Value ?? DBNull.Value } }; } @@ -353,13 +372,21 @@ private string GetPartitionKey(SqlDataReader dr) private byte[] GetBodyAsBytes(SqlDataReader dr) { - var i = dr.GetOrdinal("Body"); - var body = dr.GetStream(i); + 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(SqlDataReader dr) + { + var ordinal = dr.GetOrdinal("Body"); + return dr.IsDBNull(ordinal) ? null : dr.GetString(ordinal); + } #endregion @@ -459,7 +486,7 @@ private Message MapAMessage(SqlDataReader dr) string messageBody = string.Empty; var body = _configuration.BinaryMessagePayload ? new MessageBody(GetBodyAsBytes((SqlDataReader)dr), "application/octet-stream", CharacterEncoding.Raw) - : new MessageBody(dr.GetString(dr.GetOrdinal("Body")), "application/json", CharacterEncoding.UTF8); + : new MessageBody(GetBodyAsText(dr), "application/json", CharacterEncoding.UTF8); return new Message(header, body); } } 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/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 index 63c4b22515..ca32900ad3 100644 --- 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 @@ -61,7 +61,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(); @@ -76,7 +76,8 @@ private void AssertMessage() //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.ffZ") + .Should().Be(_message.Header.TimeStamp.ToString("yyyy-MM-ddTHH:mm:ss.ffZ")); _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_a_message_to_the_message_store.cs b/tests/Paramore.Brighter.MSSQL.Tests/Outbox/When_writing_a_message_to_the_message_store.cs index 72688b3072..2ace05bff0 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 @@ -85,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(); @@ -96,14 +96,12 @@ 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.ffZ") + .Should().Be(_message.Header.TimeStamp.ToString("yyyy-MM-ddTHH:mm:ss.ffZ")); _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_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..51846a659d 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.ffZ") + .Should().Be(_message.Header.TimeStamp.ToString("yyyy-MM-ddTHH:mm:ss.ffZ")); _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.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 index fc9796519f..3312632b71 100644 --- 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 @@ -62,8 +62,8 @@ public void When_writing_a_message_to_a_binary_body_outbox() //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.fffZ") - .Should().Be(_messageEarliest.Header.TimeStamp.ToString("yyyy-MM-ddTHH:mm:ss.fffZ")); + _storedMessage.Header.TimeStamp.ToString("yyyy-MM-ddTHH:mm:ss.ffZ") + .Should().Be(_messageEarliest.Header.TimeStamp.ToString("yyyy-MM-ddTHH:mm:ss.ffZ")); _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_message_store_async.cs b/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_writing_a_message_to_the_message_store_async.cs index 257deb48b2..dd653e7839 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 @@ -75,8 +75,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(_messageEarliest.Header.Topic); _storedMessage.Header.MessageType.Should().Be(_messageEarliest.Header.MessageType); - _storedMessage.Header.TimeStamp.ToString("yyyy-MM-ddTHH:mm:ss.fffZ") - .Should().Be(_messageEarliest.Header.TimeStamp.ToString("yyyy-MM-ddTHH:mm:ss.fffZ")); + _storedMessage.Header.TimeStamp.ToString("yyyy-MM-ddTHH:mm:ss.ffZ") + .Should().Be(_messageEarliest.Header.TimeStamp.ToString("yyyy-MM-ddTHH:mm:ss.ffZ")); _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 59e3aea1f4..042a1b9af8 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 @@ -83,8 +83,8 @@ public void When_Writing_A_Message_To_The_Outbox() //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.fffZ") - .Should().Be(_messageEarliest.Header.TimeStamp.ToString("yyyy-MM-ddTHH:mm:ss.fffZ")); + _storedMessage.Header.TimeStamp.ToString("yyyy-MM-ddTHH:mm:ss.ffZ") + .Should().Be(_messageEarliest.Header.TimeStamp.ToString("yyyy-MM-ddTHH:mm:ss.ffZ")); _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.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 index 9fe15ddb04..0ec2c926fa 100644 --- 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 @@ -59,8 +59,8 @@ public void When_Writing_A_Message_To_A_Binary_Body_Outbox() //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.fffZ") - .Should().Be(_messageEarliest.Header.TimeStamp.ToString("yyyy-MM-ddTHH:mm:ss.fffZ")); + _storedMessage.Header.TimeStamp.ToString("yyyy-MM-ddTHH:mm:ss.ffZ") + .Should().Be(_messageEarliest.Header.TimeStamp.ToString("yyyy-MM-ddTHH:mm:ss.ffZ")); _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.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 599ccd6331..2f2dab6714 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 @@ -85,8 +85,8 @@ public void When_Writing_A_Message_To_The_PostgreSql_Outbox() //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.fffZ") - .Should().Be(_messageEarliest.Header.TimeStamp.ToString("yyyy-MM-ddTHH:mm:ss.fffZ")); + _storedMessage.Header.TimeStamp.ToString("yyyy-MM-ddTHH:mm:ss.ffZ") + .Should().Be(_messageEarliest.Header.TimeStamp.ToString("yyyy-MM-ddTHH:mm:ss.ffZ")); _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); From f387394dc354f178f19d5814aac69bd2fefeb727 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Mon, 1 May 2023 15:21:53 +0100 Subject: [PATCH 23/89] Reduce precision of timestamp tests to improve success rate --- ...ng_a_message_to_the_message_store_async.cs | 4 +-- ...iting_a_message_to_a_binary_body_outbox.cs | 4 +-- ...ng_a_message_to_the_message_store_async.cs | 4 +-- .../When_writing_a_message_to_the_outbox.cs | 4 +-- .../When_writing_messages_to_the_outbox.cs | 28 +++++++++---------- ...iting_A_Message_To_A_Binary_Body_Outbox.cs | 4 +-- .../When_Writing_A_Message_To_The_Outbox.cs | 4 +-- ...iting_A_Message_To_A_Binary_Body_Outbox.cs | 3 +- .../When_Writing_A_Message_To_The_Outbox.cs | 3 +- ...n_Writing_A_Message_To_The_Outbox_Async.cs | 3 +- 10 files changed, 32 insertions(+), 29 deletions(-) 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 51846a659d..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,8 +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.ToString("yyyy-MM-ddTHH:mm:ss.ffZ") - .Should().Be(_message.Header.TimeStamp.ToString("yyyy-MM-ddTHH:mm:ss.ffZ")); + _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.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 index 3312632b71..e09c0b0ba5 100644 --- 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 @@ -62,8 +62,8 @@ public void When_writing_a_message_to_a_binary_body_outbox() //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.ffZ") - .Should().Be(_messageEarliest.Header.TimeStamp.ToString("yyyy-MM-ddTHH:mm:ss.ffZ")); + _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_message_store_async.cs b/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_writing_a_message_to_the_message_store_async.cs index dd653e7839..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 @@ -75,8 +75,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(_messageEarliest.Header.Topic); _storedMessage.Header.MessageType.Should().Be(_messageEarliest.Header.MessageType); - _storedMessage.Header.TimeStamp.ToString("yyyy-MM-ddTHH:mm:ss.ffZ") - .Should().Be(_messageEarliest.Header.TimeStamp.ToString("yyyy-MM-ddTHH:mm:ss.ffZ")); + _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 042a1b9af8..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 @@ -83,8 +83,8 @@ public void When_Writing_A_Message_To_The_Outbox() //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.ffZ") - .Should().Be(_messageEarliest.Header.TimeStamp.ToString("yyyy-MM-ddTHH:mm:ss.ffZ")); + _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_messages_to_the_outbox.cs b/tests/Paramore.Brighter.MySQL.Tests/Outbox/When_writing_messages_to_the_outbox.cs index ac99baf3ab..019dbdb35c 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 @@ -37,9 +37,9 @@ public class MySqlOutboxWritngMessagesTests { private readonly MySqlTestHelper _mySqlTestHelper; private readonly MySqlOutbox _mySqlOutbox; - 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; public MySqlOutboxWritngMessagesTests() @@ -48,25 +48,25 @@ public MySqlOutboxWritngMessagesTests() _mySqlTestHelper.SetupMessageDb(); _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() { - _mySqlOutbox.Add(_messageEarliest); - _mySqlOutbox.Add(_message2); - _mySqlOutbox.Add(_messageLatest); + _mySqlOutbox.Add(_messageOne); + _mySqlOutbox.Add(_messageTwo); + _mySqlOutbox.Add(_messageThree); _retrievedMessages = _mySqlOutbox.Get(); //should read first message last from the outbox - _retrievedMessages.Last().Id.Should().Be(_messageEarliest.Id); + _retrievedMessages.Last().Id.Should().Be(_messageOne.Id); //should read last message first from the outbox - _retrievedMessages.First().Id.Should().Be(_messageLatest.Id); + _retrievedMessages.First().Id.Should().Be(_messageThree.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 }; + 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); + _retrievedMessages.Last().Id.Should().Be(_messageOne.Id); //should read last message first from the outbox - _retrievedMessages.First().Id.Should().Be(_messageLatest.Id); + _retrievedMessages.First().Id.Should().Be(_messageThree.Id); //should read the messages from the outbox _retrievedMessages.Should().HaveCount(3); } 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 index 0ec2c926fa..11117b9ee1 100644 --- 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 @@ -59,8 +59,8 @@ public void When_Writing_A_Message_To_A_Binary_Body_Outbox() //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.ffZ") - .Should().Be(_messageEarliest.Header.TimeStamp.ToString("yyyy-MM-ddTHH:mm:ss.ffZ")); + _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.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 2f2dab6714..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 @@ -85,8 +85,8 @@ public void When_Writing_A_Message_To_The_PostgreSql_Outbox() //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.ffZ") - .Should().Be(_messageEarliest.Header.TimeStamp.ToString("yyyy-MM-ddTHH:mm:ss.ffZ")); + _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.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 index 8c7fd6fdbc..202a6190fb 100644 --- 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 @@ -89,7 +89,8 @@ public void When_Writing_A_Message_To_The_Outbox() //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.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 503b05dbad..7b0102543b 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 @@ -83,7 +83,8 @@ public void When_Writing_A_Message_To_The_Outbox() //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.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 02deb26b99..943f7a4e2d 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 @@ -79,7 +79,8 @@ public async Task When_Writing_A_Message_To_The_Outbox_Async() _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.fZ") + .Should().Be(_messageEarliest.Header.TimeStamp.ToString("yyyy-MM-ddTHH:mm:ss.fZ")); //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 From 0b406f1ce3d616e1d29e1e178803e43f787aa03c Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Mon, 1 May 2023 15:39:12 +0100 Subject: [PATCH 24/89] Add an example for Kafka binary to make my life easier --- Brighter.sln | 123 +++++++++ samples/WebAPI_Dapper/GreetingsWeb/Startup.cs | 1 + .../GreetingsEntities/Greeting.cs | 38 +++ .../GreetingsEntities.csproj | 7 + .../GreetingsEntities/Person.cs | 26 ++ .../EntityMappers/GreetingsMapper.cs | 18 ++ .../EntityMappers/PersonMapper.cs | 17 ++ .../GreetingsPorts/GreetingsPorts.csproj | 23 ++ .../Handlers/AddGreetingHandlerAsync.cs | 77 ++++++ .../Handlers/AddPersonHandlerAsync.cs | 31 +++ .../Handlers/DeletePersonHandlerAsync.cs | 52 ++++ .../FIndGreetingsForPersonHandlerAsync.cs | 60 +++++ .../Handlers/FindPersonByNameHandlerAsync.cs | 37 +++ .../Policies/GreetingsPolicy.cs | 20 ++ .../GreetingsPorts/Policies/Retry.cs | 44 ++++ .../GreetingsPorts/Requests/AddGreeting.cs | 18 ++ .../GreetingsPorts/Requests/AddPerson.cs | 16 ++ .../GreetingsPorts/Requests/DeletePerson.cs | 16 ++ .../Requests/FindGreetingsForPerson.cs | 15 ++ .../Requests/FindPersonByName.cs | 15 ++ .../GreetingsPorts/Requests/GreetingMade.cs | 15 ++ .../Responses/FindPersonResult.cs | 14 + .../Responses/FindPersonsGreetings.cs | 22 ++ .../Controllers/GreetingsController.cs | 48 ++++ .../Controllers/PeopleController.cs | 60 +++++ .../GreetingsWeb/Database/DatabaseType.cs | 20 ++ .../GreetingsWeb/Database/OutboxExtensions.cs | 63 +++++ .../GreetingsWeb/Database/SchemaCreation.cs | 232 +++++++++++++++++ .../GreetingsWeb/Dockerfile | 10 + .../GreetingsWeb/GreetingsWeb.csproj | 54 ++++ .../Mappers/GreetingMadeMessageMapper.cs | 23 ++ .../GreetingsWeb/Models/NewGreeting.cs | 7 + .../GreetingsWeb/Models/NewPerson.cs | 7 + .../GreetingsWeb/Program.cs | 54 ++++ .../Properties/launchSettings.json | 35 +++ .../GreetingsWeb/Startup.cs | 244 ++++++++++++++++++ .../GreetingsWeb/appsettings.Development.json | 9 + .../GreetingsWeb/appsettings.Production.json | 13 + .../GreetingsWeb/appsettings.json | 10 + .../Greetings_SqliteMigrations.csproj | 13 + .../Migrations/202204221833_InitialCreate.cs | 30 +++ .../Database/DatabaseType.cs | 20 ++ .../Database/SchemaCreation.cs | 229 ++++++++++++++++ .../SalutationAnalytics/Dockerfile | 7 + .../Mappers/GreetingMadeMessageMapper.cs | 19 ++ .../SalutationReceivedMessageMapper.cs | 22 ++ .../SalutationAnalytics/Program.cs | 230 +++++++++++++++++ .../Properties/launchSettings.json | 19 ++ .../SalutationAnalytics.csproj | 44 ++++ .../appsettings.Development.json | 9 + .../appsettings.Production.json | 13 + .../SalutationAnalytics/appsettings.json | 9 + .../SalutationEntities/Salutation.cs | 16 ++ .../SalutationEntities.csproj | 7 + .../EntityMappers/SalutationMapper.cs | 15 ++ .../Handlers/GreetingMadeHandler.cs | 62 +++++ .../SalutationPorts/Policies/Retry.cs | 27 ++ .../Policies/SalutationPolicy.cs | 18 ++ .../SalutationPorts/Requests/GreetingMade.cs | 15 ++ .../Requests/SalutationReceived.cs | 15 ++ .../SalutationPorts/SalutationPorts.csproj | 19 ++ .../202205161812_SqliteMigrations.cs | 20 ++ .../Salutations_SqliteMigrations.csproj | 13 + 63 files changed, 2485 insertions(+) create mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsEntities/Greeting.cs create mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsEntities/GreetingsEntities.csproj create mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsEntities/Person.cs create mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsPorts/EntityMappers/GreetingsMapper.cs create mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsPorts/EntityMappers/PersonMapper.cs create mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsPorts/GreetingsPorts.csproj create mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs create mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/AddPersonHandlerAsync.cs create mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/DeletePersonHandlerAsync.cs create mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/FIndGreetingsForPersonHandlerAsync.cs create mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/FindPersonByNameHandlerAsync.cs create mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsPorts/Policies/GreetingsPolicy.cs create mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsPorts/Policies/Retry.cs create mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsPorts/Requests/AddGreeting.cs create mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsPorts/Requests/AddPerson.cs create mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsPorts/Requests/DeletePerson.cs create mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsPorts/Requests/FindGreetingsForPerson.cs create mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsPorts/Requests/FindPersonByName.cs create mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsPorts/Requests/GreetingMade.cs create mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsPorts/Responses/FindPersonResult.cs create mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsPorts/Responses/FindPersonsGreetings.cs create mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsWeb/Controllers/GreetingsController.cs create mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsWeb/Controllers/PeopleController.cs create mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/DatabaseType.cs create mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/OutboxExtensions.cs create mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/SchemaCreation.cs create mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsWeb/Dockerfile create mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsWeb/GreetingsWeb.csproj create mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsWeb/Mappers/GreetingMadeMessageMapper.cs create mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsWeb/Models/NewGreeting.cs create mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsWeb/Models/NewPerson.cs create mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsWeb/Program.cs create mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsWeb/Properties/launchSettings.json create mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs create mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsWeb/appsettings.Development.json create mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsWeb/appsettings.Production.json create mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsWeb/appsettings.json create mode 100644 samples/WebAPI_Dapper_Kafka/Greetings_SqliteMigrations/Greetings_SqliteMigrations.csproj create mode 100644 samples/WebAPI_Dapper_Kafka/Greetings_SqliteMigrations/Migrations/202204221833_InitialCreate.cs create mode 100644 samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Database/DatabaseType.cs create mode 100644 samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Database/SchemaCreation.cs create mode 100644 samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Dockerfile create mode 100644 samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Mappers/GreetingMadeMessageMapper.cs create mode 100644 samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Mappers/SalutationReceivedMessageMapper.cs create mode 100644 samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Program.cs create mode 100644 samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Properties/launchSettings.json create mode 100644 samples/WebAPI_Dapper_Kafka/SalutationAnalytics/SalutationAnalytics.csproj create mode 100644 samples/WebAPI_Dapper_Kafka/SalutationAnalytics/appsettings.Development.json create mode 100644 samples/WebAPI_Dapper_Kafka/SalutationAnalytics/appsettings.Production.json create mode 100644 samples/WebAPI_Dapper_Kafka/SalutationAnalytics/appsettings.json create mode 100644 samples/WebAPI_Dapper_Kafka/SalutationEntities/Salutation.cs create mode 100644 samples/WebAPI_Dapper_Kafka/SalutationEntities/SalutationEntities.csproj create mode 100644 samples/WebAPI_Dapper_Kafka/SalutationPorts/EntityMappers/SalutationMapper.cs create mode 100644 samples/WebAPI_Dapper_Kafka/SalutationPorts/Handlers/GreetingMadeHandler.cs create mode 100644 samples/WebAPI_Dapper_Kafka/SalutationPorts/Policies/Retry.cs create mode 100644 samples/WebAPI_Dapper_Kafka/SalutationPorts/Policies/SalutationPolicy.cs create mode 100644 samples/WebAPI_Dapper_Kafka/SalutationPorts/Requests/GreetingMade.cs create mode 100644 samples/WebAPI_Dapper_Kafka/SalutationPorts/Requests/SalutationReceived.cs create mode 100644 samples/WebAPI_Dapper_Kafka/SalutationPorts/SalutationPorts.csproj create mode 100644 samples/WebAPI_Dapper_Kafka/Salutations_SqliteMigrations/Migrations/202205161812_SqliteMigrations.cs create mode 100644 samples/WebAPI_Dapper_Kafka/Salutations_SqliteMigrations/Salutations_SqliteMigrations.csproj diff --git a/Brighter.sln b/Brighter.sln index 7fb2343c58..df8dfcaa84 100644 --- a/Brighter.sln +++ b/Brighter.sln @@ -340,6 +340,24 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Paramore.Brighter.Azure.Tes EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Paramore.Brighter.Archive.Azure", "src\Paramore.Brighter.Archive.Azure\Paramore.Brighter.Archive.Azure.csproj", "{F329B6C6-40C2-45BA-A2A8-276ACAFA1867}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "WebAPI_Dapper_Kafka", "WebAPI_Dapper_Kafka", "{36612AF5-23DB-46C0-9EB5-0F21911741F3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GreetingsEntities", "samples\WebAPI_Dapper_Kafka\GreetingsEntities\GreetingsEntities.csproj", "{86AF8EFE-E3E0-46BF-88EB-F9096DAB2DC3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GreetingsPorts", "samples\WebAPI_Dapper_Kafka\GreetingsPorts\GreetingsPorts.csproj", "{83F3CA28-B7DD-4BF0-94B3-F0D35DF55B7A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GreetingsWeb", "samples\WebAPI_Dapper_Kafka\GreetingsWeb\GreetingsWeb.csproj", "{9E311E12-A0CE-4994-92B2-80F3A4BAEB24}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Greetings_SqliteMigrations", "samples\WebAPI_Dapper_Kafka\Greetings_SqliteMigrations\Greetings_SqliteMigrations.csproj", "{80375113-CC4F-4F04-AEC0-97FD8F5887CE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SalutationEntities", "samples\WebAPI_Dapper_Kafka\SalutationEntities\SalutationEntities.csproj", "{B1672D09-15A1-4722-8E63-529E201DC6F6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SalutationPorts", "samples\WebAPI_Dapper_Kafka\SalutationPorts\SalutationPorts.csproj", "{2DBA8A1A-76AC-4F6F-9533-2977AAD9DB15}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SalutationAnalytics", "samples\WebAPI_Dapper_Kafka\SalutationAnalytics\SalutationAnalytics.csproj", "{559212C0-0912-4586-AD6B-462E7A0E738E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Salutations_SqliteMigrations", "samples\WebAPI_Dapper_Kafka\Salutations_SqliteMigrations\Salutations_SqliteMigrations.csproj", "{B50A44CE-5F54-4559-90E0-1AF81655E944}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1958,6 +1976,102 @@ Global {EC046F36-F93F-447A-86EA-F60585232867}.Release|Mixed Platforms.Build.0 = Release|Any CPU {EC046F36-F93F-447A-86EA-F60585232867}.Release|x86.ActiveCfg = Release|Any CPU {EC046F36-F93F-447A-86EA-F60585232867}.Release|x86.Build.0 = Release|Any CPU + {86AF8EFE-E3E0-46BF-88EB-F9096DAB2DC3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {86AF8EFE-E3E0-46BF-88EB-F9096DAB2DC3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {86AF8EFE-E3E0-46BF-88EB-F9096DAB2DC3}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {86AF8EFE-E3E0-46BF-88EB-F9096DAB2DC3}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {86AF8EFE-E3E0-46BF-88EB-F9096DAB2DC3}.Debug|x86.ActiveCfg = Debug|Any CPU + {86AF8EFE-E3E0-46BF-88EB-F9096DAB2DC3}.Debug|x86.Build.0 = Debug|Any CPU + {86AF8EFE-E3E0-46BF-88EB-F9096DAB2DC3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {86AF8EFE-E3E0-46BF-88EB-F9096DAB2DC3}.Release|Any CPU.Build.0 = Release|Any CPU + {86AF8EFE-E3E0-46BF-88EB-F9096DAB2DC3}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {86AF8EFE-E3E0-46BF-88EB-F9096DAB2DC3}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {86AF8EFE-E3E0-46BF-88EB-F9096DAB2DC3}.Release|x86.ActiveCfg = Release|Any CPU + {86AF8EFE-E3E0-46BF-88EB-F9096DAB2DC3}.Release|x86.Build.0 = Release|Any CPU + {83F3CA28-B7DD-4BF0-94B3-F0D35DF55B7A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {83F3CA28-B7DD-4BF0-94B3-F0D35DF55B7A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {83F3CA28-B7DD-4BF0-94B3-F0D35DF55B7A}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {83F3CA28-B7DD-4BF0-94B3-F0D35DF55B7A}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {83F3CA28-B7DD-4BF0-94B3-F0D35DF55B7A}.Debug|x86.ActiveCfg = Debug|Any CPU + {83F3CA28-B7DD-4BF0-94B3-F0D35DF55B7A}.Debug|x86.Build.0 = Debug|Any CPU + {83F3CA28-B7DD-4BF0-94B3-F0D35DF55B7A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {83F3CA28-B7DD-4BF0-94B3-F0D35DF55B7A}.Release|Any CPU.Build.0 = Release|Any CPU + {83F3CA28-B7DD-4BF0-94B3-F0D35DF55B7A}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {83F3CA28-B7DD-4BF0-94B3-F0D35DF55B7A}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {83F3CA28-B7DD-4BF0-94B3-F0D35DF55B7A}.Release|x86.ActiveCfg = Release|Any CPU + {83F3CA28-B7DD-4BF0-94B3-F0D35DF55B7A}.Release|x86.Build.0 = Release|Any CPU + {9E311E12-A0CE-4994-92B2-80F3A4BAEB24}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9E311E12-A0CE-4994-92B2-80F3A4BAEB24}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9E311E12-A0CE-4994-92B2-80F3A4BAEB24}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {9E311E12-A0CE-4994-92B2-80F3A4BAEB24}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {9E311E12-A0CE-4994-92B2-80F3A4BAEB24}.Debug|x86.ActiveCfg = Debug|Any CPU + {9E311E12-A0CE-4994-92B2-80F3A4BAEB24}.Debug|x86.Build.0 = Debug|Any CPU + {9E311E12-A0CE-4994-92B2-80F3A4BAEB24}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9E311E12-A0CE-4994-92B2-80F3A4BAEB24}.Release|Any CPU.Build.0 = Release|Any CPU + {9E311E12-A0CE-4994-92B2-80F3A4BAEB24}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {9E311E12-A0CE-4994-92B2-80F3A4BAEB24}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {9E311E12-A0CE-4994-92B2-80F3A4BAEB24}.Release|x86.ActiveCfg = Release|Any CPU + {9E311E12-A0CE-4994-92B2-80F3A4BAEB24}.Release|x86.Build.0 = Release|Any CPU + {80375113-CC4F-4F04-AEC0-97FD8F5887CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {80375113-CC4F-4F04-AEC0-97FD8F5887CE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {80375113-CC4F-4F04-AEC0-97FD8F5887CE}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {80375113-CC4F-4F04-AEC0-97FD8F5887CE}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {80375113-CC4F-4F04-AEC0-97FD8F5887CE}.Debug|x86.ActiveCfg = Debug|Any CPU + {80375113-CC4F-4F04-AEC0-97FD8F5887CE}.Debug|x86.Build.0 = Debug|Any CPU + {80375113-CC4F-4F04-AEC0-97FD8F5887CE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {80375113-CC4F-4F04-AEC0-97FD8F5887CE}.Release|Any CPU.Build.0 = Release|Any CPU + {80375113-CC4F-4F04-AEC0-97FD8F5887CE}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {80375113-CC4F-4F04-AEC0-97FD8F5887CE}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {80375113-CC4F-4F04-AEC0-97FD8F5887CE}.Release|x86.ActiveCfg = Release|Any CPU + {80375113-CC4F-4F04-AEC0-97FD8F5887CE}.Release|x86.Build.0 = Release|Any CPU + {B1672D09-15A1-4722-8E63-529E201DC6F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B1672D09-15A1-4722-8E63-529E201DC6F6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B1672D09-15A1-4722-8E63-529E201DC6F6}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {B1672D09-15A1-4722-8E63-529E201DC6F6}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {B1672D09-15A1-4722-8E63-529E201DC6F6}.Debug|x86.ActiveCfg = Debug|Any CPU + {B1672D09-15A1-4722-8E63-529E201DC6F6}.Debug|x86.Build.0 = Debug|Any CPU + {B1672D09-15A1-4722-8E63-529E201DC6F6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B1672D09-15A1-4722-8E63-529E201DC6F6}.Release|Any CPU.Build.0 = Release|Any CPU + {B1672D09-15A1-4722-8E63-529E201DC6F6}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {B1672D09-15A1-4722-8E63-529E201DC6F6}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {B1672D09-15A1-4722-8E63-529E201DC6F6}.Release|x86.ActiveCfg = Release|Any CPU + {B1672D09-15A1-4722-8E63-529E201DC6F6}.Release|x86.Build.0 = Release|Any CPU + {2DBA8A1A-76AC-4F6F-9533-2977AAD9DB15}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2DBA8A1A-76AC-4F6F-9533-2977AAD9DB15}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2DBA8A1A-76AC-4F6F-9533-2977AAD9DB15}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {2DBA8A1A-76AC-4F6F-9533-2977AAD9DB15}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {2DBA8A1A-76AC-4F6F-9533-2977AAD9DB15}.Debug|x86.ActiveCfg = Debug|Any CPU + {2DBA8A1A-76AC-4F6F-9533-2977AAD9DB15}.Debug|x86.Build.0 = Debug|Any CPU + {2DBA8A1A-76AC-4F6F-9533-2977AAD9DB15}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2DBA8A1A-76AC-4F6F-9533-2977AAD9DB15}.Release|Any CPU.Build.0 = Release|Any CPU + {2DBA8A1A-76AC-4F6F-9533-2977AAD9DB15}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {2DBA8A1A-76AC-4F6F-9533-2977AAD9DB15}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {2DBA8A1A-76AC-4F6F-9533-2977AAD9DB15}.Release|x86.ActiveCfg = Release|Any CPU + {2DBA8A1A-76AC-4F6F-9533-2977AAD9DB15}.Release|x86.Build.0 = Release|Any CPU + {559212C0-0912-4586-AD6B-462E7A0E738E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {559212C0-0912-4586-AD6B-462E7A0E738E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {559212C0-0912-4586-AD6B-462E7A0E738E}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {559212C0-0912-4586-AD6B-462E7A0E738E}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {559212C0-0912-4586-AD6B-462E7A0E738E}.Debug|x86.ActiveCfg = Debug|Any CPU + {559212C0-0912-4586-AD6B-462E7A0E738E}.Debug|x86.Build.0 = Debug|Any CPU + {559212C0-0912-4586-AD6B-462E7A0E738E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {559212C0-0912-4586-AD6B-462E7A0E738E}.Release|Any CPU.Build.0 = Release|Any CPU + {559212C0-0912-4586-AD6B-462E7A0E738E}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {559212C0-0912-4586-AD6B-462E7A0E738E}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {559212C0-0912-4586-AD6B-462E7A0E738E}.Release|x86.ActiveCfg = Release|Any CPU + {559212C0-0912-4586-AD6B-462E7A0E738E}.Release|x86.Build.0 = Release|Any CPU + {B50A44CE-5F54-4559-90E0-1AF81655E944}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B50A44CE-5F54-4559-90E0-1AF81655E944}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B50A44CE-5F54-4559-90E0-1AF81655E944}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {B50A44CE-5F54-4559-90E0-1AF81655E944}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {B50A44CE-5F54-4559-90E0-1AF81655E944}.Debug|x86.ActiveCfg = Debug|Any CPU + {B50A44CE-5F54-4559-90E0-1AF81655E944}.Debug|x86.Build.0 = Debug|Any CPU + {B50A44CE-5F54-4559-90E0-1AF81655E944}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B50A44CE-5F54-4559-90E0-1AF81655E944}.Release|Any CPU.Build.0 = Release|Any CPU + {B50A44CE-5F54-4559-90E0-1AF81655E944}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {B50A44CE-5F54-4559-90E0-1AF81655E944}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {B50A44CE-5F54-4559-90E0-1AF81655E944}.Release|x86.ActiveCfg = Release|Any CPU + {B50A44CE-5F54-4559-90E0-1AF81655E944}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -2073,6 +2187,15 @@ Global {D7361C21-AB4D-4E82-8094-FB86C2ED1800} = {65F8C2DE-0CB6-4102-8187-A247F1D5D3D7} {18742337-075A-40D6-B67F-91F5894A50C3} = {65F8C2DE-0CB6-4102-8187-A247F1D5D3D7} {AA2AA086-9B8A-4910-A793-E92B1E352351} = {329736D2-BF92-4D06-A7BF-19F4B6B64EDD} + {36612AF5-23DB-46C0-9EB5-0F21911741F3} = {235DE1F1-E71B-4817-8E27-3B34FF006E4C} + {86AF8EFE-E3E0-46BF-88EB-F9096DAB2DC3} = {36612AF5-23DB-46C0-9EB5-0F21911741F3} + {83F3CA28-B7DD-4BF0-94B3-F0D35DF55B7A} = {36612AF5-23DB-46C0-9EB5-0F21911741F3} + {9E311E12-A0CE-4994-92B2-80F3A4BAEB24} = {36612AF5-23DB-46C0-9EB5-0F21911741F3} + {80375113-CC4F-4F04-AEC0-97FD8F5887CE} = {36612AF5-23DB-46C0-9EB5-0F21911741F3} + {B1672D09-15A1-4722-8E63-529E201DC6F6} = {36612AF5-23DB-46C0-9EB5-0F21911741F3} + {2DBA8A1A-76AC-4F6F-9533-2977AAD9DB15} = {36612AF5-23DB-46C0-9EB5-0F21911741F3} + {559212C0-0912-4586-AD6B-462E7A0E738E} = {36612AF5-23DB-46C0-9EB5-0F21911741F3} + {B50A44CE-5F54-4559-90E0-1AF81655E944} = {36612AF5-23DB-46C0-9EB5-0F21911741F3} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {8B7C7E31-2E32-4E0D-9426-BC9AF22E9F4C} diff --git a/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs b/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs index 5a16c8a4a3..ebb07934db 100644 --- a/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs +++ b/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs @@ -74,6 +74,7 @@ public void ConfigureServices(IServiceCollection services) ConfigureMigration(services); ConfigureDapper(services); ConfigureBrighter(services); + ConfigureBrighter(services); ConfigureDarker(services); } diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsEntities/Greeting.cs b/samples/WebAPI_Dapper_Kafka/GreetingsEntities/Greeting.cs new file mode 100644 index 0000000000..0b73f5bdfd --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/GreetingsEntities/Greeting.cs @@ -0,0 +1,38 @@ +using System; +using System.Runtime.CompilerServices; + +namespace GreetingsEntities +{ + public class Greeting + { + public long Id { get; set; } + public string Message { get; set; } + //public Person Recipient { get; set; } + public long RecipientId { get; set; } + + public Greeting() { /*Required by Dapperextensions*/} + + public Greeting(string message) + { + Message = message; + } + + public Greeting(string message, Person recipient) + { + Message = message; + RecipientId = recipient.Id; + } + + public Greeting(int id, string message, Person recipient) + { + Id = id; + Message = message; + RecipientId = recipient.Id; + } + + public string Greet() + { + return $"{Message}!"; + } + } +} diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsEntities/GreetingsEntities.csproj b/samples/WebAPI_Dapper_Kafka/GreetingsEntities/GreetingsEntities.csproj new file mode 100644 index 0000000000..4f444d8c8b --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/GreetingsEntities/GreetingsEntities.csproj @@ -0,0 +1,7 @@ + + + + net6.0 + + + diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsEntities/Person.cs b/samples/WebAPI_Dapper_Kafka/GreetingsEntities/Person.cs new file mode 100644 index 0000000000..10dce975d4 --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/GreetingsEntities/Person.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; + +namespace GreetingsEntities +{ + public class Person + { + public byte[] TimeStamp { get; set; } + public long Id { get; set; } + public string Name { get; set; } + public IList Greetings { get; set; } = new List(); + + public Person(){ /*Required for DapperExtensions*/} + + public Person(string name) + { + Name = name; + } + + public Person(int id, string name) + { + Id = id; + Name = name; + } + } +} diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/EntityMappers/GreetingsMapper.cs b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/EntityMappers/GreetingsMapper.cs new file mode 100644 index 0000000000..ede3108fab --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/EntityMappers/GreetingsMapper.cs @@ -0,0 +1,18 @@ +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_Kafka/GreetingsPorts/EntityMappers/PersonMapper.cs b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/EntityMappers/PersonMapper.cs new file mode 100644 index 0000000000..7a29337bbb --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/EntityMappers/PersonMapper.cs @@ -0,0 +1,17 @@ +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_Kafka/GreetingsPorts/GreetingsPorts.csproj b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/GreetingsPorts.csproj new file mode 100644 index 0000000000..61b428817b --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/GreetingsPorts.csproj @@ -0,0 +1,23 @@ + + + + net6.0 + + + + + + + + + + + + + + + + + + + diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs new file mode 100644 index 0000000000..c4f768a767 --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using DapperExtensions; +using DapperExtensions.Predicate; +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 +{ + public class AddGreetingHandlerAsync: RequestHandlerAsync + { + private readonly IAmACommandProcessor _postBox; + private readonly ILogger _logger; + private readonly SqliteDapperConnectionProvider _uow; + + + public AddGreetingHandlerAsync(IAmABoxTransactionConnectionProvider uow, 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 + _postBox = postBox; + _logger = logger; + } + + [RequestLoggingAsync(0, HandlerTiming.Before)] + [UsePolicyAsync(step:1, policy: Policies.Retry.EXPONENTIAL_RETRYPOLICYASYNC)] + public override async Task HandleAsync(AddGreeting addGreeting, CancellationToken cancellationToken = default) + { + var posts = new List(); + + //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(); + 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); + + //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); + } + 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(addGreeting, cancellationToken); + } + + //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 + await _postBox.ClearOutboxAsync(posts, cancellationToken:cancellationToken); + + return await base.HandleAsync(addGreeting, cancellationToken); + } + } +} diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/AddPersonHandlerAsync.cs b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/AddPersonHandlerAsync.cs new file mode 100644 index 0000000000..21baf781e5 --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/AddPersonHandlerAsync.cs @@ -0,0 +1,31 @@ +using System.Threading; +using System.Threading.Tasks; +using DapperExtensions; +using GreetingsEntities; +using GreetingsPorts.Requests; +using Paramore.Brighter; +using Paramore.Brighter.Dapper; +using Paramore.Brighter.Logging.Attributes; +using Paramore.Brighter.Policies.Attributes; + +namespace GreetingsPorts.Handlers +{ + public class AddPersonHandlerAsync : RequestHandlerAsync + { + private readonly IUnitOfWork _uow; + + public AddPersonHandlerAsync(IUnitOfWork uow) + { + _uow = uow; + } + + [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)); + + return await base.HandleAsync(addPerson, cancellationToken); + } + } +} diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/DeletePersonHandlerAsync.cs b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/DeletePersonHandlerAsync.cs new file mode 100644 index 0000000000..f3b300dcd7 --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/DeletePersonHandlerAsync.cs @@ -0,0 +1,52 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using DapperExtensions; +using DapperExtensions.Predicate; +using GreetingsEntities; +using GreetingsPorts.Requests; +using Paramore.Brighter; +using Paramore.Brighter.Dapper; +using Paramore.Brighter.Logging.Attributes; +using Paramore.Brighter.Policies.Attributes; + +namespace GreetingsPorts.Handlers +{ + public class DeletePersonHandlerAsync : RequestHandlerAsync + { + private readonly IUnitOfWork _uow; + + public DeletePersonHandlerAsync(IUnitOfWork uow) + { + _uow = uow; + } + + [RequestLoggingAsync(0, HandlerTiming.Before)] + [UsePolicyAsync(step:1, policy: Policies.Retry.EXPONENTIAL_RETRYPOLICYASYNC)] + public async override Task HandleAsync(DeletePerson deletePerson, CancellationToken cancellationToken = default) + { + var tx = await _uow.BeginOrGetTransactionAsync(cancellationToken); + try + { + + var searchbyName = Predicates.Field(p => p.Name, Operator.Eq, deletePerson.Name); + var people = await _uow.Database.GetListAsync(searchbyName, transaction: tx); + var person = people.Single(); + + var deleteById = Predicates.Field(g => g.RecipientId, Operator.Eq, person.Id); + await _uow.Database.DeleteAsync(deleteById, tx); + + await tx.CommitAsync(cancellationToken); + } + catch (Exception) + { + //it went wrong, rollback the entity change and the downstream message + await tx.RollbackAsync(cancellationToken); + return await base.HandleAsync(deletePerson, cancellationToken); + } + + return await base.HandleAsync(deletePerson, cancellationToken); + } + } +} diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/FIndGreetingsForPersonHandlerAsync.cs b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/FIndGreetingsForPersonHandlerAsync.cs new file mode 100644 index 0000000000..d908df5c2c --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/FIndGreetingsForPersonHandlerAsync.cs @@ -0,0 +1,60 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Dapper; +using GreetingsEntities; +using GreetingsPorts.Policies; +using GreetingsPorts.Requests; +using GreetingsPorts.Responses; +using Paramore.Brighter.Dapper; +using Paramore.Darker; +using Paramore.Darker.Policies; +using Paramore.Darker.QueryLogging; + +namespace GreetingsPorts.Handlers +{ + public class FIndGreetingsForPersonHandlerAsync : QueryHandlerAsync + { + private readonly IUnitOfWork _uow; + + public FIndGreetingsForPersonHandlerAsync(IUnitOfWork uow) + { + _uow = uow; + } + + [QueryLogging(0)] + [RetryableQuery(1, Retry.EXPONENTIAL_RETRYPOLICYASYNC)] + public override async Task ExecuteAsync(FindGreetingsForPerson query, CancellationToken cancellationToken = new CancellationToken()) + { + //Retrieving parent and child is a bit tricky with Dapper. From raw SQL We wget back a set that has a row-per-child. We need to turn that + //into one entity per parent, with a collection of children. To do that we bring everything back into memory, group by parent id and collate all + //the children for that group. + + 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) => + { + person.Greetings.Add(greeting); + return person; + }, splitOn: "Id"); + + var peopleGreetings = people.GroupBy(p => p.Id).Select(grp => + { + var groupedPerson = grp.First(); + groupedPerson.Greetings = grp.Select(p => p.Greetings.Single()).ToList(); + return groupedPerson; + }); + + var person = peopleGreetings.Single(); + + return new FindPersonsGreetings + { + Name = person.Name, + Greetings = person.Greetings.Select(g => new Salutation(g.Greet())) + }; + + } + + } +} diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/FindPersonByNameHandlerAsync.cs b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/FindPersonByNameHandlerAsync.cs new file mode 100644 index 0000000000..1ab5898541 --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/FindPersonByNameHandlerAsync.cs @@ -0,0 +1,37 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using DapperExtensions; +using DapperExtensions.Predicate; +using GreetingsEntities; +using GreetingsPorts.Policies; +using GreetingsPorts.Requests; +using GreetingsPorts.Responses; +using Paramore.Brighter.Dapper; +using Paramore.Darker; +using Paramore.Darker.Policies; +using Paramore.Darker.QueryLogging; + +namespace GreetingsPorts.Handlers +{ + public class FindPersonByNameHandlerAsync : QueryHandlerAsync + { + private readonly IUnitOfWork _uow; + + public FindPersonByNameHandlerAsync(IUnitOfWork uow) + { + _uow = uow; + } + + [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(); + + return new FindPersonResult(person); + } + } +} diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Policies/GreetingsPolicy.cs b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Policies/GreetingsPolicy.cs new file mode 100644 index 0000000000..ed2afaec91 --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Policies/GreetingsPolicy.cs @@ -0,0 +1,20 @@ +using Paramore.Brighter; + +namespace GreetingsPorts.Policies +{ + public class GreetingsPolicy : DefaultPolicy + { + public GreetingsPolicy() + { + AddGreetingsPolicies(); + } + + private void AddGreetingsPolicies() + { + Add(Retry.RETRYPOLICYASYNC, Retry.GetSimpleHandlerRetryPolicy()); + Add(Retry.EXPONENTIAL_RETRYPOLICYASYNC, Retry.GetExponentialHandlerRetryPolicy()); + Add(Paramore.Darker.Policies.Constants.RetryPolicyName, Retry.GetDefaultRetryPolicy()); + Add(Paramore.Darker.Policies.Constants.CircuitBreakerPolicyName, Retry.GetDefaultCircuitBreakerPolicy()); + } + } +} diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Policies/Retry.cs b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Policies/Retry.cs new file mode 100644 index 0000000000..df39319263 --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Policies/Retry.cs @@ -0,0 +1,44 @@ +using System; +using Polly; +using Polly.CircuitBreaker; +using Polly.Contrib.WaitAndRetry; +using Polly.Retry; + +namespace GreetingsPorts.Policies +{ + public static class Retry + { + public const string RETRYPOLICYASYNC = "GreetingsPorts.Policies.RetryPolicyAsync"; + public const string EXPONENTIAL_RETRYPOLICYASYNC = "GreetingsPorts.Policies.ExponenttialRetryPolicyAsync"; + + public static AsyncRetryPolicy GetSimpleHandlerRetryPolicy() + { + return Policy.Handle().WaitAndRetryAsync(new[] + { + TimeSpan.FromMilliseconds(50), TimeSpan.FromMilliseconds(100), TimeSpan.FromMilliseconds(150) + }); + } + + public static AsyncRetryPolicy GetExponentialHandlerRetryPolicy() + { + var delay = Backoff.ExponentialBackoff(TimeSpan.FromMilliseconds(100), retryCount: 5, fastFirst: true); + return Policy.Handle().WaitAndRetryAsync(delay); + } + + public static AsyncRetryPolicy GetDefaultRetryPolicy() + { + return Policy.Handle() + .WaitAndRetryAsync(new[] + { + TimeSpan.FromMilliseconds(50), TimeSpan.FromMilliseconds(100), TimeSpan.FromMilliseconds(150) + }); + } + + public static AsyncCircuitBreakerPolicy GetDefaultCircuitBreakerPolicy() + { + return Policy.Handle().CircuitBreakerAsync( + 1, TimeSpan.FromMilliseconds(500) + ); + } + } +} diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Requests/AddGreeting.cs b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Requests/AddGreeting.cs new file mode 100644 index 0000000000..0e10dc0697 --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Requests/AddGreeting.cs @@ -0,0 +1,18 @@ +using System; +using Paramore.Brighter; + +namespace GreetingsPorts.Requests +{ + public class AddGreeting : Command + { + public string Name { get; } + public string Greeting { get; } + + public AddGreeting(string name, string greeting) + : base(Guid.NewGuid()) + { + Name = name; + Greeting = greeting; + } + } +} diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Requests/AddPerson.cs b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Requests/AddPerson.cs new file mode 100644 index 0000000000..2e86c6d7f3 --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Requests/AddPerson.cs @@ -0,0 +1,16 @@ +using System; +using Paramore.Brighter; + +namespace GreetingsPorts.Requests +{ + public class AddPerson : Command + { + public string Name { get; set; } + + public AddPerson(string name) + : base(Guid.NewGuid()) + { + Name = name; + } + } +} diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Requests/DeletePerson.cs b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Requests/DeletePerson.cs new file mode 100644 index 0000000000..d09f213ba9 --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Requests/DeletePerson.cs @@ -0,0 +1,16 @@ +using System; +using Paramore.Brighter; + +namespace GreetingsPorts.Requests +{ + public class DeletePerson : Command + { + public string Name { get; } + + public DeletePerson(string name) + : base(Guid.NewGuid()) + { + Name = name; + } + } +} diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Requests/FindGreetingsForPerson.cs b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Requests/FindGreetingsForPerson.cs new file mode 100644 index 0000000000..b7ef165f22 --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Requests/FindGreetingsForPerson.cs @@ -0,0 +1,15 @@ +using GreetingsPorts.Responses; +using Paramore.Darker; + +namespace GreetingsPorts.Requests +{ + public class FindGreetingsForPerson : IQuery + { + public string Name { get; } + + public FindGreetingsForPerson(string name) + { + Name = name; + } + } +} diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Requests/FindPersonByName.cs b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Requests/FindPersonByName.cs new file mode 100644 index 0000000000..bc571978c0 --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Requests/FindPersonByName.cs @@ -0,0 +1,15 @@ +using GreetingsPorts.Responses; +using Paramore.Darker; + +namespace GreetingsPorts.Requests +{ + public class FindPersonByName : IQuery + { + public string Name { get; } + + public FindPersonByName(string name) + { + Name = name; + } + } +} diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Requests/GreetingMade.cs b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Requests/GreetingMade.cs new file mode 100644 index 0000000000..4f8ea156a2 --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Requests/GreetingMade.cs @@ -0,0 +1,15 @@ +using System; +using Paramore.Brighter; + +namespace GreetingsPorts.Requests +{ + public class GreetingMade : Event + { + public string Greeting { get; set; } + + public GreetingMade(string greeting) : base(Guid.NewGuid()) + { + Greeting = greeting; + } + } +} diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Responses/FindPersonResult.cs b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Responses/FindPersonResult.cs new file mode 100644 index 0000000000..ea50242c8c --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Responses/FindPersonResult.cs @@ -0,0 +1,14 @@ +using GreetingsEntities; + +namespace GreetingsPorts.Responses +{ + public class FindPersonResult + { + public string Name { get; private set; } + public FindPersonResult(Person person) + { + Name = person.Name; + } + + } +} diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Responses/FindPersonsGreetings.cs b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Responses/FindPersonsGreetings.cs new file mode 100644 index 0000000000..6ab530b4ba --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Responses/FindPersonsGreetings.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; + +namespace GreetingsPorts.Responses +{ + public class FindPersonsGreetings + { + public string Name { get; set; } + public IEnumerable Greetings { get;set; } + + } + + public class Salutation + { + public string Words { get; set; } + + public Salutation(string words) + { + Words = words; + } + + } +} diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Controllers/GreetingsController.cs b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Controllers/GreetingsController.cs new file mode 100644 index 0000000000..10bbb3ad1e --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Controllers/GreetingsController.cs @@ -0,0 +1,48 @@ +using System.Threading.Tasks; +using GreetingsPorts.Requests; +using GreetingsPorts.Responses; +using GreetingsWeb.Models; +using Microsoft.AspNetCore.Mvc; +using Paramore.Brighter; +using Paramore.Darker; + +namespace GreetingsWeb.Controllers +{ + [ApiController] + [Route("[controller]")] + public class GreetingsController : Controller + { + private readonly IAmACommandProcessor _commandProcessor; + private readonly IQueryProcessor _queryProcessor; + + public GreetingsController(IAmACommandProcessor commandProcessor, IQueryProcessor queryProcessor) + { + _commandProcessor = commandProcessor; + _queryProcessor = queryProcessor; + } + + [Route("{name}")] + [HttpGet] + public async Task Get(string name) + { + var personsGreetings = await _queryProcessor.ExecuteAsync(new FindGreetingsForPerson(name)); + + if (personsGreetings == null) return new NotFoundResult(); + + return Ok(personsGreetings); + } + + [Route("{name}/new")] + [HttpPost] + public async Task> Post(string name, NewGreeting newGreeting) + { + await _commandProcessor.SendAsync(new AddGreeting(name, newGreeting.Greeting)); + + var personsGreetings = await _queryProcessor.ExecuteAsync(new FindGreetingsForPerson(name)); + + if (personsGreetings == null) return new NotFoundResult(); + + return Ok(personsGreetings); + } + } +} diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Controllers/PeopleController.cs b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Controllers/PeopleController.cs new file mode 100644 index 0000000000..0ce06da254 --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Controllers/PeopleController.cs @@ -0,0 +1,60 @@ +using System; +using System.Threading.Tasks; +using GreetingsPorts.Requests; +using GreetingsPorts.Responses; +using GreetingsWeb.Models; +using Microsoft.AspNetCore.Mvc; +using Paramore.Brighter; +using Paramore.Darker; + +namespace GreetingsWeb.Controllers +{ + [ApiController] + [Route("[controller]")] + public class PeopleController : Controller + { + private readonly IAmACommandProcessor _commandProcessor; + private readonly IQueryProcessor _queryProcessor; + + public PeopleController(IAmACommandProcessor commandProcessor, IQueryProcessor queryProcessor) + { + _commandProcessor = commandProcessor; + _queryProcessor = queryProcessor; + } + + + [Route("{name}")] + [HttpGet] + public async Task> Get(string name) + { + var foundPerson = await _queryProcessor.ExecuteAsync(new FindPersonByName(name)); + + if (foundPerson == null) return new NotFoundResult(); + + return Ok(foundPerson); + } + + [Route("{name}")] + [HttpDelete] + public async Task Delete(string name) + { + await _commandProcessor.SendAsync(new DeletePerson(name)); + + return Ok(); + } + + [Route("new")] + [HttpPost] + public async Task> Post(NewPerson newPerson) + { + await _commandProcessor.SendAsync(new AddPerson(newPerson.Name)); + + var addedPerson = await _queryProcessor.ExecuteAsync(new FindPersonByName(newPerson.Name)); + + if (addedPerson == null) return new NotFoundResult(); + + return Ok(addedPerson); + } + + } +} diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/DatabaseType.cs b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/DatabaseType.cs new file mode 100644 index 0000000000..29118a556c --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/DatabaseType.cs @@ -0,0 +1,20 @@ +namespace GreetingsWeb.Database; + +public static class DatabaseGlobals +{ + //environment string key + public const string DATABASE_TYPE_ENV = "BRIGHTER_GREETINGS_DATABASE"; + + public const string MYSQL = "MySQL"; + public const string MSSQL = "MsSQL"; + public const string POSTGRESSQL = "PostgresSQL"; + public const string SQLITE = "Sqlite"; +} + +public enum DatabaseType +{ + MySql, + MsSql, + Postgres, + Sqlite +} diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/OutboxExtensions.cs b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/OutboxExtensions.cs new file mode 100644 index 0000000000..80f172a2be --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/OutboxExtensions.cs @@ -0,0 +1,63 @@ +using System; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Paramore.Brighter.Extensions.DependencyInjection; +using Paramore.Brighter.Extensions.Hosting; +using Paramore.Brighter.MySql; +using Paramore.Brighter.MySql.Dapper; +using Paramore.Brighter.Outbox.MySql; +using Paramore.Brighter.Outbox.Sqlite; +using Paramore.Brighter.Sqlite; +using Paramore.Brighter.Sqlite.Dapper; +using UnitOfWork = Paramore.Brighter.MySql.Dapper.UnitOfWork; + +namespace GreetingsWeb.Database; + +public static class OutboxExtensions +{ + public static IBrighterBuilder AddOutbox(this IBrighterBuilder brighterBuilder, IWebHostEnvironment env, DatabaseType databaseType, + string dbConnectionString, string outBoxTableName) + { + if (env.IsDevelopment()) + { + AddSqliteOutBox(brighterBuilder, dbConnectionString, outBoxTableName); + } + else + { + switch (databaseType) + { + case DatabaseType.MySql: + AddMySqlOutbox(brighterBuilder, dbConnectionString, outBoxTableName); + break; + default: + throw new InvalidOperationException("Unknown Db type for Outbox configuration"); + } + } + 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 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; + }); + } +} diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/SchemaCreation.cs b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/SchemaCreation.cs new file mode 100644 index 0000000000..d628c0bdb4 --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/SchemaCreation.cs @@ -0,0 +1,232 @@ +using System; +using System.Data; +using System.Data.Common; +using FluentMigrator.Runner; +using Microsoft.AspNetCore.Hosting; +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 Paramore.Brighter.Outbox.MySql; +using Paramore.Brighter.Outbox.Sqlite; +using Polly; + +namespace GreetingsWeb.Database +{ + public static class SchemaCreation + { + private const string OUTBOX_TABLE_NAME = "Outbox"; + + public static IHost CheckDbIsUp(this IHost webHost) + { + using var scope = webHost.Services.CreateScope(); + + var services = scope.ServiceProvider; + var env = services.GetService(); + var config = services.GetService(); + string connectionString = DbServerConnectionString(config, env); + + //We don't check in development as using Sqlite + if (env.IsDevelopment()) return webHost; + + WaitToConnect(connectionString); + CreateDatabaseIfNotExists(GetDbConnection(GetDatabaseType(config), connectionString)); + + return webHost; + } + + public static IHost MigrateDatabase(this IHost webHost) + { + using (var scope = webHost.Services.CreateScope()) + { + var services = scope.ServiceProvider; + + try + { + var runner = services.GetRequiredService(); + runner.ListMigrations(); + runner.MigrateUp(); + } + catch (Exception ex) + { + var logger = services.GetRequiredService>(); + logger.LogError(ex, "An error occurred while migrating the database."); + throw; + } + } + + 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) + { + //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(); + } + + + private static void CreateOutbox(IConfiguration config, IWebHostEnvironment env) + { + try + { + var connectionString = DbConnectionString(config, env); + + if (env.IsDevelopment()) + CreateOutboxDevelopment(connectionString); + else + CreateOutboxProduction(GetDatabaseType(config), connectionString); + } + 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) + { + 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(); + } + + private static void CreateOutboxProduction(DatabaseType databaseType, string connectionString) + { + switch (databaseType) + { + case DatabaseType.MySql: + CreateOutboxMySql(connectionString); + break; + default: + throw new InvalidOperationException("Could not create instance of Outbox for unknown Db type"); + } + } + + private static void CreateOutboxMySql(string connectionString) + { + 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; + + if (exists) return; + + using var command = sqlConnection.CreateCommand(); + command.CommandText = MySqlOutboxBuilder.GetDDL(OUTBOX_TABLE_NAME); + command.ExecuteScalar(); + } + + private static string DbConnectionString(IConfiguration config, IWebHostEnvironment env) + { + //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)); + } + + private static string GetDevDbConnectionString() + { + return "Filename=Greetings.db;Cache=Shared"; + } + + private static string DbServerConnectionString(IConfiguration config, IWebHostEnvironment env) + { + return env.IsDevelopment() ? GetDevConnectionString() : GetProductionConnectionString(config, GetDatabaseType(config)); + } + + private static string GetDevConnectionString() + { + return "Filename=Greetings.db;Cache=Shared"; + } + + private static DbConnection GetDbConnection(DatabaseType databaseType, string connectionString) + { + return databaseType switch + { + DatabaseType.MySql => new MySqlConnection(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("GreetingsMySql"), + _ => throw new InvalidOperationException("Could not determine the database type") + }; + } + + private static string GetProductionDbConnectionString(IConfiguration config, DatabaseType databaseType) + { + return databaseType switch + { + DatabaseType.MySql => config.GetConnectionString("GreetingsMySql"), + _ => 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(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 = new MySqlConnection(connectionString); + conn.Open(); + }); + } + } +} diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Dockerfile b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Dockerfile new file mode 100644 index 0000000000..b6633e7655 --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Dockerfile @@ -0,0 +1,10 @@ +FROM mcr.microsoft.com/dotnet/aspnet:5.0-buster-slim + +WORKDIR /app +COPY out/ . + +# Expose the port +EXPOSE 5000 +ENV ASPNETCORE_URLS=http://+:5000 +#run the site +ENTRYPOINT ["dotnet", "GreetingsWeb.dll"] diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/GreetingsWeb.csproj b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/GreetingsWeb.csproj new file mode 100644 index 0000000000..3d30f65e5d --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/GreetingsWeb.csproj @@ -0,0 +1,54 @@ + + + + net6.0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <_ContentIncludedByDefault Remove="out\web.config" /> + <_ContentIncludedByDefault Remove="out\appsettings.Development.json" /> + <_ContentIncludedByDefault Remove="out\appsettings.json" /> + <_ContentIncludedByDefault Remove="out\appsettings.Production.json" /> + <_ContentIncludedByDefault Remove="out\GreetingsAdapters.deps.json" /> + <_ContentIncludedByDefault Remove="out\GreetingsAdapters.runtimeconfig.json" /> + + + diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Mappers/GreetingMadeMessageMapper.cs b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Mappers/GreetingMadeMessageMapper.cs new file mode 100644 index 0000000000..19dc67d0a8 --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Mappers/GreetingMadeMessageMapper.cs @@ -0,0 +1,23 @@ +using System.Text.Json; +using GreetingsPorts.Requests; +using Paramore.Brighter; + + +namespace GreetingsWeb.Mappers +{ + public class GreetingMadeMessageMapper : IAmAMessageMapper + { + public Message MapToMessage(GreetingMade request) + { + var header = new MessageHeader(messageId: request.Id, topic: "GreetingMade", messageType: MessageType.MT_EVENT); + var body = new MessageBody(System.Text.Json.JsonSerializer.Serialize(request, new JsonSerializerOptions(JsonSerializerDefaults.General))); + var message = new Message(header, body); + return message; + } + + public GreetingMade MapToRequest(Message message) + { + throw new System.NotImplementedException(); + } + } +} diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Models/NewGreeting.cs b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Models/NewGreeting.cs new file mode 100644 index 0000000000..3870feb5fd --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Models/NewGreeting.cs @@ -0,0 +1,7 @@ +namespace GreetingsWeb.Models +{ + public class NewGreeting + { + public string Greeting { get; set; } + } +} diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Models/NewPerson.cs b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Models/NewPerson.cs new file mode 100644 index 0000000000..19dc16dfb8 --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Models/NewPerson.cs @@ -0,0 +1,7 @@ +namespace GreetingsWeb.Models +{ + public class NewPerson + { + public string Name { get; set; } + } +} diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Program.cs b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Program.cs new file mode 100644 index 0000000000..749522914a --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Program.cs @@ -0,0 +1,54 @@ +using System.IO; +using FluentMigrator.Runner; +using GreetingsWeb.Database; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace GreetingsWeb +{ + public class Program + { + public static void Main(string[] args) + { + var host = CreateHostBuilder(args).Build(); + + host.CheckDbIsUp(); + host.MigrateDatabase(); + host.CreateOutbox(); + + host.Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureAppConfiguration((context, configBuilder) => + { + var env = context.HostingEnvironment; + configBuilder.AddJsonFile("appsettings.json", optional: false); + configBuilder.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true); + configBuilder.AddEnvironmentVariables(prefix:"BRIGHTER_"); + }) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseKestrel(); + webBuilder.UseContentRoot(Directory.GetCurrentDirectory()); + webBuilder.CaptureStartupErrors(true); + webBuilder.UseSetting("detailedErrors", "true"); + webBuilder.ConfigureLogging((hostingContext, logging) => + { + logging.AddConsole(); + logging.AddDebug(); + logging.AddFluentMigratorConsole(); + }); + webBuilder.UseDefaultServiceProvider((context, options) => + { + var isDevelopment = context.HostingEnvironment.IsDevelopment(); + options.ValidateScopes = isDevelopment; + options.ValidateOnBuild = isDevelopment; + }); + webBuilder.UseStartup(); + }); + } +} diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Properties/launchSettings.json b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Properties/launchSettings.json new file mode 100644 index 0000000000..1186c52e1f --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Properties/launchSettings.json @@ -0,0 +1,35 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:8854", + "sslPort": 44355 + } + }, + "profiles": { + "Development": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "BRIGHTER_GREETINGS_DATABASE": "Sqlite" + } + }, + "Production": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Production", + "BRIGHTER_GREETINGS_DATABASE": "MySQL" + } + } + } +} diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs new file mode 100644 index 0000000000..ebb07934db --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs @@ -0,0 +1,244 @@ +using System; +using DapperExtensions; +using DapperExtensions.Sql; +using FluentMigrator.Runner; +using Greetings_MySqlMigrations.Migrations; +using Greetings_SqliteMigrations.Migrations; +using GreetingsPorts.EntityMappers; +using GreetingsPorts.Handlers; +using GreetingsPorts.Policies; +using GreetingsWeb.Database; +using Hellang.Middleware.ProblemDetails; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.OpenApi.Models; +using Paramore.Brighter; +using Paramore.Brighter.Dapper; +using Paramore.Brighter.Extensions.DependencyInjection; +using Paramore.Brighter.MessagingGateway.RMQ; +using Paramore.Darker.AspNetCore; +using Paramore.Darker.Policies; +using Paramore.Darker.QueryLogging; + +namespace GreetingsWeb +{ + public class Startup + { + private const string _outBoxTableName = "Outbox"; + private IWebHostEnvironment _env; + + public Startup(IConfiguration configuration, IWebHostEnvironment env) + { + Configuration = configuration; + _env = env; + } + + public IConfiguration Configuration { get; } + + // 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(); + app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "GreetingsAPI v1")); + } + + app.UseHttpsRedirection(); + app.UseRouting(); + + app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); + } + + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddMvcCore().AddApiExplorer(); + services.AddControllers(options => + { + options.RespectBrowserAcceptHeader = true; + }) + .AddXmlSerializerFormatters(); + services.AddProblemDetails(); + services.AddSwaggerGen(c => + { + c.SwaggerDoc("v1", new OpenApiInfo { Title = "GreetingsAPI", Version = "v1" }); + }); + + ConfigureMigration(services); + ConfigureDapper(services); + ConfigureBrighter(services); + ConfigureBrighter(services); + ConfigureDarker(services); + } + + private void ConfigureMigration(IServiceCollection services) + { + if (_env.IsDevelopment()) + { + services + .AddFluentMigratorCore() + .ConfigureRunner(c => + { + c.AddSQLite() + .WithGlobalConnectionString(DbConnectionString()) + .ScanIn(typeof(SqlliteInitialCreate).Assembly).For.Migrations(); + }); + } + else + { + ConfigureProductionDatabase(GetDatabaseType(), services); + } + } + + private void ConfigureProductionDatabase(DatabaseType databaseType, IServiceCollection services) + { + switch (databaseType) + { + case DatabaseType.MySql: + ConfigureMySql(services); + break; + default: + throw new ArgumentOutOfRangeException(nameof(databaseType), "Database type is not supported"); + } + } + + private void ConfigureMySql(IServiceCollection services) + { + services + .AddFluentMigratorCore() + .ConfigureRunner(c => c.AddMySql5() + .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 }); + } + + private static void ConfigureDapperByHost(DatabaseType databaseType, 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"); + } + } + + private static void ConfigureDapperSqlite(IServiceCollection services) + { + DapperExtensions.DapperExtensions.SqlDialect = new SqliteDialect(); + DapperAsyncExtensions.SqlDialect = new SqliteDialect(); + services.AddScoped(); + } + + private static void ConfigureDapperMySql(IServiceCollection services) + { + DapperExtensions.DapperExtensions.SqlDialect = new MySqlDialect(); + DapperAsyncExtensions.SqlDialect = new MySqlDialect(); + services.AddScoped(); + } + + private void ConfigureBrighter(IServiceCollection services) + { + services.AddSingleton(new DbConnectionStringProvider(DbConnectionString())); + + 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() + ) + //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) + .AutoFromAssemblies(typeof(AddPersonHandlerAsync).Assembly); + } + + private void ConfigureDarker(IServiceCollection services) + { + services.AddDarker(options => + { + options.HandlerLifetime = ServiceLifetime.Scoped; + options.QueryProcessorLifetime = ServiceLifetime.Scoped; + }) + .AddHandlersFromAssemblies(typeof(FindPersonByNameHandlerAsync).Assembly) + .AddJsonQueryLogging() + .AddPolicies(new GreetingsPolicy()); + } + + 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() ? GetDevDbConnectionString() : GetConnectionString(GetDatabaseType()); + } + + private DatabaseType GetDatabaseType() + { + 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") + }; + } + + private static string GetDevDbConnectionString() + { + return "Filename=Greetings.db;Cache=Shared"; + } + + private string GetConnectionString(DatabaseType databaseType) + { + return databaseType switch + { + DatabaseType.MySql => Configuration.GetConnectionString("GreetingsMySql"), + _ => throw new InvalidOperationException("Could not determine the database type") + }; + } + } +} diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/appsettings.Development.json b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/appsettings.Development.json new file mode 100644 index 0000000000..1d7a4e1ad3 --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "Microsoft": "Information", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/appsettings.Production.json b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/appsettings.Production.json new file mode 100644 index 0000000000..c02f13f346 --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/appsettings.Production.json @@ -0,0 +1,13 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Warning", + "System": "Warning", + "Microsoft": "Warning" + } + }, + "ConnectionStrings": { + "GreetingsMySql": "server=localhost; port=3306; uid=root; pwd=root; database=Greetings", + "GreetingsMySqlDb": "server=localhost; port=3306; uid=root; pwd=root" + } +} \ No newline at end of file diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/appsettings.json b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/appsettings.json new file mode 100644 index 0000000000..6b9d1b5c56 --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Information", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*", +} diff --git a/samples/WebAPI_Dapper_Kafka/Greetings_SqliteMigrations/Greetings_SqliteMigrations.csproj b/samples/WebAPI_Dapper_Kafka/Greetings_SqliteMigrations/Greetings_SqliteMigrations.csproj new file mode 100644 index 0000000000..eb42023016 --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/Greetings_SqliteMigrations/Greetings_SqliteMigrations.csproj @@ -0,0 +1,13 @@ + + + + net6.0 + enable + disable + + + + + + + diff --git a/samples/WebAPI_Dapper_Kafka/Greetings_SqliteMigrations/Migrations/202204221833_InitialCreate.cs b/samples/WebAPI_Dapper_Kafka/Greetings_SqliteMigrations/Migrations/202204221833_InitialCreate.cs new file mode 100644 index 0000000000..1b1edf2cbf --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/Greetings_SqliteMigrations/Migrations/202204221833_InitialCreate.cs @@ -0,0 +1,30 @@ +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_Kafka/SalutationAnalytics/Database/DatabaseType.cs b/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Database/DatabaseType.cs new file mode 100644 index 0000000000..19e4d6162d --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Database/DatabaseType.cs @@ -0,0 +1,20 @@ +namespace SalutationAnalytics.Database; + +public static class DatabaseGlobals +{ + //environment string key + public const string DATABASE_TYPE_ENV = "BRIGHTER_GREETINGS_DATABASE"; + + public const string MYSQL = "MySQL"; + public const string MSSQL = "MsSQL"; + public const string POSTGRESSQL = "PostgresSQL"; + public const string SQLITE = "Sqlite"; +} + +public enum DatabaseType +{ + MySql, + MsSql, + Postgres, + Sqlite +} diff --git a/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Database/SchemaCreation.cs b/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Database/SchemaCreation.cs new file mode 100644 index 0000000000..f727b948a3 --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Database/SchemaCreation.cs @@ -0,0 +1,229 @@ +using System; +using System.Data; +using FluentMigrator.Runner; +using Microsoft.Data.Sqlite; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using MySqlConnector; +using Paramore.Brighter.Inbox.MySql; +using Paramore.Brighter.Inbox.Sqlite; +using Paramore.Brighter.Outbox.MySql; +using Paramore.Brighter.Outbox.Sqlite; +using Polly; + +namespace SalutationAnalytics.Database +{ + public static class SchemaCreation + { + internal const string INBOX_TABLE_NAME = "Inbox"; + internal const string OUTBOX_TABLE_NAME = "Outbox"; + + public static IHost CheckDbIsUp(this IHost host) + { + using var scope = host.Services.CreateScope(); + + var services = scope.ServiceProvider; + var env = services.GetService(); + var config = services.GetService(); + string connectionString = DbServerConnectionString(config, env); + + //We don't check in development as using Sqlite + if (env.IsDevelopment()) return host; + + WaitToConnect(connectionString); + CreateDatabaseIfNotExists(connectionString); + + return host; + } + + public static IHost CreateInbox(this IHost host) + { + using (var scope = host.Services.CreateScope()) + { + var services = scope.ServiceProvider; + var env = services.GetService(); + var config = services.GetService(); + + CreateInbox(config, env); + } + + return host; + } + + public static IHost MigrateDatabase(this IHost host) + { + using (var scope = host.Services.CreateScope()) + { + var services = scope.ServiceProvider; + + try + { + var runner = services.GetRequiredService(); + runner.ListMigrations(); + runner.MigrateUp(); + } + catch (Exception ex) + { + var logger = services.GetRequiredService>(); + logger.LogError(ex, "An error occurred while migrating the database."); + throw; + } + } + + return host; + } + + private static void CreateDatabaseIfNotExists(string connectionString) + { + //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(); + } + + private static void CreateInbox(IConfiguration config, IHostEnvironment env) + { + try + { + var connectionString = DbConnectionString(config, env); + + if (env.IsDevelopment()) + CreateInboxDevelopment(connectionString); + else + CreateInboxProduction(connectionString); + } + catch (System.Exception e) + { + Console.WriteLine($"Issue with creating Inbox table, {e.Message}"); + throw; + } + } + + private static void CreateInboxDevelopment(string connectionString) + { + using var sqlConnection = new SqliteConnection(connectionString); + sqlConnection.Open(); + + using var exists = sqlConnection.CreateCommand(); + exists.CommandText = SqliteInboxBuilder.GetExistsQuery(INBOX_TABLE_NAME); + using var reader = exists.ExecuteReader(CommandBehavior.SingleRow); + + if (reader.HasRows) return; + + using var command = sqlConnection.CreateCommand(); + command.CommandText = SqliteInboxBuilder.GetDDL(INBOX_TABLE_NAME); + command.ExecuteScalar(); + } + + private static void CreateInboxProduction(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; + + if (exists) return; + + using var command = sqlConnection.CreateCommand(); + command.CommandText = MySqlInboxBuilder.GetDDL(INBOX_TABLE_NAME); + command.ExecuteScalar(); + } + + 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 CreateOutbox(IConfiguration config, IHostEnvironment env) + { + try + { + var connectionString = DbConnectionString(config, env); + + if (env.IsDevelopment()) + CreateOutboxDevelopment(connectionString); + else + CreateOutboxProduction(connectionString); + } + catch (System.Exception e) + { + Console.WriteLine($"Issue with creating Outbox table, {e.Message}"); + throw; + } + } + + private static void CreateOutboxDevelopment(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(); + } + + private static void CreateOutboxProduction(string connectionString) + { + 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; + + if (exists) return; + + using var command = sqlConnection.CreateCommand(); + command.CommandText = MySqlOutboxBuilder.GetDDL(OUTBOX_TABLE_NAME); + 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"); + } + + private static string DbServerConnectionString(IConfiguration config, IHostEnvironment env) + { + return env.IsDevelopment() ? "Filename=Salutations.db;Cache=Shared" : config.GetConnectionString("SalutationsDb"); + } + + private static void WaitToConnect(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 = new MySqlConnection(connectionString); + conn.Open(); + }); + } + } +} diff --git a/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Dockerfile b/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Dockerfile new file mode 100644 index 0000000000..dfaa24d0b8 --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Dockerfile @@ -0,0 +1,7 @@ +FROM mcr.microsoft.com/dotnet/aspnet:5.0-buster-slim + +WORKDIR /app +COPY out/ . + +#run the site +ENTRYPOINT ["dotnet", "GreetingsWatcher.dll"] diff --git a/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Mappers/GreetingMadeMessageMapper.cs b/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Mappers/GreetingMadeMessageMapper.cs new file mode 100644 index 0000000000..f96c47a9c7 --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Mappers/GreetingMadeMessageMapper.cs @@ -0,0 +1,19 @@ +using System.Text.Json; +using Paramore.Brighter; +using SalutationPorts.Requests; + +namespace SalutationAnalytics.Mappers +{ + public class GreetingMadeMessageMapper : IAmAMessageMapper + { + public Message MapToMessage(GreetingMade request) + { + throw new System.NotImplementedException(); + } + + public GreetingMade MapToRequest(Message message) + { + return JsonSerializer.Deserialize(message.Body.Value, JsonSerialisationOptions.Options); + } + } +} diff --git a/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Mappers/SalutationReceivedMessageMapper.cs b/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Mappers/SalutationReceivedMessageMapper.cs new file mode 100644 index 0000000000..bdbb2c1061 --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Mappers/SalutationReceivedMessageMapper.cs @@ -0,0 +1,22 @@ +using System.Text.Json; +using Paramore.Brighter; +using SalutationPorts.Requests; + +namespace SalutationAnalytics.Mappers +{ + public class SalutationReceivedMessageMapper : IAmAMessageMapper + { + public Message MapToMessage(SalutationReceived request) + { + var header = new MessageHeader(messageId: request.Id, topic: "SalutationReceived", messageType: MessageType.MT_EVENT); + var body = new MessageBody(System.Text.Json.JsonSerializer.Serialize(request, new JsonSerializerOptions(JsonSerializerDefaults.General))); + var message = new Message(header, body); + return message; + } + + public SalutationReceived MapToRequest(Message message) + { + throw new System.NotImplementedException(); + } + } +} diff --git a/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Program.cs b/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Program.cs new file mode 100644 index 0000000000..a221b2604d --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Program.cs @@ -0,0 +1,230 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using DapperExtensions; +using DapperExtensions.Sql; +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.MySql; +using Paramore.Brighter.Inbox.Sqlite; +using Paramore.Brighter.MessagingGateway.RMQ; +using Paramore.Brighter.ServiceActivator.Extensions.DependencyInjection; +using Paramore.Brighter.ServiceActivator.Extensions.Hosting; +using SalutationAnalytics.Database; +using SalutationPorts.EntityMappers; +using SalutationPorts.Policies; +using SalutationPorts.Requests; +using Salutations_SqliteMigrations.Migrations; + +namespace SalutationAnalytics +{ + class Program + { + public static async Task Main(string[] args) + { + var host = CreateHostBuilder(args).Build(); + host.CheckDbIsUp(); + host.MigrateDatabase(); + host.CreateInbox(); + host.CreateOutbox(); + await host.RunAsync(); + } + + private static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureHostConfiguration(configurationBuilder => + { + 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: "BRIGHTER_"); + configurationBuilder.AddCommandLine(args); + }) + .ConfigureLogging((context, builder) => + { + builder.AddConsole(); + builder.AddDebug(); + }) + .ConfigureServices((hostContext, services) => + { + ConfigureMigration(hostContext, services); + ConfigureDapper(hostContext, services); + ConfigureBrighter(hostContext, services); + }) + .UseConsoleLifetime(); + + 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 rmqConnection = new RmqMessagingGatewayConnection + { + AmpqUri = new AmqpUriSpecification(new Uri($"amqp://guest:guest@{host}:5672")), Exchange = new Exchange("paramore.brighter.exchange") + }; + + var rmqMessageConsumerFactory = new RmqMessageConsumerFactory(rmqConnection); + + services.AddServiceActivator(options => + { + options.Subscriptions = subscriptions; + options.ChannelFactory = new ChannelFactory(rmqMessageConsumerFactory); + options.UseScoped = true; + options.HandlerLifetime = ServiceLifetime.Scoped; + options.MapperLifetime = ServiceLifetime.Singleton; + options.CommandProcessorLifetime = ServiceLifetime.Scoped; + options.PolicyRegistry = new SalutationPolicy(); + }) + .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 + ) + ); + + services.AddHostedService(); + } + + private static void ConfigureMigration(HostBuilderContext hostBuilderContext, IServiceCollection services) + { + if (hostBuilderContext.HostingEnvironment.IsDevelopment()) + { + services + .AddFluentMigratorCore() + .ConfigureRunner(c => + { + c.AddSQLite() + .WithGlobalConnectionString(DbConnectionString(hostBuilderContext)) + .ScanIn(typeof(Salutations_SqliteMigrations.Migrations.SqliteInitialCreate).Assembly).For.Migrations(); + }); + } + else + { + services + .AddFluentMigratorCore() + .ConfigureRunner(c => c.AddMySql5() + .WithGlobalConnectionString(DbConnectionString(hostBuilderContext)) + .ScanIn(typeof(Salutations_mySqlMigrations.Migrations.MySqlInitialCreate).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) + { + 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"); + } + } + + private static void ConfigureDapperSqlite(IServiceCollection services) + { + DapperExtensions.DapperExtensions.SqlDialect = new SqliteDialect(); + DapperAsyncExtensions.SqlDialect = new SqliteDialect(); + services.AddScoped(); + } + + private static void ConfigureDapperMySql(IServiceCollection services) + { + DapperExtensions.DapperExtensions.SqlDialect = new MySqlDialect(); + DapperAsyncExtensions.SqlDialect = new MySqlDialect(); + services.AddScoped(); + } + + + private static IAmAnInbox ConfigureInbox(HostBuilderContext hostContext) + { + if (hostContext.HostingEnvironment.IsDevelopment()) + { + return new SqliteInbox(new SqliteInboxConfiguration(DbConnectionString(hostContext), SchemaCreation.INBOX_TABLE_NAME)); + } + + return new MySqlInbox(new MySqlInboxConfiguration(DbConnectionString(hostContext), SchemaCreation.INBOX_TABLE_NAME)); + } + + 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"); + } + + private static DatabaseType GetDatabaseType(HostBuilderContext hostContext) + { + return hostContext.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") + }; + } + + + 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"); + } + } +} diff --git a/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Properties/launchSettings.json b/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Properties/launchSettings.json new file mode 100644 index 0000000000..4154fbe3ff --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Properties/launchSettings.json @@ -0,0 +1,19 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "Development": { + "commandName": "Project", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "BRIGHTER_GREETINGS_DATABASE" : "Sqlite" + } + }, + "Production": { + "commandName": "Project", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Production", + "BRIGHTER_GREETINGS_DATABASE" : "MySQL" + } + } + } +} diff --git a/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/SalutationAnalytics.csproj b/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/SalutationAnalytics.csproj new file mode 100644 index 0000000000..a59fc78023 --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/SalutationAnalytics.csproj @@ -0,0 +1,44 @@ + + + + Exe + net6.0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + PreserveNewest + PreserveNewest + + + true + PreserveNewest + PreserveNewest + + + + diff --git a/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/appsettings.Development.json b/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/appsettings.Development.json new file mode 100644 index 0000000000..1d7a4e1ad3 --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "Microsoft": "Information", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/appsettings.Production.json b/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/appsettings.Production.json new file mode 100644 index 0000000000..dbbfec462a --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/appsettings.Production.json @@ -0,0 +1,13 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Warning", + "Microsoft": "Warning" + } + }, + "ConnectionStrings": { + "Salutations": "server=localhost; port=3306; uid=root; pwd=root; database=Salutations", + "SalutationsDb": "server=localhost; port=3306; uid=root; pwd=root" + } +} \ No newline at end of file diff --git a/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/appsettings.json b/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/appsettings.json new file mode 100644 index 0000000000..27bbd50072 --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + } +} \ No newline at end of file diff --git a/samples/WebAPI_Dapper_Kafka/SalutationEntities/Salutation.cs b/samples/WebAPI_Dapper_Kafka/SalutationEntities/Salutation.cs new file mode 100644 index 0000000000..b3c44a995b --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/SalutationEntities/Salutation.cs @@ -0,0 +1,16 @@ +namespace SalutationEntities +{ + public class Salutation + { + public long Id { get; set; } + public byte[] TimeStamp { get; set; } + public string Greeting { get; set; } + + public Salutation() { /* ORM needs to create */ } + + public Salutation(string greeting) + { + Greeting = greeting; + } + } +} diff --git a/samples/WebAPI_Dapper_Kafka/SalutationEntities/SalutationEntities.csproj b/samples/WebAPI_Dapper_Kafka/SalutationEntities/SalutationEntities.csproj new file mode 100644 index 0000000000..dbc151713b --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/SalutationEntities/SalutationEntities.csproj @@ -0,0 +1,7 @@ + + + + net6.0 + + + diff --git a/samples/WebAPI_Dapper_Kafka/SalutationPorts/EntityMappers/SalutationMapper.cs b/samples/WebAPI_Dapper_Kafka/SalutationPorts/EntityMappers/SalutationMapper.cs new file mode 100644 index 0000000000..aacce5f63e --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/SalutationPorts/EntityMappers/SalutationMapper.cs @@ -0,0 +1,15 @@ +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_Kafka/SalutationPorts/Handlers/GreetingMadeHandler.cs b/samples/WebAPI_Dapper_Kafka/SalutationPorts/Handlers/GreetingMadeHandler.cs new file mode 100644 index 0000000000..4d9894b23d --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/SalutationPorts/Handlers/GreetingMadeHandler.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using DapperExtensions; +using Microsoft.Extensions.Logging; +using Paramore.Brighter; +using Paramore.Brighter.Dapper; +using Paramore.Brighter.Logging.Attributes; +using Paramore.Brighter.Policies.Attributes; +using SalutationEntities; +using SalutationPorts.Requests; + +namespace SalutationPorts.Handlers +{ + public class GreetingMadeHandlerAsync : RequestHandlerAsync + { + private readonly IUnitOfWork _uow; + private readonly IAmACommandProcessor _postBox; + private readonly ILogger _logger; + + public GreetingMadeHandlerAsync(IUnitOfWork uow, IAmACommandProcessor postBox, ILogger logger) + { + _uow = uow; + _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) + { + var posts = new List(); + + var tx = await _uow.BeginOrGetTransactionAsync(cancellationToken); + try + { + var salutation = new Salutation(@event.Greeting); + + await _uow.Database.InsertAsync(salutation, tx); + + posts.Add(await _postBox.DepositPostAsync(new SalutationReceived(DateTimeOffset.Now), cancellationToken: cancellationToken)); + + await tx.CommitAsync(cancellationToken); + } + catch (Exception e) + { + _logger.LogError(e, "Could not save salutation"); + + //if it went wrong rollback entity write and Outbox write + await tx.RollbackAsync(cancellationToken); + + return await base.HandleAsync(@event, cancellationToken); + } + + await _postBox.ClearOutboxAsync(posts, cancellationToken: cancellationToken); + + return await base.HandleAsync(@event, cancellationToken); + } + } +} diff --git a/samples/WebAPI_Dapper_Kafka/SalutationPorts/Policies/Retry.cs b/samples/WebAPI_Dapper_Kafka/SalutationPorts/Policies/Retry.cs new file mode 100644 index 0000000000..4db47aa42d --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/SalutationPorts/Policies/Retry.cs @@ -0,0 +1,27 @@ +using System; +using Polly; +using Polly.Contrib.WaitAndRetry; +using Polly.Retry; + +namespace SalutationPorts.Policies +{ + public static class Retry + { + public const string RETRYPOLICYASYNC = "SalutationPorts.Policies.RetryPolicyAsync"; + public const string EXPONENTIAL_RETRYPOLICYASYNC = "SalutationPorts.Policies.ExponenttialRetryPolicyAsync"; + + public static AsyncRetryPolicy GetSimpleHandlerRetryPolicy() + { + return Policy.Handle().WaitAndRetryAsync(new[] + { + TimeSpan.FromMilliseconds(50), TimeSpan.FromMilliseconds(100), TimeSpan.FromMilliseconds(150) + }); + } + + public static AsyncRetryPolicy GetExponentialHandlerRetryPolicy() + { + var delay = Backoff.ExponentialBackoff(TimeSpan.FromMilliseconds(100), retryCount: 5, fastFirst: true); + return Policy.Handle().WaitAndRetryAsync(delay); + } + } +} diff --git a/samples/WebAPI_Dapper_Kafka/SalutationPorts/Policies/SalutationPolicy.cs b/samples/WebAPI_Dapper_Kafka/SalutationPorts/Policies/SalutationPolicy.cs new file mode 100644 index 0000000000..ddf21c324f --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/SalutationPorts/Policies/SalutationPolicy.cs @@ -0,0 +1,18 @@ +using Paramore.Brighter; + +namespace SalutationPorts.Policies +{ + public class SalutationPolicy : DefaultPolicy + { + public SalutationPolicy() + { + AddSalutationPolicies(); + } + + private void AddSalutationPolicies() + { + Add(Retry.RETRYPOLICYASYNC, Retry.GetSimpleHandlerRetryPolicy()); + Add(Retry.EXPONENTIAL_RETRYPOLICYASYNC, Retry.GetExponentialHandlerRetryPolicy()); + } + } +} diff --git a/samples/WebAPI_Dapper_Kafka/SalutationPorts/Requests/GreetingMade.cs b/samples/WebAPI_Dapper_Kafka/SalutationPorts/Requests/GreetingMade.cs new file mode 100644 index 0000000000..e22c75a351 --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/SalutationPorts/Requests/GreetingMade.cs @@ -0,0 +1,15 @@ +using System; +using Paramore.Brighter; + +namespace SalutationPorts.Requests +{ + public class GreetingMade : Event + { + public string Greeting { get; set; } + + public GreetingMade(string greeting) : base(Guid.NewGuid()) + { + Greeting = greeting; + } + } +} diff --git a/samples/WebAPI_Dapper_Kafka/SalutationPorts/Requests/SalutationReceived.cs b/samples/WebAPI_Dapper_Kafka/SalutationPorts/Requests/SalutationReceived.cs new file mode 100644 index 0000000000..c2610ec790 --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/SalutationPorts/Requests/SalutationReceived.cs @@ -0,0 +1,15 @@ +using System; +using Paramore.Brighter; + +namespace SalutationPorts.Requests +{ + public class SalutationReceived : Event + { + public DateTimeOffset ReceivedAt { get; } + + public SalutationReceived(DateTimeOffset receivedAt) : base(Guid.NewGuid()) + { + ReceivedAt = receivedAt; + } + } +} diff --git a/samples/WebAPI_Dapper_Kafka/SalutationPorts/SalutationPorts.csproj b/samples/WebAPI_Dapper_Kafka/SalutationPorts/SalutationPorts.csproj new file mode 100644 index 0000000000..e54eee1aa4 --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/SalutationPorts/SalutationPorts.csproj @@ -0,0 +1,19 @@ + + + + net6.0 + + + + + + + + + + + + + + + diff --git a/samples/WebAPI_Dapper_Kafka/Salutations_SqliteMigrations/Migrations/202205161812_SqliteMigrations.cs b/samples/WebAPI_Dapper_Kafka/Salutations_SqliteMigrations/Migrations/202205161812_SqliteMigrations.cs new file mode 100644 index 0000000000..0afb14f529 --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/Salutations_SqliteMigrations/Migrations/202205161812_SqliteMigrations.cs @@ -0,0 +1,20 @@ +using FluentMigrator; + +namespace Salutations_SqliteMigrations.Migrations; + +[Migration(1)] +public class SqliteInitialCreate : 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_Kafka/Salutations_SqliteMigrations/Salutations_SqliteMigrations.csproj b/samples/WebAPI_Dapper_Kafka/Salutations_SqliteMigrations/Salutations_SqliteMigrations.csproj new file mode 100644 index 0000000000..e157295180 --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/Salutations_SqliteMigrations/Salutations_SqliteMigrations.csproj @@ -0,0 +1,13 @@ + + + + net6.0 + enable + disable + + + + + + + \ No newline at end of file From 771603a2a2efd610c0fe12a657ade2d48b86a354 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Mon, 1 May 2023 16:32:44 +0100 Subject: [PATCH 25/89] Clean up errors in SQL tests --- Brighter.sln | 30 +++++++++++++ .../Greetings_MySqlMigrations.csproj | 14 +++++++ .../Migrations/20220527_InitialCreate.cs | 30 +++++++++++++ .../Migrations/20220527_MySqlMigrations.cs | 20 +++++++++ .../Salutations_mySqlMigrations.csproj | 13 ++++++ .../MsSqlQueries.cs | 2 +- .../MySqlQueries.cs | 2 +- .../PostgreSqlQueries.cs | 2 +- .../SqliteQueries.cs | 2 +- ..._message_to_a_binary_body_message_store.cs | 4 +- ..._writing_a_message_to_the_message_store.cs | 4 +- ...ing_messages_to_the_message_store_async.cs | 42 +++++++++---------- 12 files changed, 136 insertions(+), 29 deletions(-) create mode 100644 samples/WebAPI_Dapper_Kafka/Greetings_MySqlMigrations/Greetings_MySqlMigrations.csproj create mode 100644 samples/WebAPI_Dapper_Kafka/Greetings_MySqlMigrations/Migrations/20220527_InitialCreate.cs create mode 100644 samples/WebAPI_Dapper_Kafka/Salutations_mySqlMigrations/Migrations/20220527_MySqlMigrations.cs create mode 100644 samples/WebAPI_Dapper_Kafka/Salutations_mySqlMigrations/Salutations_mySqlMigrations.csproj diff --git a/Brighter.sln b/Brighter.sln index df8dfcaa84..c3cb5a2860 100644 --- a/Brighter.sln +++ b/Brighter.sln @@ -358,6 +358,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SalutationAnalytics", "samp EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Salutations_SqliteMigrations", "samples\WebAPI_Dapper_Kafka\Salutations_SqliteMigrations\Salutations_SqliteMigrations.csproj", "{B50A44CE-5F54-4559-90E0-1AF81655E944}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Salutations_mySqlMigrations", "samples\WebAPI_Dapper_Kafka\Salutations_mySqlMigrations\Salutations_mySqlMigrations.csproj", "{E66CA4D7-8339-4674-B102-BC7CA890B7C5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Greetings_MySqlMigrations", "samples\WebAPI_Dapper_Kafka\Greetings_MySqlMigrations\Greetings_MySqlMigrations.csproj", "{2FADCED2-8563-4261-9AA8-514E5888075F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -2072,6 +2076,30 @@ Global {B50A44CE-5F54-4559-90E0-1AF81655E944}.Release|Mixed Platforms.Build.0 = Release|Any CPU {B50A44CE-5F54-4559-90E0-1AF81655E944}.Release|x86.ActiveCfg = Release|Any CPU {B50A44CE-5F54-4559-90E0-1AF81655E944}.Release|x86.Build.0 = Release|Any CPU + {E66CA4D7-8339-4674-B102-BC7CA890B7C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E66CA4D7-8339-4674-B102-BC7CA890B7C5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E66CA4D7-8339-4674-B102-BC7CA890B7C5}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {E66CA4D7-8339-4674-B102-BC7CA890B7C5}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {E66CA4D7-8339-4674-B102-BC7CA890B7C5}.Debug|x86.ActiveCfg = Debug|Any CPU + {E66CA4D7-8339-4674-B102-BC7CA890B7C5}.Debug|x86.Build.0 = Debug|Any CPU + {E66CA4D7-8339-4674-B102-BC7CA890B7C5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E66CA4D7-8339-4674-B102-BC7CA890B7C5}.Release|Any CPU.Build.0 = Release|Any CPU + {E66CA4D7-8339-4674-B102-BC7CA890B7C5}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {E66CA4D7-8339-4674-B102-BC7CA890B7C5}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {E66CA4D7-8339-4674-B102-BC7CA890B7C5}.Release|x86.ActiveCfg = Release|Any CPU + {E66CA4D7-8339-4674-B102-BC7CA890B7C5}.Release|x86.Build.0 = Release|Any CPU + {2FADCED2-8563-4261-9AA8-514E5888075F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2FADCED2-8563-4261-9AA8-514E5888075F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2FADCED2-8563-4261-9AA8-514E5888075F}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {2FADCED2-8563-4261-9AA8-514E5888075F}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {2FADCED2-8563-4261-9AA8-514E5888075F}.Debug|x86.ActiveCfg = Debug|Any CPU + {2FADCED2-8563-4261-9AA8-514E5888075F}.Debug|x86.Build.0 = Debug|Any CPU + {2FADCED2-8563-4261-9AA8-514E5888075F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2FADCED2-8563-4261-9AA8-514E5888075F}.Release|Any CPU.Build.0 = Release|Any CPU + {2FADCED2-8563-4261-9AA8-514E5888075F}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {2FADCED2-8563-4261-9AA8-514E5888075F}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {2FADCED2-8563-4261-9AA8-514E5888075F}.Release|x86.ActiveCfg = Release|Any CPU + {2FADCED2-8563-4261-9AA8-514E5888075F}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -2196,6 +2224,8 @@ Global {2DBA8A1A-76AC-4F6F-9533-2977AAD9DB15} = {36612AF5-23DB-46C0-9EB5-0F21911741F3} {559212C0-0912-4586-AD6B-462E7A0E738E} = {36612AF5-23DB-46C0-9EB5-0F21911741F3} {B50A44CE-5F54-4559-90E0-1AF81655E944} = {36612AF5-23DB-46C0-9EB5-0F21911741F3} + {E66CA4D7-8339-4674-B102-BC7CA890B7C5} = {36612AF5-23DB-46C0-9EB5-0F21911741F3} + {2FADCED2-8563-4261-9AA8-514E5888075F} = {36612AF5-23DB-46C0-9EB5-0F21911741F3} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {8B7C7E31-2E32-4E0D-9426-BC9AF22E9F4C} diff --git a/samples/WebAPI_Dapper_Kafka/Greetings_MySqlMigrations/Greetings_MySqlMigrations.csproj b/samples/WebAPI_Dapper_Kafka/Greetings_MySqlMigrations/Greetings_MySqlMigrations.csproj new file mode 100644 index 0000000000..be526d4af0 --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/Greetings_MySqlMigrations/Greetings_MySqlMigrations.csproj @@ -0,0 +1,14 @@ + + + + net6.0 + enable + disable + + + + + + + + diff --git a/samples/WebAPI_Dapper_Kafka/Greetings_MySqlMigrations/Migrations/20220527_InitialCreate.cs b/samples/WebAPI_Dapper_Kafka/Greetings_MySqlMigrations/Migrations/20220527_InitialCreate.cs new file mode 100644 index 0000000000..9fa274705a --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/Greetings_MySqlMigrations/Migrations/20220527_InitialCreate.cs @@ -0,0 +1,30 @@ +using FluentMigrator; + +namespace Greetings_MySqlMigrations.Migrations; + +[Migration(1)] +public class MySqlInitialCreate : Migration +{ + public override void Up() + { + Create.Table("Person") + .WithColumn("Id").AsInt32().NotNullable().PrimaryKey().Identity() + .WithColumn("Name").AsString().Unique() + .WithColumn("TimeStamp").AsDateTime().Nullable().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_Kafka/Salutations_mySqlMigrations/Migrations/20220527_MySqlMigrations.cs b/samples/WebAPI_Dapper_Kafka/Salutations_mySqlMigrations/Migrations/20220527_MySqlMigrations.cs new file mode 100644 index 0000000000..d1f4e4aec2 --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/Salutations_mySqlMigrations/Migrations/20220527_MySqlMigrations.cs @@ -0,0 +1,20 @@ +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_Kafka/Salutations_mySqlMigrations/Salutations_mySqlMigrations.csproj b/samples/WebAPI_Dapper_Kafka/Salutations_mySqlMigrations/Salutations_mySqlMigrations.csproj new file mode 100644 index 0000000000..9107f11c61 --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/Salutations_mySqlMigrations/Salutations_mySqlMigrations.csproj @@ -0,0 +1,13 @@ + + + + net6.0 + enable + enable + + + + + + + diff --git a/src/Paramore.Brighter.Outbox.MsSql/MsSqlQueries.cs b/src/Paramore.Brighter.Outbox.MsSql/MsSqlQueries.cs index 5da2d389fd..d0343b2fc6 100644 --- a/src/Paramore.Brighter.Outbox.MsSql/MsSqlQueries.cs +++ b/src/Paramore.Brighter.Outbox.MsSql/MsSqlQueries.cs @@ -3,7 +3,7 @@ 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 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 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 < 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, 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}"; diff --git a/src/Paramore.Brighter.Outbox.MySql/MySqlQueries.cs b/src/Paramore.Brighter.Outbox.MySql/MySqlQueries.cs index bf5a885af5..068493e403 100644 --- a/src/Paramore.Brighter.Outbox.MySql/MySqlQueries.cs +++ b/src/Paramore.Brighter.Outbox.MySql/MySqlQueries.cs @@ -3,7 +3,7 @@ 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 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}"; diff --git a/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlQueries.cs b/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlQueries.cs index 31d7736ade..f622b013e5 100644 --- a/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlQueries.cs +++ b/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlQueries.cs @@ -3,7 +3,7 @@ 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 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}"; diff --git a/src/Paramore.Brighter.Outbox.Sqlite/SqliteQueries.cs b/src/Paramore.Brighter.Outbox.Sqlite/SqliteQueries.cs index 634adef8b3..085eda2401 100644 --- a/src/Paramore.Brighter.Outbox.Sqlite/SqliteQueries.cs +++ b/src/Paramore.Brighter.Outbox.Sqlite/SqliteQueries.cs @@ -3,7 +3,7 @@ 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, 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}"; 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 index ca32900ad3..d2d97dc0fa 100644 --- 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 @@ -76,8 +76,8 @@ private void AssertMessage() //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.ffZ") - .Should().Be(_message.Header.TimeStamp.ToString("yyyy-MM-ddTHH:mm:ss.ffZ")); + _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_a_message_to_the_message_store.cs b/tests/Paramore.Brighter.MSSQL.Tests/Outbox/When_writing_a_message_to_the_message_store.cs index 2ace05bff0..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 @@ -100,8 +100,8 @@ private void AssertMessage() //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.ffZ") - .Should().Be(_message.Header.TimeStamp.ToString("yyyy-MM-ddTHH:mm:ss.ffZ")); + _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() From fc6f5c2b8260bb0a3cfd69fc8b30392c07e3ed1e Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Mon, 1 May 2023 16:43:34 +0100 Subject: [PATCH 26/89] fix up sqlite --- .../Outbox/SQlOutboxMigrationTests.cs | 3 +- ...iting_A_Message_To_A_Binary_Body_Outbox.cs | 4 +-- .../When_Writing_A_Message_To_The_Outbox.cs | 4 +-- ...n_Writing_A_Message_To_The_Outbox_Async.cs | 4 +-- .../When_Writing_Messages_To_The_Outbox.cs | 32 ++++++++--------- ...en_Writing_Messages_To_The_Outbox_Async.cs | 36 +++++++++---------- 6 files changed, 41 insertions(+), 42 deletions(-) diff --git a/tests/Paramore.Brighter.Sqlite.Tests/Outbox/SQlOutboxMigrationTests.cs b/tests/Paramore.Brighter.Sqlite.Tests/Outbox/SQlOutboxMigrationTests.cs index 84894b1742..165d659d41 100644 --- a/tests/Paramore.Brighter.Sqlite.Tests/Outbox/SQlOutboxMigrationTests.cs +++ b/tests/Paramore.Brighter.Sqlite.Tests/Outbox/SQlOutboxMigrationTests.cs @@ -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_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 index 202a6190fb..deda20eeba 100644 --- 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 @@ -89,8 +89,8 @@ public void When_Writing_A_Message_To_The_Outbox() //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.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); 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 7b0102543b..0f89eec3cd 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 @@ -83,8 +83,8 @@ public void When_Writing_A_Message_To_The_Outbox() //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.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); 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 943f7a4e2d..619b7ddd07 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 @@ -79,8 +79,8 @@ public async Task When_Writing_A_Message_To_The_Outbox_Async() _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.ToString("yyyy-MM-ddTHH:mm:ss.fZ") - .Should().Be(_messageEarliest.Header.TimeStamp.ToString("yyyy-MM-ddTHH:mm:ss.fZ")); + _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 04288ca26b..b977d55376 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 @@ -37,9 +37,9 @@ public class SqlOutboxWritngMessagesTests { private readonly SqliteTestHelper _sqliteTestHelper; private readonly SqliteOutbox _sqlOutbox; - 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; public SqlOutboxWritngMessagesTests() @@ -48,25 +48,25 @@ public SqlOutboxWritngMessagesTests() _sqliteTestHelper.SetupMessageDb(); _sqlOutbox = new SqliteOutbox(new SqliteConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableNameMessages)); - _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() { - _sqlOutbox.Add(_messageEarliest); - _sqlOutbox.Add(_message2); - _sqlOutbox.Add(_messageLatest); + _sqlOutbox.Add(_messageOne); + _sqlOutbox.Add(_messageTwo); + _sqlOutbox.Add(_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); } @@ -74,14 +74,14 @@ public void When_Writing_Messages_To_The_Outbox() [Fact] public void When_Writing_Messages_To_The_Outbox_Bulk() { - _sqlOutbox.Add(new List {_messageEarliest, _message2, _messageLatest}); + _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); + _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 c39c073ada..de79fa91fa 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 @@ -38,9 +38,9 @@ public class SqlOutboxWritngMessagesAsyncTests : IDisposable { private readonly SqliteTestHelper _sqliteTestHelper; private readonly SqliteOutbox _sSqlOutbox; - private Message _message2; - private Message _messageEarliest; - private Message _messageLatest; + private Message _messageTwo; + private Message _messageOne; + private Message _messageThree; private IList _retrievedMessages; public SqlOutboxWritngMessagesAsyncTests() @@ -57,10 +57,10 @@ public async Task When_Writing_Messages_To_The_Outbox_Async() _retrievedMessages = await _sSqlOutbox.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); } @@ -73,28 +73,26 @@ public async Task When_Writing_Messages_To_The_Outbox_Async_Bulk() _retrievedMessages = await _sSqlOutbox.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 _sSqlOutbox.AddAsync(_messageEarliest); + _messageOne = new Message(new MessageHeader(Guid.NewGuid(), "Test", MessageType.MT_COMMAND, DateTime.UtcNow.AddHours(-3)), new MessageBody("Body")); + if(addMessagesToOutbox) await _sSqlOutbox.AddAsync(_messageOne); - _message2 = new Message(new MessageHeader(Guid.NewGuid(), "Test2", MessageType.MT_COMMAND, DateTime.UtcNow.AddHours(-2)), new MessageBody("Body2")); - if(addMessagesToOutbox) await _sSqlOutbox.AddAsync(_message2); + _messageTwo = new Message(new MessageHeader(Guid.NewGuid(), "Test2", MessageType.MT_COMMAND, DateTime.UtcNow.AddHours(-2)), new MessageBody("Body2")); + if(addMessagesToOutbox) await _sSqlOutbox.AddAsync(_messageTwo); - _messageLatest = new Message(new MessageHeader(Guid.NewGuid(), "Test3", MessageType.MT_COMMAND, DateTime.UtcNow.AddHours(-1)), new MessageBody("Body3")); - if(addMessagesToOutbox) await _sSqlOutbox.AddAsync(_messageLatest); + _messageThree = new Message(new MessageHeader(Guid.NewGuid(), "Test3", MessageType.MT_COMMAND, DateTime.UtcNow.AddHours(-1)), new MessageBody("Body3")); + if(addMessagesToOutbox) await _sSqlOutbox.AddAsync(_messageThree); - return new List { _messageEarliest, _message2, _messageLatest }; + return new List { _messageOne, _messageTwo, _messageThree }; } private void Release() From 790bb6c138c4c9b9d923a4be34c6259089c95afb Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Mon, 1 May 2023 17:34:47 +0100 Subject: [PATCH 27/89] Fix mysql messages --- .../When_writing_messages_to_the_outbox.cs | 14 ++++---- ...en_writing_messages_to_the_outbox_async.cs | 34 +++++++++---------- .../When_Writing_Messages_To_The_Outbox.cs | 34 +++++++++---------- 3 files changed, 41 insertions(+), 41 deletions(-) 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 019dbdb35c..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 @@ -63,10 +63,10 @@ public void When_Writing_Messages_To_The_Outbox() _retrievedMessages = _mySqlOutbox.Get(); - //should read first message last from the outbox - _retrievedMessages.Last().Id.Should().Be(_messageOne.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(_messageThree.Id); + _retrievedMessages.First().Id.Should().Be(_messageOne.Id); //should read the messages from the outbox _retrievedMessages.Should().HaveCount(3); } @@ -78,10 +78,10 @@ public void When_Writing_Messages_To_The_Outbox_Bulk() _mySqlOutbox.Add(messages); _retrievedMessages = _mySqlOutbox.Get(); - //should read first message last from the outbox - _retrievedMessages.Last().Id.Should().Be(_messageOne.Id); - //should read last message first from the outbox - _retrievedMessages.First().Id.Should().Be(_messageThree.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 8e93c8cc63..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 @@ -38,9 +38,9 @@ public class MySqlOutboxWritingMessagesAsyncTests : IDisposable { private readonly MySqlTestHelper _mySqlTestHelper; private readonly MySqlOutbox _mySqlOutbox; - private Message _message2; - private Message _messageEarliest; - private Message _messageLatest; + private Message _messageTwo; + private Message _messageOne; + private Message _messageThree; private IList _retrievedMessages; public MySqlOutboxWritingMessagesAsyncTests() @@ -57,10 +57,10 @@ public async Task When_Writing_Messages_To_The_Outbox_Async() _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); } @@ -73,26 +73,26 @@ public async Task When_Writing_Messages_To_The_Outbox_Async_Bulk() _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 _mySqlOutbox.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 _mySqlOutbox.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 _mySqlOutbox.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/Outbox/When_Writing_Messages_To_The_Outbox.cs b/tests/Paramore.Brighter.PostgresSQL.Tests/Outbox/When_Writing_Messages_To_The_Outbox.cs index 6f1f11f825..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,9 +36,9 @@ 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 PostgreSqlOutbox _sqlOutbox; @@ -48,27 +48,27 @@ public SqlOutboxWritngMessagesTests() _postgresSqlTestHelper.SetupMessageDb(); _sqlOutbox = new PostgreSqlOutbox(_postgresSqlTestHelper.Configuration); - _messageEarliest = new Message(new MessageHeader(Guid.NewGuid(), "Test", MessageType.MT_COMMAND, DateTime.UtcNow.AddHours(-3)), new MessageBody("Body")); + _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() { - _sqlOutbox.Add(_messageEarliest); - _sqlOutbox.Add(_message2); - _sqlOutbox.Add(_messageLatest); + _sqlOutbox.Add(_messageOne); + _sqlOutbox.Add(_messageTwo); + _sqlOutbox.Add(_messageThree); _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() { - _sqlOutbox.Add(new List{_messageEarliest, _message2, _messageLatest}); + _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); } From d3e58000dedf839ad2c94f759803fc46441c876b Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Mon, 1 May 2023 18:57:23 +0100 Subject: [PATCH 28/89] missing files --- samples/WebAPI_Dapper_Kafka/README.md | 124 ++++++++++++++++++ samples/WebAPI_Dapper_Kafka/TODO.txt | 6 + samples/WebAPI_Dapper_Kafka/build.sh | 15 +++ .../WebAPI_Dapper_Kafka/docker-compose.yml | 51 +++++++ samples/WebAPI_Dapper_Kafka/tests.http | 31 +++++ 5 files changed, 227 insertions(+) create mode 100644 samples/WebAPI_Dapper_Kafka/README.md create mode 100644 samples/WebAPI_Dapper_Kafka/TODO.txt create mode 100644 samples/WebAPI_Dapper_Kafka/build.sh create mode 100644 samples/WebAPI_Dapper_Kafka/docker-compose.yml create mode 100644 samples/WebAPI_Dapper_Kafka/tests.http diff --git a/samples/WebAPI_Dapper_Kafka/README.md b/samples/WebAPI_Dapper_Kafka/README.md new file mode 100644 index 0000000000..b78be28dd2 --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/README.md @@ -0,0 +1,124 @@ +# Table of content +- [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) +# 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. + +*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. + +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. + +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. + +* **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. + + +## Build and Deploy + +### Building + +Use the build.sh file to: + +- 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. + +(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 common error is to change something, forget to run build.sh and use an old Docker image. + +### Deploy + +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 +``` + +```sh +docker compose up -d mysql # will just start mysql +``` + +and so on. + +### 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 + +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 +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) + +## 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_Kafka/TODO.txt b/samples/WebAPI_Dapper_Kafka/TODO.txt new file mode 100644 index 0000000000..fde917f5a8 --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/TODO.txt @@ -0,0 +1,6 @@ +# TO DO List + + - Implement for MySQL + - Consider if we need to implement migrations etc for MSSQL & Postgres? Would need a different flag from isdev to pick the db we wanted to use + - Anything else? + - Delete this file \ No newline at end of file diff --git a/samples/WebAPI_Dapper_Kafka/build.sh b/samples/WebAPI_Dapper_Kafka/build.sh new file mode 100644 index 0000000000..052b5d5dc6 --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/build.sh @@ -0,0 +1,15 @@ +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_Kafka/docker-compose.yml b/samples/WebAPI_Dapper_Kafka/docker-compose.yml new file mode 100644 index 0000000000..703b1a2372 --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/docker-compose.yml @@ -0,0 +1,51 @@ +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_Dapper_Kafka/tests.http b/samples/WebAPI_Dapper_Kafka/tests.http new file mode 100644 index 0000000000..56382375bb --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/tests.http @@ -0,0 +1,31 @@ +### Clean up between runs if needed + +DELETE http://localhost:5000/People/Tyrion HTTP/1.1 + +### Add a Person + +POST http://localhost:5000/People/new HTTP/1.1 +Content-Type: application/json + +{ + "Name" : "Tyrion" +} + +### Now see that person + +GET http://localhost:5000/People/Tyrion HTTP/1.1 + + +### Now add some more greetings + +POST http://localhost:5000/Greetings/Tyrion/new HTTP/1.1 +Content-Type: application/json + +{ + "Greeting" : "I drink, and I know things" +} + +### And now look up Tyrion's greetings + +GET http://localhost:5000/Greetings/Tyrion HTTP/1.1 + From d2b9af717ef212d12c864453a19b9fdb98e9a129 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Tue, 2 May 2023 00:31:34 +0100 Subject: [PATCH 29/89] Use the CreatedTime to ensure that we get the correct time --- Docker/dynamodb/shared-local-instance.db | Bin 552960 -> 552960 bytes .../DynamoDbOutbox.cs | 3 ++- .../MessageItem.cs | 13 ++++--------- ...en_writing_a_utf8_message_to_the_outbox.cs | 3 ++- ...ting_a_utf8_message_to_the_outbox_async.cs | 4 ++-- 5 files changed, 10 insertions(+), 13 deletions(-) diff --git a/Docker/dynamodb/shared-local-instance.db b/Docker/dynamodb/shared-local-instance.db index 881b750a6eb237392c9abed45ae60446e8761956..7d2d85cc9720a932129c78eed4a451ccc5991caa 100644 GIT binary patch literal 552960 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≪}`)RM|%+TLYPnL=~z(ODBV`0O6{3l_`!yr6g-K)gMnd`35m*sh^`y07()l ztxaAFnHy*tjv7(~M-DgAnn15s=~yJ0{^qvS-=S@3>(E;^ZtC0IzNx#jZ^uCEx;;kg zcG1<*wp!oZzFuo>Uq47~Q|oBoad^x2%%y&1FG}rCrmhApIFTGFBWJaYYjZZqb?&-* z<*23ULbL}mAB`EwdjZoU{0DH|XdHN`qS_wyRK=y4kp{p(+CK7}7>*H1-zD{KfFRlh z5G%3f_%1ZlD)3ogp96*Qp}M2yQm0G%w|u?d-`hR2L%qsdoE%IGU#LY!Tq>)K0n@5H zdvQgp@c;2b#1__n%Fzu|Q^L@c`2RN5EjzXJNlX5yarvS*EnKdKU9KNP(^?cBvElXQn5_D)jyikcR zY0yHsaqEdyh+uP082xu6Jv(cN0u364X8Ie*g=);q^ZM)*$gu*$)uD>=!O8*~uQI$M z4cg%F)60g>2T9SqwBa2KReMyXFuW|u#bP)-hfV7EiIHIzezJK`YJO%m7 zzn3;Ws~n+hXf6Yn(zhw3f+zDln4a<+tRj)+W@mVvD#*v02d*D9v5n9t;9|-rUWm>o z$sx1_q2>i;Olm4iQ4R8g!&l|X;r}^pb;AG*6c*KJVTr)%5>*X97iy|rY#N-Sy*W1D_d;-HN9NutyCxK=9sv6DJ zo9k3o&y-D0Rc|Si>K+`vvTXHSr16)(ca+s}YMRGh0e)NRZ;*#bDMlCoodBXy$c=3g zXZCf4G~R|yEu?o1c#0ctlG@6n6Y-sq3tr!1WV)KcTP=+*o&Pt=^Z&9X_cdO$=z)b- zFL>vCqv1pK8^;`QYu`?60S^CBo}$LnmZbgHgArG=1HE8GjC`5Y?ajY^mDTIhR)_8#oEIlg2J7e| zGLj`>K{`x-PH1vC^zAt${TJ*8oCJWJg+6(S4@<1?fLlkY#UhfS#7vENOkSdD^}*qw zZ1r56{FnbMZS|NLSIO;P5E7hEtw}Nml8uLvM>op|EoxnvW3|%plcNu1Ei=NyV@#hg zqT%|!29no^q~2d5SKAoVtEYs1o*aJpf2B8EmAK0?c*> zIyiiL+4Q-{3zvT}ZF=xaZj>PJVG>(u&a%=ylM#F7vOM2G@qn zuf#Kj5$M?suN35Uomu`6(+lALRJ*9LGl9*m*L@UjrPqh03G7t0OEt(34&Rz9(|?I!S{thLHq}91$@GGP8Z07ooB5!X#eZB99CVD) zt`s08G(EHF4-W6jRk#_&K`*mtS=xqFMK(2P^m7c9WI#{=q3~x2el(ZiMonjXbevZe z)j2qPORfz6rnJwSR*0?(Y8OnY5$p|s4@?b#e$a?EOeW~=49Nc#;(Q27mPtiXh(k&D z;86G?(y`C8`6SHLSxilg-Cz|BU*i8e0RF$RY2o_$yXp^2e8{c+W>ze4A~{@g^%vy> zTXa#{)!D?fAc4LZ_F`NuHsq|F>%&YBzgS>~35VCrF-9r8TXsTO0P3{Zl)yum{-JMC z>%GsiEu|+D+u8u9TC2nzaOjpGgok$Ml z$7Hy$gOTQ(K+&Bm zFk(iEoJdqNQ=VC=i^^M^+|do%9>xTzi^{VrJ322kgNS}GSC0M%>C`1bWEGVthSG3s zs$V{s6U2;xzQmrau?K;f9R#41qjLiGLZ;p6DS`Wddx`-b&Zxlb031PFiOO{eaCGKI zbd|R_g`-O%SabZ;MdjJm9lazLkQx6!zu{ALOTM(|^9w%97qyR?1wgQV*k&8kt=?OJ+#wdX%N~%7`X3&zpaAsZ#j*KQpSTvt?DuP995a@OtdNaso z4a*2@Nyx&vCLM~W%Z5Gq2)^zJ4JQ{<4IF)HdJX6{-C+yHH+U$hp)-wCAR#6@CyDVb2Yg$g3J$26Cl+3)Q!^pu z_96iP|Bt=%fUm1A`}j@UbflzZ?@v?{|0)}ckw5VK7F?A2jlNn}v|66PhFh)9J-+ep>RJ_;pPuv`cJz9z8z_OB>1E<{+ zode_}9X#%(H0^~x5p{J4@aV4_G7t$1GPe`=hT44&(3(<&7>nar5d2j5W`ZW1W(EXI z3v^q|?nm-I-3Jt?$2yvheY+1RRXp~SHwWU5R)RTj-k4LebKna`LhgFs>PnpEMCX7> z(^7J2E8)@sW(M(xw5@ZAbPZpjlj+Ndti-Ilcbfxd$YBbkwy5NDF*e|)y9y`;2O>rY z=+q0v3Z48c)OXsvkoDzyB`aDlSK%FRNb=U!=IzkBz}^#ey}ZY_qO~sMDjwL235!`$ z0Jz;v>yp7BvW3H_!EaagHC^^8V(U^dZY5hrptl;G>thx*PFjFKuOv4#Hwb~=)|zG^ z(CatVv`cEOGHYSo)av%Sd6Tt<)`s2)^u|u>$YP=EN<9Amu=3Qv!EdET3>n@&_$~3P z?A~qf`uG3xjfa(&t7wg$bF>gAm+9$*^vg}1^8|iN6bE$*awfEU{LcB-a~?VW-81iP zZJIFl^heT_E2_e?{2g~H8(h5BXzt6Pun$ndA$8oldUFVf+6O4(Bx785!cWYlk}oW> zJ5C%qH1bRH5*TN&*rr4!_M|o;LmpN1>=Nb9&lmlDSoy%ch4QgNTG0AaEFF|0hUKJ% zuR>hXCD@@7ykKA!u5E>5Pyf>ocX@P=r{v39X3ze6#o^)E;W=evicgC~dP^vj%D|a= z$eJGZps1@t@AAd95Duk1fl3BD4wZEmJOI|-s-#K6w#=i-EiyZX{;UvM+DcAYaYi|B zK6QzIG>&QH-x+BBrBGx22h><_R-?M&pdI+ zzkdI_&z|fqJZjf>4&8hEQdwr!?L2AYX9*yQI36rMnd=eFI=BM1CazAF?;_@_1%Yf% z+iPjt>GRE-zifZ++qe8TtUPG@`^T4+7e6-&Etwm|^ZRtk>~LuOq%4_n1}krhizRbo zlsnTp`a5FrZDPqh0FQzYl@7&#&968rU1S?A1E|F;2AoMfAI2r-w0o|-`IQqtH$pwB z;=m*J9M|-Nf1cm)K-mGg(<19pkl7lUAQ-=8cB^8=;`_JO(=(42HZR*PxMITr)&JOU-y8q(%yCoZmYtnD=L)4*;%(Z{WUJ!? z|NBI(KDg1H{B83m-1y>ahi}VPWSPaKGZ=l)G7?GGN9GrCh9mF5cd>Nq z{Cv^h1^$0{S#4^>EtU1dKdiWE*sLM%55A#%#=!p$xQ;*W|0*g1Yi6z4E?awDJGc~T zub1uEMVd<%#B-I}D`y#c#b~!vT0=(^SuD%LIl%{moV4~T!G4r{3kIBw-kpa0S(=6h zCK$fKwF&PhE_tMB7m$jk_fVti5C@sHW_W2uwTtQ&H$<%0(NyrbBZ^BSd^8uII9K*O zozVL9e3C{Ln|D|bD67Kg6qrU#{wx~}MTba3GIW(7=*D%6AJvM|it01bJ^=-Lth6cq zQX7Bz=Eq*2ukQ?X%mrRg>Z>_5N>3$$QbHWdTDxFej9ljrKN z(u(SjqEV&8sGtK!10Fg)|HleotC+JRniM%hbHs3S!+>0E4QsWU{6hw%DySK=2moKjR3*tZ44JDcewNgV=u#p`z*1-+Lud!UvVBiVx?B0K2Nleu9Dv${ zSONSr0zMwl5eI>BWL3@X!kY1;I;gawdPX#=o@$!NDx#t5XIjAm<*3<651?%1huB3! zNKG15$1)sH^eiXT`jM#>I0VB(MZl%BAeoXK+TCQS;zxC0X+?EuG^!e+{fK0QX_bpU z>nc%o03;zh$axZbOYQAmhyDyl0Ku7m6ffV(a=RGRmS6NW01RK2+c3@ELrcEPns zIs#v^kU105H_(=;n#kmn55Q3e=c}U0n>1J1>=X}G3&Mz`FM^}UjT(ka=fan`VQ4J3 zS*B?5>uUe~zo-j-4}Y2azg1E>y31qHna>>(aWrTL7H7JB_$r;1cJcah?WyYbvUt`RKDIzf+l|71jHrQ4J`Gf@Sm4fvZuy zmwl=#ie#nePFz(nN-L_j zMWZUT%=HXAJ~TK{`B=dM=|;d%QHiN(IPBX=qiXV>TP8pmHxyWO;hp1xJWO*=(@@UB zD!Mm!D!sI#dTlhSppQ+o@UgO!H16n3O_1`mn@mr&eGlPYaeUZ(6dL)_`0kG_oeOFLqj&AkjAN^wGx#?aRgQ-PYBZ<^P0rRkgID+Qo&D zb0oD5c&&oJ!3jMSTGz;aL3*X0jMO!=H)&KQ8D_YrvI5ZS=d>nvLPY%1hAIiCM^qET zUQ_^;(u!(pG*>Z<6YgQ)8$j!Dn}m?Yxt}`~j(kW(qTqbynv$F^ROSIe7A*l?NAbW50<{;lDyg)hI1J zszMrC7G8d~1&Y|=Rp`1X8|xhVEq;Jz;RMS6u-?z%FXqdMj|swf=kIERSJpSTB*t@wyy!ROLbH52Ek zh4KyVY61O3mT4_8$WhdJY2QM=@(tZZjXPnE64eQ%6;&@9Rh28hj^!#fPYyhqONkEo zI3M|IOb$7*26*r#T~Bq17MKG_Syxy^!Js0B=mX#|IiO)OHfz|B+$v@L`09>x7(BF%m{km3B3#4M+Sq zVIP+~SC1~Os2NkFPFO_%6<;+Agtv;)<| zeJVKBl=&#q;vfp;0>4%uFt~^~kQD${7&2b$$#eBHr4`k5G^#kmAYmeIlL$|9mgKh1 zu|EL$fXkOI!!%ExtKg8F6n<~yHT-qqxsz_#QSzGW# zyVVh;71bT1QMD~K!0r^NgKM%+n<#DC8q2I~ngYLO0XIv0r^2;VX3JxHqB2d*R>yeE zX5W(30N9bo&jnwYxCd}}Nkw&+ccW1a3#tUs?>@7%^8MfG9O63dc40kw{Al9s|7S>-CADWXazrmE>U zfu}mU=ctLRDlvIl39Hq1X+`zvXjD-x&?TYF4+&bx76@vjMUTaILeIo`-K6B4bX^7J z$L7Ss5&+a-UB%KDQFcHUqMK{Ez_z4r-bOJyE8_k?WZ>ng5&Cw`71s>?^^jrZGxjygIRXF6zLayd&o;)WvAu>-dVT*;$2VLDwc4*tFN@z=vjv5G;zshV!3f%Xd{7XWgCr`&L7bAHM4&w^YC5?#?K#p*!t2Uyw?v~~IOCh?c%kB;+EA{vyl+nm;=ogYS#Ahc5EmyCM3 zgA`pw*bwGey;dJTw0^W0JuS{p@RlqV%&zpJpgq^n-Spnx!^`*Id)Dabr|og#v7vt7 zThASz`Y8XHaM@7YQ z$c+SG>4ziFet7j>+nv#V{XdsZ9<$4lmpt^%_=7}+79TYW?b19Ny>Z?q_xy^yV+jmW zoX-(wav9@PZJcUU5wq;%n%OySaV+T~F`0gQdd$v0N#FCcz4lr6w`V^4y(iOmyj8@w z!Im&(By#eti*ep&TNt-EpCit=Y2y@WoMKiHv&^_ns~uW@m{=5NfPv6Mla7d}tPAQ1 zsu_w0kO8?2Yy?f;m!!>Ql4o4=y)Xa#!$&XJ>zmh}F!Ap*8_%6`_Q4xQ{;KZ4ryrd5 z`p>2ufA2GMmCApfdVE=Bu9cBN6Jo(QMCRGl;=EGs!Q1>r`)cNmn(QoVX+ol;7yDbB zPZfVCHTJiRwEfy?(>jMTj#t>`5n43y@4Ctp2c?je=uk8Zm2;=IzfH0&)EI;Uhz^}H_pFTbO55I0#`#onhB9-3kfkajm>tT=okIzJ3K%iXFOYu> zC1rk~=`L-sz*c2hwG2<|HY=N6xG6R08~1InywbKelyN>)?4gwGDk3O`Qr!Os4g7p+yZb8sHRK0;)Bn{Uf&K`5JP|l=%+hRy zvow@@PpKOjt#D$jPGg*gE_MTyA$(|EHhnnOdy4Ujrw6{CnB{ExI$TCjxWF3d^#zpV zEu?$XNcMV;N8QSnvOSK33hsTc+^b6CG?h!g3SaKyQ0@i1!+nu!Mqa_~9^{y^1wx^1 zD1?Z`$I;l|#i=`~qUZ_Au(O?H&tHNAX0rA4Q4^?_lN!uat zf_R3bq`gpx-DoAxf`B1!~$M-;r>!SS^w?kxrVB)Rr`hsR_@f^U_H5ysINh zQ4>?KP{6I?fxVcR9{o&I6EvDpE?O#>Tj=s&UoSwxX|u^f9#z+R zkQAz}ojrM0{n`nOhn-zLFIB$o z*oBZ^F#m>K0R{k@kx-Vc!1Ln5->Uu2o)FRwx*2Mj9URmPV(MVEfM}~iXAqZXMFab> zxjz0}r{&a*J-B)bexovfGAdlMY*c3~U^R`JxG?Y*H7#BsCQpJN85u;Xp|J+>objz4 zE1+N8AiB$%7A$NFmYvk6MhA;Wua1r&`XUH7;SwJr-A5G}L=2rTSC~Q4T|s~gsC?2D z5KO48m|!9lh`T^Su)c95Zry?J(GfCq{&f~=*V76}r$;!~kc@n1BYc{_^J;^_wJOK2p=Gq{TGd9Y!XY)2s3o+nA0ItqQd=VdHi*~Vh+ zdD16u&x<|U(K!@z&pW5AxwQ6?(a||%Akcti8_;yy;rdKbjV**chXkN-yEHk%Ck7J6 zj_MZNM~a>@{d1tF6)!pm;_f45O(iu4KK0k=96;k8nHO%RSa}K>G5!lkiF#?|bO61h zOYQ^CPDoKkiVp%5bTTLjJljL4*a1C&SSJi#*b5ke;4j7%V$00|1@TV$(V2US09SS` z59PWl#hL>=_7ivai9K40=D@;|ngjbh9-RZ24@ow9-B4&yea1Od5~4knf$UHXs4?oU zo0zC0Yg&Lvg_cG^mf8d)V8jpLS~AmA)c>e3IlXGPWGbi~Iok>07EWE8j$S&_I3P{? zcn;_o=Juw8QKI+%xTBR|4v72zz=~@+@c$)y_3wYN{s{C(pg#ir5$KOVe+2p?&>w;S z9}!qPZA2&17}aYNYMRh2z*0bi@+L%Ek=S?f2}dE{^bDHv%iGcsO4Y#X;G~IZB0wUCx-Gcz_gBg%4SrKIIpzvlHY}Dy7`M{|5oqGrhUQ6 ziSwxnBaO2ySq%QWqX{OGwNhf+pI0U%){)|+q&ab9WL8_GDJs+gf9 zg4vB?YFgUt^<P&qRA{{bhYhW~1)S?=&n|5twm z`YHnFlr1Q!0_z@EM+>a3g^LW{M<~n*RZYMe)K;PSDu`|Bz@mhxU(zN+;3?=U*&e$6 zLJwF5tZHKe!cW0t&h!=6wzqJBl?FKm0i8=^Uo{!DDjxfZYcj+htpo+u^|QlaXO9PI zdVWPhkYdwfcgf29C{vZ1x0D}cYMW_nlmf-nRDm+xV_kF#_`pG}P>1P`r5e)GLBKu^ z3j^CpVK0X~B54}0&o(iu5)zS9iJZf89=Q;Vacmd+H9W`QlNgA+SrVoJu7^A+C~*hq zc(+De+#mC?Nw&oJP?2rA6Uw-$F#>+ZP&;s#-MHiNLI%SE*_<3|Fj1%^WDnaAhq*3V zOz6PIg5I9HN043FFWfm{R`)IRc8@NKtTs#l6v8k_f}A7V*Z?GfaYf@!$BjidNUX$a zY_bZ@57m{lPKAEz>6+vsC=u>eiX<4Xb7?Y@FI4Pp^jrv}$}??WFPT zjT7aC4Xfs~PoFodrKWyLQ_ZAV&5g~ImD<_IfD|~a)3c+?l2d=uq`HZ7Y9`m#NB+j9 z>t^gMitIQjwZ)p7=Z*2RbF(y9gp2p+0ufX|KCtH?IO$+Sr5OlO)6fplpbi1XV-n*? ziTx*BTqZ4Hl&srShq#QQKdF04TF}zClrrywtonSyx!LzDLV<8+f+>;;vp%Dwf+b9# zXVkyqG^0DVlHRQsuo2)AW@m7>gJP2Pq7%Fb^+gb6mQ7ylE%604q(jMVPW=@0>R3>g z!voWoL)aus%@UxRyVa~MdNI_m4&CNcGaR)HOiDlunBv^B8;8Zk;Nh|i?CwQTd5re z-jW)5M`io)mxk^>X!^kS2FvAt81l%#TLxV<=ozNfba1u*!XLw z0XvmFRJNpSpVZ^2_HLKTvMaO!qBL|;+mfYC-e*38$LVCwL6W5lWqRxs^L`z%s_n7o zulewSo6r2*o;y0`VOP(oEKglOvwW1F?}-u&{1e_wUq z(SI1Y`_ZYd^zcNcZT_#h|GIC;@aNBdbH&wdKO5qobmQ@tpYem#B?X^oVv&>ofzkuJ z%)PLo=J3?mVb@ib4L{Snu3`IusV^6NA|-55m&^C{)DypbwQ2SDezc2!^IczE`siJ= z{hR-hI=AB5)U3R5?BR)*3_I$ouOG2!@6W$|^Ecw3f zfB29EGgtpjuK4QbP8j=S@Zy2rxqP?O*<|cYtt@x;sJvsL?D@=d2fw%Duz44}zWB~_ z_g)>o@We;od-cr14>W9!D|x?OJAALZSA2O^!{g@E$KG4>&_ma`$1G?oKdR6h7JlHP zyX}95eBMjv|KqE{$?6A#eoMi7(y22+m0dEXD z>$an79yz*UV4(+cNG|>(7kr?qK#0x%wd9KrANQZfr_|j4`k$YD@rILLKI0EBytb&& z1I0&H^FL4epPzhx*kJe04ezzTbi$J_UTn=8x#w=>gV(11o-c+fZ-`e@-mhCzPhWq} z4|bHl`|h5pvSCN<^}uf~zT{*ccx}=Lj(_NxGjDy{Joou=yS}k<`IU8l^$xut;DL4d z9;g^#RP(-{B(3<%m*3T{JZt|CPZ?Qu#O>3oZ+oJ;(CX0BD*Nd3AGI_-^^4W_RJ`u(&?`qklIJ7(#;MYo;Nc=*5G z{dMEeAEag!dSIcs{Pge3Pd#M%%D;W(jth3%{m|4Ne>nI4F$K~PVQSPe3yjI58@4~< zr@wx5xAc-zT0Z;xrPk9c+gFV(@IX#t+=vQ1uxi49Z(aWB6JNe`?ed@6r`$07JQ1(r8d3avIboA14;m!}7$FM9W)J)U@|VeZ?Dr&JgG$O;$x0$&S{8++Sj z@?@!|a?P+0PMT^VEhN_8+*e=Ka&3SoSHY zz#H;FR9OnV;TIqJN3AkXzTxW2{{5ZxE$>`>;;X;?``(2g2%oXQ17F?kRpqK(?SXGT zedm(nZ<{~i#ur~Z+!#I~_4PudTxehZ=()ei``y0&-I;Zbo^@^G)PMZ)k6+%i;2V-Q zg&w%z=VOm~?XD$PIgJm!aM7MCzo@<&-nx5F4=gk&)y9Kw9rfXre;rhD>+aXK9KYXD zr!QZ#>vrel{m433PTn#9*9CuAZCSKM&X;8`zTen^2g6b~#k$kO;{xBu1DJuf|A=*>>aF?5M4I>)*E|2}YkxTW(F@LMZ~yr>c6emSjmH&uAa972Z-ECM;2-nW z&N~m&zkTSi?|Yy7=$8#+M(=#kh?cx*BOX|AakzK+y@y?J%e;T@`_a4s5A1rvr~X;@ zsjK%X@P-Kb7zK}Wi`TyWoux1Qb;IL396$TG6}}J z+)i`GQU=)a{ua50N_XF3rvs6^~xz{%6HcPc{JUGy9cS^g*dF6?~#%tA(C;`$Ok%U;dww zhko#ln@(>ZA%9{2-~DuP>hpuD^WM}k3D)sO&i{3x|J7XvFZjpb{`BOZ{&3nAr(b&e zV=LcEeJ<}ic4#GWTW5#NJ+WHazdq{pHy3ObF5&YS4}=%RDipMUui15)b>t#smvg*M2?mz;fW^F4JB-1Pn#fB)_p zbHFn#H(zpAZytEasBb-VZ|aIGe>v~j9WGh*+8gK2esA{b!Vi?}LT~uyF8eoKI^@aO z_nmd+3&&hPa`(%B`k$rkJv}h@fMf^%uK$jo@nh+hmTy0Q?@NDt*f9^|n-#qWd^(W2!e$9ruxz8ScM(Vx1PZW8`1*FiJEP7#i^^0G8^2{^8 zQ1O^o^`mRPIQy5sOl8F^n$$n}W(8?L#+gMX1{X~;fNwsEJ#Iw@xWUtT?IpVp<@|F`wG!MP_keXxG-9d=Yw zKknfjg=?11pyYhw|BM`Sz=ZcJulvTe=bv`%l|Q^_%`T@jq<%Ey$A#We!RSmTadNNR z=ln&!^Qd1OJ8;(d+AN=ze`G*%jk$SN3J5tkPlxZpFe8;JEj~Rcsrg`2EjvsgK zFFsf~bpEnahNZrf_lY8HI64&MAMUlHh2AiC(qun!&XP*`M(|I2#omPfuh_kn4DJgnlX z{Kto$ibi8sUwPlni+B9#1;2jp?B}%V)&FW8bbeLg7#amH49`{`d6%+mg7fLs*M0w# zi@ty1hj)#;W&g_4@}8bWHKE`G8|VJ{$-VMsvRDyVr7^|BUnu9)kRH3x9PyXW{rBG| z{(iT=o_p?D?|%LAgPtqfuHX}eGb4)(|DW^gwK2awPWt(n`~UXj+=lhLKla@A`k(hG zs~Ennz=}ZCN#mx)DwFex3%}-%x}o*p;m4mf@}&p=xZe*}o!szf>Z2ask&_&sK2No1 z>XZXdu5q9F?dMOram>B@A9U-6{aWr!eX!lZh2K$;FaZT9k?ZRP?VE=kIb`L}FFpBy zhN^v6U-glD?TFO-1>cd@pbY%b%2VL4T6pOHOn*e(`IFD3czpi@nmJ=SG@wK%hF8%y(84}Rw7|MQJ~&Ks3_am3CA-cjs%Ab(^jcS3&Zzz0toai}wN{9_9i zUvt!oBbE(lKOyzI0`I6c8G|>>E2ax!y~d)71ax-d+Cm*th#N2y)_ERrf{m|=b`|scR)=L+?Gycze z{p`4X|I?VdxrZkzxkows!~-uJdG+$zcP<_F`-vyN@#gFUs%B64SL!;h(D_=kVr00g zILL*W01m(`|5+)`OT{KJZyhRquC{@@$R zXAJ!Bfb00<{;#4UfT#LyS?tx?!KHwUylkg$N8!F;ARQ?bqHI2>8qE`)r>1SCb<9*f zY``Vlk7L4pYl7v=odv-rM4QSTUcvub0J)&&2~QWtQansXZ5xerU$cAT{=I8yMfEGu zr~)=~Jy4A4zy$}YAuEKdv;dz96;|%5p`(RKqY6O|FJ)Y64I95BO$xDBu_YkWpifbO z(OgOGg{S&1r4`ln(Ws)#C)|J4w5$l%W6vAgm`Z_HxZfI~?QD*Wj_Ie$3b1o&MYTN|Rm{INSJN?%)?MKy5BlDQs*?7gzZfcB zl0Hh{@c{qbRsck*r7$q?$F};K66?MxwGhD;97cIvF>hB#fK1&2}X>P4fDV?O@V3E(!k_g&wv#uxmlJ( z1pF62S9dI}sGb;&s;>&)WAxux>p+gXwu1l`RAHZG5F$iq7TGBA`2E0k?4`XK8+Q>j!44dV{e^Yb~RS76* zwgWFEX|9eat*GuAjjD#NEp87G^)c?_WF>PS&>-Kjqi|rQLPA?QX;g81(QJpEUv+UO z&~yj+LftmfC_yWsZg?u*2g##aSz1vY6pgB>cyj1-lnQiANj2e23cC@`8Phiiebw}n zbIvG_YlZ@)h5e8fzzE3Iz(o?+q@|&T8WzP_&ROy6YGvh&)WGs1QWe(@d8_=0@*@UZ zP}GID|1bAPpwA*O;Ldt&zpeLqfKzCBcn8==m_VktF9kgJ#1sf#@P6Bp^mq9#&*`l`zm8mu}DY^R$LjKd(Rem zO1Z$*IpB)(81PIah-p7kHxv(#!Smbl8C#=@Y^UHUhivea5ORuozt?&RA6m0UpUpnr5d^j)$JgB|=V1fk|0i zY*h-!uvi(p<*Dps>8d#^8z;?KDQN4p^)uUQr_NMsCQ3RK_onIf3m4WXb5_@rRTnxl9o1#zSV`AYl&o}Md(^i8n5!trmq@-gS8FxOhUun!QF(|cF$_E+QfEXu&Ex*3jtf*~Xpf*mO4D-LS zwRRq7Sh2n~$xRKb{HE#i@DEmIsJ@B>8@H5_s4V*rD^XFl*&%@jBMtFC4PIS zEXV2dN1rClUNh<=^(ZrB1W8sn%O^UEy`ZHf@cB2P)3Q7`Z2ssrXZa!?x3nd|lQZ(? zP2wNskB;-93hH118_*H^=-AyhllYAD^vurIvksP8#ScoLgO#<t!&$!=omY>Akbd$Q95D%m4zBRJi zOs?*>ezsFeS-tzq(X2K?&yqsUvfyrLGWwjV91gm4hG;QLlA}0!T-nazvznrXVY|Ma zrcWaJ#umm&A$MpOpbibd(7_Ip&A6+1Sll1;ajn&HK2#4`4f)*2`e=67EQ}R0Zq2dQ zY9)DApLfW~+5XWNv{Jz4E#B?((X_UG9h?k^1Wy)5TmY2l@$emuIt^!bQ^&O~X;;oP z4aYL6pSz(+aoCh~A)POC5d`!{zmK+G7VS_{x@c+@uCa>pG0CEJ@)DC>in7+^?7ULF zqKp$NZ{pMR#+uBPJP)vju!A!NZi7-kdXtPHGL&!*us{eH!`Q{AEhLN9QdjROwarr& zHcph(#@5D`>GiXk8tdn@HPnyS8rtPWHIw{>4U|f2CQV*}hGK7I(PC$Hq${fH@$^mC zhbMLHfKtM>qEA(OcXOgk1&Jg(t{bESO@KmWdiO4D9WbJBI%L(AxXZ*`D!Ok?qOu&d z(1_d>HXZJK)d4Pr>c6K8FAhgTH`r0x5H;PSy5s{Q%7Q1N3rbNxt50rUsitEz%$GIEpq?=fBn!w z8a2Ulrce`vk7F@S^)yPQT=p^ONL#Kb%zC=&crN%^Rn4V!Q8ujj8g_02PQX)9v5h?Y zTPzb=$%r*mK?QX`JF8P^@y%xc*ZJ>?*x=lGb60uFl*Ns+Cp5XrtW2T1 z16ZRtU%H_mwFt-f^$bmwP?{=ROv8KS$9$qXP;rXjn?;X>qLRS$vrDeUsPuUfhs zdVwkXXPkZJC9i(iy4$MxUwP$K^I`w)&ouw7KQlx`w|-{C8L#A-QE}CP*<{AK7HbAK zTODXVj@5w>$`4dm0QN%Wh~+@X0~;2qxpeEw(#5YCiP12e)cdaQrH$+5Vj?`kGpKI3 z4jfQCkE)jnFNs5n%h}u#0un`}Ojcoh!n}rgD`ggxR#&ZFFn8K93*E)F3meo~OI<}< zFjHCB(x9|0bz8k7#iJ7}HgN_zZ%XMnmv0<`=pxhk{ra&r8=L=<;`XC zG#mvPABe!R6|pTj?!&vxImxj;d(oJC{^!oqhQD+DrN6vaFFSQ>rj)#qMlAxGIX`x_ zO7B6OdNY8PHBLvM8!g^0? zY@P+cs-=~C7I2jXtLl)dZk#kzqrocGPHK>AR?%o}KBl>*DRFZuP97D?*i9qcnWfR+ zX{K(31O|2>X(77tma9|4u;DI(UslpSrzZzohT$2~sqdvey+XZg?9IPzf9W6h-MOE1 z$CV??F5Q|LyXo!wBx8+n9lGpEAb}owO~NKVEu(M19JgT5iBpAd8@}cG3L^Y*Gj=Ng ztdxf4xeMziHPSjgW+AOr+NrgzR16l@v>rnh!P9H&y_I!StI6P&*4pIl)Hr!mC}TH` zaIWvxR~?&KW7`B=MR74QQ~$2l}>_J?PRR^^;*OqfhrIxMSiWwV`a_ysx zm3wgq(3HUSsduI&S(rn(+>Qoz<=Kzou@Wb9qrH*K|}RFL3M5GRcaBy41Wb0us)|GzH_6KiIj zt(6o?lTZ=Fiz`X+^aQryAQD)mPyAK&~osFt{wkwb*T>WggaIRwbw1#}e6E1B~C7Arl7?o!$r4`jKoN6g>O~;}#CU}+N z(n~-ULL0h%2)$C%Z7pt*yIZc(EkRB}G>%o11siTUxytPaT>#b8{gA#>a;Ey(a%n}i zA-V!^i$QJ;mYR_%*ZX)dJ21@Cp~*=c1w_+v67y6;QXMuLyR~Kscr*k+%zr%$y0Kjn zDtUA&lIN;aT2Y-Gjj9J!9&osyHUK9Go+|u*re+$jjbllUX~VHhx&kC}3e$V2{m+_Q zQ#W@qyVcmzimDxrstTpX(S15xmMMT9FrY0|!P1Hq1cEq%0(D~kzYjqZUMlOV0OV>i zbanm-LvkflRZd)-Hz_G5U0170E2@V@qe|`HhCWW=0{LlXY3NdB_Sm)qtk{IuX6Ph7 z13*%xPRWhOGoS!#z@AZTV1;FuqU`2*2BJ>nSI3l8 zRJZ$gG^)0Y=PZQAw2s@ifF+8%6&EZJCzhwV5SEj>iQA!Uf;A*p(P$y6RH0pkQ=4rE zAwsPztXg9JKiA)W{lC7d0dS%^s-$vsM3)m)$mV3o6eDe#V4QjoltMnrPy1$wSdC|( z2xKQ+PjxU!KIZUgTG*8C(E6a-sXA#{R^U{~uAu~!?vl>@&y-eFyPT-dJyT>ikkg9l zF*`F8GvFT`aLPs~%aZ3OKK;8!h#@#6X)8qZ1LU~Nstx7Qrhp#^mrND6+{95mvb3VQ zPjo$10!g)z*JaJ)u&S9t`^P4!Oym?rNz|jho-|h-$2ENkwI&5Uc=XzU(qX!8f+)xI zK2WI+2%=fiTs@++qFNb^Y9PZx3^Wzs9=oy@Ab14n8V_`lFq231@REvZ&hPt)DN zm6PVGFUaOx%cy<{T~!ZtH8g)HF>-A6WX!J71WF#&aita2H=|MY>8Wc>Mq2DbAcC|N zhjW{As-HRghe4<%jVjuKDsttZt1MSk!Uf?^4Q${Lswbg~YlXs?kHQ3&gq1v;iN9?ZrS-Bk^tpUdg1|<)zU&AXhz2 zOWOf6BJe}kRRY;XWi)xNnxz%h??$8Q+h`m^nWKa(Xt@C^tYdSrf(_${Pi=95O`5AA zgc#M6wKU@Rq5`OLUlac*`Zb=6k_UILU(bD^D2cdS=Uv;O>somCO`+%rGXCs&vQI3v9{EUsOqH^)k~sLrEbcO3=0Fg z8@B+5%95y2@g3@_0`ZNwbkbafYT;YH0Xz@tC?f_-3^fF53qA^%9#WNPWhczl%0Y*v z1`gOUHEd4h`2%*${_3mSaQ}0*#R#n1?YNDYbWbuhKa@ldPXGphxlk$$1K6*qg*&DT z;#AJ%ZrYDY7g(Dj;GliPR){lREEKub!+K;Z3|~@yh|snX3XN#II_R5n(J`sW6Y zF8M}UhrWspf@7d9cI=_I|NMv_{_pOU8;+my=~>tBwffGThHqT}@g_iYUx3?s#-tZg zXw*QdvNtk$Oj^U5*n8o3aA))dUyoxsm7XFpW{U!dn`@MY)>$o$&5Kq|pVzRWZr&UK z#MKJwVOs6f$pDC}<(g&`nI zgzq@;Ni1j>Oo9B+72m}DfPe2)f2B&kXv{-Dd1UVM|NE~izVoLS6!X@ygL1<$5q0RK zQemWM;PWj;FOSG*KNYL@RIE4yD28BC(9(2Tuq^P$w{_HozKl!N%Z{NH0;1ieMdi6a&+9AeP zrOCKSWe*ztTU8Tiq{BvcN}vhNz&<*M#~Hw0ZdDxvnQIp+W~-_kcztS6*_)}Me;Tq| z`H5w3cK%AGbKYIcemJ!LU`-paew-1|e{cYJOF&7f^k61qVXmA8RHFkUp$4Rc%a^nU zqn5NS43>>r6twTT+k4v$ot18VYToPjeDvezU)%fa(^lSn$dLo)AN}Zw=ha+2;kCm) zwZS|5{e@)%PS1z|b#C4r>RB1vQkuzn9!Z5bFC=2#U529TYi9pWMsThRQG6?k=f(Lb z5$%cDZBFab&S+(iIywv*p*$lYDlb>zR4N1_*6L>;IJADW7(UvZJrAF^WU(Mvrt^&5 zP4DeJynO$?XN{hI+8!q!8|wGH_1y8PkMfQkemHo}W)8{SG@|0XP42OayyGWu;E0(R zxQg4$?&%yj6_F_H8@W8Pj5F_SuHYV3K{F64zpg8pG%K~lMYTKyqknz;=IkaFmQ1`M=>+bGKAKXm~s1e4xDG%p+p-fEt$CP z!E~dU;aC)L1Dk#({RSUHw@m~0o3rZHe6#kKdi9PshSxc7*8b_%L+>bZ;5aD&Wz0Qr zao%QI7`Ql}qnLpsW|@JTcJ_Wl>kku4XIWPFMlIAn=!DZtrO0p#yl*vNTk zpmtSFy6c6vhCT1U^v)^MRz)o#}pl1p(uRqU~p?J6SJF_!ywjwKFi zDz<+PEH#0BHRz-WD^XliTx|b*AS#Bkd5C`1a^62qyL9f8Ynz^ZV&b47*Z=(#Rmh(K!iSuBm1VauN!c zMkC*X8sn96kKX1l+E+u^7n@_8Ko>KXL{N-nM*qKTWvX&aMeUGd2047&|Fsn&us%Go zqy$Efjbd4JI%K%aD{0deJ1d6hY1iQYLWq|>gf_T*ad`q0vy1kz!A6&X0+Vdhy%Gaq z*$m*hU~|cSfCqxm*~VboVM71`E+ckD_V%Q+UEpVZ?5Ev0A2BkaJL@|UEqNWA+I(8_ zah@)gw^*v%{2;MbbYU3ca?}IKxVm=s(Yx3-BYNQ>H6wwg#86pNeBfI_ zsN)K*0E>5IWWBa=4gwz*4Ij$PiiEO1&gzMxcb(~@2Hezl(wNTv9X$GT#h04og$h-rkE72t<**}1=4yz9HsB^{J&5V)QJ-4#+I zGR;6V;en%A0L{jgv3=L#I^1+4ea5DJN7bM^y7aMaT5}^PWJ@w6u3a$t8g=pj zv!ArDB1{021}8R5*wPY2a1XOKFT}wj9-F-TsU~j^Y15fTgJ9tqDBAeCU*y}q#sQW=iiJ9B88#{8l zq9k(r+tKCSQ;;%JTofK9l&4^yi?qT&8{=oX3nuXm<8u1DXPmdG@RD6%H*@2PH$*+2 zVRHYNZ8)n;HfIq?I5mdKCUSi%2I+$Lrf4v0wY!Q&V-t(j)4}d zu_IJLg}0WA#TpyFLy5(=P_<0Hj;>u@auIzonxhh~M83unYFI9CPG6ABC6vLSNt;Yw z-@!|^=!lY`)HXHRxR$WAF$G-=kwNr^u?RyNBbRnX{2V=_r4=qU&yStJl2 zUMK?VfKC*yVz`8|!T80WNAZP5z=?u2k!2|{P;zlYLr;UY^-v0tF<|S(tUtZvBKlY~ zqN*H#f5&T%egrj48{HZUS~Vhzj?ZTA_=Z$`jyeiwP!}9CvMmgg&O>dB<1WAwEFe9= z&2l##!1y`3s^lVie>9?GqKAHjl9p9rQ6R#=CL*O(U$(hTal45v2)1djR~*wvXIM|; zoKB9CmIyWmmaw5fzE1Xi86~`=cc!)NB^S}JMiyX;>Zae4-M5{udnm*K!j>=)QWE<~K-{^-R{Yaq`bJ#PN!)0~!5#kYrL|4`ZVhkV=dhxmDn zlNGh&pR~+h*+ep}m+97N^z5pTQ&SarYg-sg6j4-k-aE)a*$O0Fj;>v?BFwWoK2GLA z`K*Wvnmd}N&6V2VD(NA*MJEPV95xsZ>?qE=o@HW)Bc2|^*hSE^wv2mmb$iXKIqkKR zJh^7Jq%}4#P;oDA#f7M?p|wV?#fzwR)nuHDtJ@n}=PqifZ|IGCah$|vmlOz=liHRn zZSszc(7L(|Z!QX0Dpj#jssVty#bGH-%MJ=>unD;=Ic&-{)gjI|4t~<=t*NK4zvl-# z%HMr=&s5p4qxO2>Hy2;BMdo(kPzLTqyw@_685(_;&$D&XwUAT;Dn(&HKRHclf<-5= zXqF4_2`-xWMhMR{gZfYGY~8X_4KvlcNi9vatux!F&zpJ9(TfC&G_!O{Cgnt2>(YuZ zdO{{zOfEv~!UQbfBWTEU=u5u%@Nxfnd`ivzumAbk7jHP}SoKWHLY{>#(6VWHqNWo>gsEHvvkHu?7~ZD z=kVrQI@h%|Z6spLGWTk6KSC~**vpjfYz?ynoSN-`f-~+r-r}`ye`o0nf8Fr-4#&?v zZu!ekX&2nBl^vM-^wFUiBCIPEC}J3R71vmO?w8}-(HB?bb%|HRDa=FkbUDp&gV#vW!Lc~cRW|gaWxKU`@p&~(_ zRs$q4VZg%`b3HFDS%Kl(3YAAR%zK7RmloV8&WGv&M2Ah5oQIbf=joZvxv4cMpeB(8 z#j<}SfE(4+O{$*KuzvP&C6(3ZN3$BhgfPD`(ki_sZI8TqC=#tlWGz`W#Nnj(9{NcrkQX{;u5L4@nrP?<0XmJ z8t^~I14I;rm6wvnuP2wG-Cu zlLf%7JGr>ov&CpwH*Odgoj?{`auX6dGsYmvF~Ek}!`)AGb0xrl(A49)wr>h4lKml_ zPoa_^1W6PKUYs|m03r%&2tg1#u)EEixdO-2IS=Z=QH;v`$*6G2vQeFsw5BDCM@?K9 zc#E1AFA#&7;73MA5pVTaqqx3d-QFF?%TnM;-?(9i=m@HI=$ouZbf2IoL;J8mKe(6= zJ35MMdSGEWmbkbZgD347a^R{4#pMwCo`&W%vtC#>7y;)?EF9Dzi^z@233NZm=KV=s z=JHll^^Nnn@}4N+^|Cy3(nFUdv!E{7jD#EK>Eb^uDEP8**_LvZc;oV<*X`Aj+@(O2 zzVW#aqsi@K%1tG}Ps<90Fiq^n4zd8~t7B{=s}?NBxD34RDd+V-l#Vw>3{v6D&i!My z-Ly6=8ru0P0V8p*TjQO*ZqJfR>sO*_9he%5U0_rk2f7VKFnVGZdRaIQ zsfq`)R8ix4r52yo*wZ60AP`C@SwNgZk`t|OYG0NVm;v_Hn5=f&o#XzPk1MU?e5k^! zwPC;*%X9ya^YqxOH9sgpT4x(_;{IPz)|eV`LFLZFudX*d?ANGIs zA_8kxtnCmCSv#w^qxu%<3SBnp;^=~5hiJM(!A&F9DY*SmRBRovE3P9hdb%5XN4rzsDYdgRb zYp0b&=)1I;S*K~eVT|qvHuwcmLA5biB=nxAg@EC4&`W&2&V4r66mig&3Y!BzCT8DASc-D);Qy`#BbB2ix*O?Yk%s&ApU&oz{UcSQ(HJm4Hu z+!Y6eEsA?vEb1O~72KK;6yW?x#a>XAO%N=?4p$Ei^1B@1&n2?Vv_+$i zjJ+QQz|UQani}Lc9e-Txj9?X`#I8HEf8%-$CtPM+#Zc4`%&y6DL+G*R7E7GK4S}Lm zErU6ie3|KJhn7gfFN%gfaL`D_a2dQm?HNsmf$30ZgfwWYp#v*PSVJV0JL_qgaSYVF zkyHa9X3&cbZK0bZ2nd`7=^Sw`RJuhyZo)5H+tHgVk*J>>jk*RY%`sH;Yy?;6#wYaXr{m}Spied5A_fzyB@gRxXkgRUZ1`Glt|QT zqfuAUdBF;sLXjqsZjOa|0mlktb_^Z;0u=(xmRsBj2MU`43Tr((n&BFbV#Za9>Atx4w8F$O ze%X9ok013}B^7lu8g)wO1QuNqUG+dy2^9ns_&LZU*W=mrkZ9y=v2|UMSx4AE**-HS zMjC@JYXbO-Ex2!Z5a!h6QJ+~-Q9mRab?UCFhDv4Hp}j$k0}B~$E5tt|?W3Z@?Zs_t z6rby|jy$LkpmL3nKA6l8cLHt-(D(t5 zl+EbH<@f*Xf4XMbTBVe-c;9QIS*$ybZ*kvYZ{qf$32d4z+H}65*to}GL*^&H_4l-5 zqru1pOxDwiL`#XI5Ug4msdI&%v*n`*H^=sFy^oW#2DGqfWmJN`fYo z=>V5&o`U2d&4#o=-x4Jou~#OKdaPOtyEIoXDB!5wa6ZDIGwn(eH(>aSk_B80sl3gX z?YOm;6>FuE3jLaB=t(O(KnqZS2l(K^$-+T>Dvi!Q1un%7d~*v!-UvfR+2V{#YppXH=b4^P-(1j$+ zH)NHT&z8D>a{hKOQ~>l2!koGTGT%btgs3C8>FGX#`H62rE7n$(RKmAJqwc{9H$pCc z4tH$M8txVq7Km}QmPK8ing??4vM6EnEdJ=Ynh41i2+NIsC_i1q!M0_hTfx;Xj9KZ5zD8@ zBy<66#7_~NW1g#FfTDKtl6p@GA0oeqRT2$;tZ2AJ0L!$2({P5j495%cphhNiGjdVf zgkQGykdlgem;1lMJsYJ&DvY7e8D2)*&oxMR1ZNR5MkF0U&n>ph_yO|Zv;_erv(RaH z4ztG~REvK!lvIndkHEop&Gnd37x(``15QY7=MVeB;0ySs|EoU&eHDT8#?UjEh-btX zjD&6YK85-p|Cv8T7gFrCLGuG_K5sqsOl!!p13>0&b9*u~DR75m5iW(<{{R7^_#5agsSsv8qstGBSMk_h%)d@eqs`RBGXL25?~0gzeIzLsd$b~RK$Uw^W9~3Zo!!xRJiip6 z^9?h;7o7sWq${+Vw6tM{Xt+|l<`jx^sVO_~*%YKQ)wuiR#BQ#@JzYFp*T8=S*$M>k zS&I!?Z*W8`YUnniXs|-?z3Dbb;{KSA3u7GTL*>O7XXmX6vYP@2yHGbWeVnIfrcYtf zpN^Sq=5o&K367b~>T~y?u`5>g3wJJsV_?IK%c5zG#V#K8xO*W;Oj`SZQ7uu$L*CH@ z42kzNh{$*q)9x#~k1s3kW1O)7#iJ-B1R=OOQi^s(BOOIg#*V>Dcnr*}>`57)mMydJ zV-jWd(UB{=ac6c)uKeDd>wA z)O7Bb!$*$5SrBaECB#W=c1qD2NFay5ZZ}_u>z*?OM+t+@vyT~iS``+dh$gbtbGS?g zA@@0512*lMzdvQ&_pcf{@1kpN`Qg``^x!YO_rMpPDjSwt(>ZhaN}{D}^ozA~i|aVw zCOX7)_+0d$g?!;gnN5`e7AD0_t~q~?sMK%_ci=&$hwM~%F@M8>`zF#S`hsl8n?9b zgsz6bC0U*@{6uv{c+#SuSIl{PD+#Q`8R)`$MdymqSw)Ke-mm|kXyxk+VQCfX`t|=e zdhBE!2$X*P|8b)?og&4Je8LjHUC0CpW2ru>5pQGq|EV>p?cN*q-ND};*uuA4`D=Z+ z?%*u$!?=B)?$zj6KquRC z0#5^^Z?VLJJfR0g)le3ERyeePid|XnRzoc6XZ5Q5-52V~aXw<+IyiiG^u}}@Z}QxA z@HkKJu@0V5Mv|2(eo%LP>1xPUkZir8(-;ym50Iw2OeS>jA$^^p)RK( z@(J_;>=0zQTo9~D3*qo!;bW4zNX1fQ$99(9k!2y23=zeUa6*@|@YJ3P;W#MKv&@b; z-*ogWi@JVy{`WZP$cV>@q0da0uI`ArdY*Y*g0dirjX5HBl)$rEi{slmMd+d}($2rk zTYTbLq_IakQm`h;dMH?D1#8YJTX$e7;RJ4cC5-MDt}HwiC3Gc1ADn=TJ+*7XxiKJH zIYD6AW^700Zr!BVVeM%W6<@MSK4&KOPl6tnyyzr|i4$Na`E+URBwf(qLV({4a1{4! zOw2UQ?y1BeGNXk7TsIBtAT=&Tx!WXwWsZ;m(z%9=c8#WcU~qx}K9z};Ip6@DeXHAX zC>OFWvXe-N1kjfiJ3+FYMAp$f-vT=cFP7aLVof4h98HpH?Uv^KJ@#lvCsF*J%#Osgh_{7Rv@7E0oV^!*J2A6ZMjLbHRnvcouq#faH|}R{B}IV zS>@0_rv?qYJXP+G(1+|j@bc`hvR`cFB0&F->!S#iJzjs9rVUs>11`U%+U$-p{AZvv zP!es~qsZ@CRQCf95h!!>`oqaAK5$97yiWfQT_2jf<)XP~1UZaW#iVDNpX~z%k7)mQt(T;=6X;5zB zI3?T`QQ}UAMu7H;72rlJZyupLyq{ck@uU#Hy;jP6ErDCggCp2uc zp^WoN+ul&d`BX&>rIOJbUaQX;T0dIMN)7dC?uX(itY=&(bn-c zsYoyVaOBw!uik6BGup5J=hDezb~*BrhrW5!dwUNr-+%8}qo<#?$BD;=`h9ObcYNxj zyc1Hj5P{31LL298a*tx<9s6o7&L4f){Lw3%2Q&6-`e4n~65^JoX;6%_C12ZbAl5F}&3Q(cl zd%4F#XB#2`@gTxopDl;Q_#Y*Ov@wq9_N*E58mc4+E*jk7p34h zpQ@OlB!XfnQ>k*?IxE3=~CzQaI=Lp&fDZ3#mGDM)d2Ql3dZ>y#f%#<%Z}T| z0}b*D5+HwtwNjYt0eeTj#(*Wm0_UJB=^*L4d1n9k@h2v!Q@{SHe;qLEfhUhV_i3~4 z?emMAg1U*TN1g*soL9;{c$>dyUyWd2Ou;yxs+gf9g4xw{Ugv6Rfz8ofAb3uuG)Aom zO;%e^BOh;~Vuz!ht#2No@xu)xLK@MGtH=ismJU+hq-CX$%B&!%x+ zY1N|lLfucI7!E;J2xK6Cla?hvR6N&^ zWv`pCyr%ci^6L!PZw#I>7neEg`6Dy^t?5z;oHdf6UYR_Nn%R!4UxEv&!N z=;cYaLoXFBdeW#Wfu`evAi}5$X*vXKMc+1!{Xr<3ic5u6`yORRs#Y>f%#LPH_lQAVs^15P}O`DKvE}0DGA*26oKDx4Abc*+!lWYz}&HvqPkBssvg_1 zZXtMMpj0pPNVT*TVA6?Lr=)8jzYIi864IR&)W-r`uZg8tS0xS^#M7r}ra^Y5?&2a% zwwX><{HWq8SV~ddJ{na%Il2%Yd@>!_17!+MKd{KtawxWMAE`VoF?J3yJ=a8t2I&JK z_>Qz7+;rj__!i3njy(>LqQp_f)UK4G+QsNTa3$Xj0VA>uumad9uQ&!Ra6F3XjoavI z>T%69x~%}3K=%3yhhI_F)$o4gj3sGlTI}2`ZLDcfFiafPJxVI7JG~LjRYz8o(5DqB z30Yi?WM!5D$P@-4hGRH(eCw}nQFW2qHCdn`6oup}rc#{RbRiR|s)0}rG}Kh7f# z)t^P98lWDbp)pK`3S&{lg;vU%3WV0tI9Abrl@gx;AZdAMAyDviSW`3=UmX-5!?a@J zHEsJEf{a=5lj2WS;{IQrIyE)?S3^G-e9*v|e7ohp);G-PK>U}2{c*#LGou4(Vd=m` z5H?Y);u`W!IMmrlx2Yh@s0C}#3}bGP6T8XWncvoJ+bo<;nmUNy{n(IJTQxgrIu2(|$5X;$n;R*+5V18pj@71(68$D1w*> z8JW5Zuf^Z?gvsK3sKRTtWmpJH<<9CjPmjG;^Mev(b(TxAes)bsW%c3Fto9^X$%sB< zXUDx#lz_NaI%zME(2vD0SHkou-bKAXt1~d6ICdbpo5;0@<_toB6ewwVDZ*7!W@o4Kqi6hM zK4#VBW14E4k7=%L9Oy<&R(tVNBjliTX*{pQ*xn(LD$uc)bSXs??zxxKb` zs>`v{I&xi!tGb-M|KFObJY?8~gYO=AN9xusfBjGR6JUPspIz8WLm~RcKfV=R*f3Ui zH4R8%=w})=Fy9h!P;3t_C>V z;_4)h_6ZlZ!V1xXZ7tcFW#~n*rDO1Qq$(cE@h$0EQJSQ+*rOGhuW|W0Oo9%CNr_B? z4fjMRfo$V!vRUB38tb5f} zXaCl{>mGIQ{r`P83Eprs4i~7mHaD%gBq!(G^PcznzW00I=Y1XkLBoYsKqe&FR=(7R zGA2a$8nu9c&JK~S2dYvG3LSwawr~yu1g8HmW`w)u8@I@_bzdi*cDNL!0&wo=GNC_j z8IG?aTR{(L%1ROUGQU)kEzV7KxN&3kV&Edf*-?@$&e^-%xFzU3#85WUPqHoFVo3`k zD>I1FSH83;d2WZamL_?TH`d4?H}BlgsVH`UX9N*CaK0Tk&6X12m;s48A=Mcnbq&_T zMYhtT=n-17rZ@m#LL;^4u?9Q0wIWJ)AkzjI!sfKYsecW2Nvk|X6&((pL_VDnrJKG9 z`3<_ZAq5fU7+M1DK!D}K&@m&(bQK+3VC+rOdL>G)pQ4o2Ojk-rl|qz0eO}q9+J>@G zQ|p9WP1V@ZY@9~aRE?TZJ)wMR-GuVd+FB5$$M&M6H771nI%^KVz6@G(yyUTozChq3 zCtVCB7~{tPCFW=#MkC!)K%XgrYsAH$#9a)2PJst-ZWuZ#a5{~FYFa2l+6F~%58Whi zg;td4pIL2p68AE{)S46LraE79Sa2*hPKhx-YqcJQIA@Q&=5V1jQsB+n90~ouob9B- z!TIIX|F=tXZ{Gc9-4^(@T3}h$KADtus+`aU%>|2ng!~bm$(GK{Z>b2`IDoql88m>n zX~5*;&ipA+nG1{)ZY7;_laJL!0*WXDKrXXA&^aKv?s`l+B`_ZlI?M$Y;}oNRWZdVRj1aK&XrRg-0!isdZcScMgZv*_;x3tsfP-+k-DH}`c5g%O?9|aN-Xl= z0W>5T+TAI|IeV8orB$;?BIm+RG|8(qwJQi*D_Ce;MtOCc4m2}#32C4R3`5b)V1TOd zbXrDeae$tN4phUAi$0I*Za?q;<2|DPvi*OYo9f)F30zgZwPL#3{y)yyV|z6hTC4qk zcmMyo+PXGPT6B1GoSY#Yl^b4>y!H|07@!KlUWryJ0)kb1#W` zN^YcLRmXH~&p};Qj|V`>Yl?>Z4bG_y(njFb;^X?;QL5tl{ zxu_wttoqwzt5V)jsA5pOobfQrjl=#QsVTR2hv2*DsC; zuPt=CX;w?OOO1JTPG(uPI@zkWtbzHDLO}nr$iXo{ZR+hXH(L;^5RF-ypRQG9H3$r9 zJ9NQAs|5KcTUP__v%o-1K-JODXvx8)H3J-)SypY+)?vvmQadzNSVBcX2S*353e=gQ zCpPm&Xaw*{q=^(Ju?ZZB069<~s?l|Lay^?FR-qZkwE`0eAKQ_(8-@5Y!0gPj>ZoL2 zMM+x&%txOp#eA$9v0}TrTWm0+TE458@OGtZRl*A0i-<2g*W`(@TmcSXd#jqBs;Ysb zCZN0NT6I=tS=CCmY6KF^bA32~AUh%YVH%((m&R(Uu(Hn>^_93yqbaMEZFs6r3o!s9 zix?oJ(CP&s;K*mu3<|1JU4E_8kk)It?k;fYz`S{zU;OamrdHKq{)txeym^_gYWHVhz9K(9Kz_{F|~Fo*7G z;L~)%b*NC#Q?euCt+=^@6kk5SVRS%-o^qG|w)n%4d z`zHIU;oD3B5OnxH@Ye+D4?YP-sq0`6XqOBt?HC}kO@QAlf+|Exj051`Hh~)UHB<$G;1{E_fp6`hqJ8mP)S_99&@My^?cM z-c3Ck^G?gF&n?d_&h6Ff)vR7wgcPd zoBmhMxSWC6-(^3QeRcM6*UMCNY(|? zPHj#W-X>3a+P?qQiH9Ej!nC4-(K9~T;3?_FtkkpVyI-*4hpfSOKe^*$OE%BnP^*3Z z)k!HIZQC91I=pBXxA*XerqBH2UX8oY$)10p^j!8zX-u1oR&2fa{xc7r?mThhhTiVS zq@3lE+6|V!al>U5uPr@z>P@9DO_=(^ZbwMprF^7^XDwP@bMu5dF8%(9Pi}0ovY)BD z?2MyRPWDiczkm4wyY92{{@p%#GrQ*6H?G=ZGihbYVGq9iQ(e{nJ$B^H`ENXR$~!;4 z@#ef^dP|q2T=T(~HrV}vCm-Cbc-EnH$2>pVetO>g1w&Jh+HAWclw;pG?$bYnhiE_a zc=owlUKn*)&I?j$n+J6NabF&FYkl>Ip$qRXE^HhU9UW}6@g5x2_5uCv?^TuauY2U) z%11ssYw+U!_bmDFyaT0+QZ9Mjx5M|lSH8UNnwM{V<6m!`I{t*)E8d!!a@1e0I{wt6 zY1^HZW99E`9Wi_6{fF25UQIpfl<&9M?*FcO?eX{XrG2-1`krBfFX*#Rip8#cX{%l% z_SmPe;f9g_oUp-`FKzzy&IQsNDTmFtAQ*5-{mumk9NqVgyZ*J!Eej5*d{Fu+<*>11 zc08oid*<1b4qiF<*6nt>;-zisu9bf1u|hhxjpv+Kch<4tLmLcjI`51Td2_$Hv0&48 zj+4GkIa}4e({396*vwI<4SHxa2bZoA=L>KJ2-%RMzGeet+LIRUh~5;_tUQdiXXMt}NK~%7<#D4^qzdr!TTT-R<9h z9{Bm$4|&gin1A*?+blUqdOzi`Av1P4dEfiBet&%X`<))oU3%bNU%a?ddM|fD)}w7M zy7yDclS3ZA{J;nIyI^tAnI~PmWz{u@NpJOBzS^DcS)RAx`9V+q_1K2Wm$&?S(dcKc zcyFZidWwTw{?%rC)>bb3>#G~B*nYq}@8>yxJ!10>xn^Y>|Gv!n?%zhw;_TwnzC3M< zNA9ki`q|7eCAsHkX>C5)%1yFIj&(LUYM0xNJ9qD=cRJ-y*0b}ZCvrc?>eJ?Amt6SQ zmuH``r6$>`Kq2%@8#6!yex!+ih zx~t7m7w><;HrLl~f9BiYj$ORX4X0gwK*_;R7WQ_fbJ~2c^UuA!tZZ5E)`IiL-LzZn z_%r@_+I5xEO}(#@=C(QPjJ&}yTx+3M8$G>umszvsy>Xa9c8H~;t2hTG*VDgA!oBXhQuIjXHQF8e^acCW|w$;s>g z`l9Q9_+q?r=jo3~cX8MyZBDlI?JcGA54iT$GU@%R`j&0H>1X#{@o=B~U9yg8WB;WS z_n-8IoBPhxcWaFAR^;`oJv{3O>GJ%Et2yK1Dbv4LdEp*opHTPxa+BSzc;wiq@tuP> zs;x5~bJWMLZ}Hjh4tn6$DR2ESe4Ed1*#3+QrJM5`amM5o_~;)Ge`1S~?~S@?+2{Sv zbg%e!<0szTR(dRdh4f~d8#rRoQD?mKWBq0erv3h{cdUDY8}_XEct4K1v&~WSUpS@U zqe4qM_xz8R&7Uw}!`mMzI(2)FI=jtLjUW8(Oa0uVxBKzXzFE6py>H1?kCa?>VE%nL zWAd7R*mu$njoMj5FMD?W8=u~G?KbjX&h5Kz{`;#fJab39v!dj=N&S~p+SkuNV#U?F zjEzd|f{j+23+8UpP`4=9X4dBSZGG@leJAIR8uR8;(jzI3G3%6j_W$PLF{QV^_x1~~ zU2^c7N4)atyS3YkU?eXo7?~*#%I-JxvQ7RzW$ZcrUJp#UJoko$DXv(1B)u9^|}4{-iP+x_wi5Edn)^!D&5`2D-QnYlEy1%552l+XunG? zezbN)-#@>3sdNm7wVhX{esx=J!OM&O-MFIR@43OjD-SsPh+CvH3VW>fD5oCv@QNiz zxM9O}r*GT8@rnzxwb?Td;Hc6z*PK#NboTTw_y6r>KfQWp{}p2fKYIT)dD5eWgIAkZ z_Sxm?M|Z#TiOmaNI$+EWlm4{Tk=JfqP?2K)l2bO?d-R;wPdWdxH-G$O!EJlLlC$~V z((hC3Ut-?%;a)$U`%#blD>gsB?tpFfS~$09)0CseZ8f`L;WX>A-#LH1=JIc&!ksp_ zefX%9OOBoJkon3VYbtIzVA%496k`RrKg;6Tu#j-LMr^?7R(c9B`?!dD#yK|NV=v4leqf zi6D7a*?mF7LofgF$2%@N>Vz#fa;;rgOeyRs{i*Mk(phaTxtoz|?s~@G4r{veIqT(< zuYaTA?fKW$_uX5X)8;6B@jegq9C7_ci$D11xR-}b-*RH!D{u7cd(vvFlH#546_K;_N2CLUEk+d^PZs#U%zvc2eLJ)OVXD0D^BgV@`l@I z&E4<7Gp@M$p6mAfWGm^P9M(2I+G5*HKHui=2hMN&^3t6zp1x?}K`Z}u&R*;z+r~$m z-#zt~?f>ulCvSYT@rLUTePf5^8{a;AJV)Kt#t}Ciw(p7ijJ#U+$9}L$$#t83a^v6b z{HJsyhqYajHZ{i$divn`yF7Z~+>)gmtFH|@aa^w*IjZfF)bFKx&-(k0@7?j&=dWA% zz}73aexP>DZnHS5?L5{e*y`vHqE~u`r+oI!H9vf{!+ukCyidBH!`jA2`QL2u$FIIv zF|netXrucq>xqY!l-#zl!2joGozl$z|8lkSo?rD?lvBVDyI=ot3p6!0?cWSD zT0ALD1%c|B-mG~;=Qf}jTR*g!p96(Cqg-)@Y349p8=~0q+!^kipd=`6K{6|*r~+EL zWzw@kG0+M@cWYV8pF3;I&61N`5jp%Yq{>Z-4=`mwXE(IIuzz^4TvCkk-5@U5JQZdn zBA#OZXYsRk;b1@&6skZ@F-2e?>8ekT9c?^BfxsQ;&@)opTs>`Tbp`Nhkr|lNRMBGk zaT#IytYp)>CXCmiED&B48mTc-T}9uq*rCVD)Q~`u)wub^RMU55b@6HYj_I&5ri;~H2ty5iuH(&1xA^NdzmeQG9IeTxs0 ztq%QX1Ua}?j9gKoqJ_IDz-9unyryB%t*WN)@LgHmR5e#MpdN)#SF~TC6X?-`D`b*F zO##{tx!mf$9^dLyGRo@DCtDr#j277zb;JbfMHV$mAJq%gqUa`s{lp5^pReoGmwi*` z85=fM4^(L9;iQ7m7e9Sd%;Ro8#cuT-T;DWmD<2eX2*1;u|O}8V&^Vt2=z$$-S{@az>f{ z&ScXgU8kW!qQdA5WDWrd$fGqO^Xe%U_&G#i*7v%K=7HsDxH*s=O0Dn>3Gt4AK@jMX z82~p+1@FW+{iKXC{q@PFw|uzCftnBsal7=j5z_D@pk*w*}*bC7aa#L!XPUh5(8?%};8A z-#mj<6*k)=+3axr6Rs(gk7e;6pZV62X>d~%RkJ*mM zMy7F)ic^t{n;%cRzqQMDuUyYQVc>wGAPzg?6Q7*D$<{xN+vBF^o;+^j4JO^PTWQk|S>G)jxCRU| z-Q1rYP?>dee`WypXHvCj&pp{RjW$8Z)38hnQY*h}Ytwr2_a*%oMPeW=+mC@1r+>V? zQh3&eYc!BLr`hXn`kx-Qp}Z+Sc+`ZEQ%Xl8Uk-3K1#lPVp%KQk?;p*w8Z8tH$$ruL ze%a_n!K#TiPRnq6xRwl=XuBKYXa3x>+1(Hy&YkN{4}Z2u9PjRMAT>|4Eu;sngD3gB zBxMiyd7{-ELT~V~Do|r$I{KSHhoYS8hx8;EfoEB2dtjn&phHzLa_|K`?BUVrGyud|Fb57Z`wv+L%U&uSfJiGL0cf;mi| zVj#8b352SaIHZj6wkq;OM$OS3dSMa=in`v+hU5{qrYtZx4kug-$I;!$2t)&&uPZbs zFkQm}L;Ev|k0ABolSaZWsQdwPChb@9&ZB#1rFbcj#DRVOsSZTAeBRdxHNzdw^85TmFx|cka18Z@XXJ z7FcD0Wm#jJlWv(oH(xpa>*S=HuhLvcf}X*wl0mk_O6dwpczEGynbACwk?AJg+;|KA zdeU4F`{34qp@aD|z2#T^pAOI3wDiX%zgw0yCZoo{m`jt#fCIH}2uVb-Wk;mkLpu4O zJVz(Y3KaUnWX}opbjLu1U$v} zLYH1%$kpiOm2G%!WI^l(Mc&j8d%|-N^*r3#Sb{+^st-OlaMe1EyOGvEDd0 z)fxL046om18C)yS0TDJ4)>a-O~b)W6@lU41p=SfXN>PXd@Wu z6JW&KRkZcmH7J*@mEc11rlo_4zkBmIP>lJ~uF3;(A9M2HG4w8wo6Un~@raRmi%JmT zS#rpl4W82Z<#XJbb6vk73`WmvsBW0wF6`>KbI3p0`+n$s> z9DEnyG$R7jX9}DdlvTx!0<@M*bfgf%xVpLiP_?iuJHBs`#Et~aE(Es^ZUxjGqOYMR z2cc`H5QD^pTQx3*ZY5#$jA)tIwvB3%>$)L}cF3#)Y9350Y%73Tivtp~;I|bk@Zzc_ zr3Q_n>d|G(1U(t(zrgr}U^Fupd_hFVcauE8fJuZ2GoZ0 zleV8f$0%OFm_q-FMl*+L&Vd67K{!g7HaR5HQD`zWi*qK$D5ldzh|=02w75E>+s!cAbLmfOx22uVNRW7 zVy&t~Fb-^I6dcf;XLIf`wfnwOZ0WGpg;W@U{H{K~$6Zw$7f;V9)62=G59q;C3{d+j ziD!I{NydShuvm16gs2S#hLt{QKbGlzhMOHa^bN=Y4lVTNJh(#XLM7Wq>lmz|PP(R_ zvpCAAxn`SW(|cy5h9(I{ErhX?J~+XzDH>L6U$A*cf&;9Sk)hHW7P+dKI>UFh32m9-8yANe5t1Ff!dql2cuF-@#D($#}ThOp5@1y>nuEUG{e^%kYB>va^ z>b5|)1-dQJZGmnJbX%a?0^Jtqwm`Q9x-HOcf&XR;Ea|`Ds>Tw_MGIH3s+frJu#bdB zi&P5vGYV8@7$E>;ME6uo7<9*Z4~=_x*8wY*U4Ot=4_!Il59csQe(+22Q926q;0ui)Fx==t?e>imue|{qr2U^IoQA9vskX$-Kb&jW_%p6fVLmS$ZVX8^DZWk-7OVgPQts0_Z zx(z}ACM2Qlh+duwP{+3;YKN)H^8a0q9_JRjaCFsye6oE z9DVdV$&QZNJ(Oao+(iQWsLQ~ATn+e-rT`Lv!csNN^$bAr9LAK4%2r67idHS;OkG){ zilqUDaRQ-`^j|-t$GF8V99>3|umeYrXTZ3QE^BMy=zC?9qd$=B=z-0I;L(VI=pMiT zjdS{|mCpn)&~K%)c~mQttP?F6p^n&bQeY^W*N<8d;!eQd4n8aa3-y zZSuqsi4agRAQ$!mS`{5M>m4{ci|qhBmmb&viqdYEw_y~8vKVYR63SxrjR0FCG#-US zmURJEe?Rcf1d$tbJ#`zhI~;5bcRgN7XfZ0;OX9|%t` zC>B}=pj|CRkK!hxQb_ZBB19yGXxeghI$;b~1|=@Ie|5xvQL2XfJO!H{-&Y4`mQ_DV zwyLc|Z*L&vfPAuuP>CPx0LU`n_)zyeHMG-L|He&8r2zLQ13-0E&}H!yEqu}uw^I`!8VQcs2vBi*@qx8 z(!@kSW`gaUrUm?o zQL38wlZ=iF`AFW!ZO=)WS4q^d1Bb@lh)AdEDlvdVY=L-+6_V(2={8Hj#K*Vlj+tfE zYm$A{^;KZdv>HR8jz3wbQTB@&CmU_v|+D6>U&k+X?^pHZYt_mxOJaL`s`ew=D(WvUf!7ApXXNP_U?6Kub|_v zHQFL~AKYz$ZVPl3`NvdHI8D4!n23 z-=5yI==!rZy7JkqqR!7q8PL>qMMJ!+o!3JrT16k6@a@+JJ-^vU%as0O8 zC+13DbHe!|8;rM*U#Q7ec7oU4ZMe#$fP^6 zz1Ng8Z@OmmLHAzWZ{dhLN>Bd7`^Rkb+%W0YuFhx^!a`f!?kgzT_T#3L_Br%#%l3I- zr=i!}dge#BR7o#)dPW!kb%f4Hi?@5mKi~ZC!;ZQ8FDJhI<+xjsx%}s$*B)|%^i2Nl z(xeXUD9%VZxq~4i=S^?;On&3lk$Wm%mOXdRC5NBz?U93}r@Huzj2TNcK|Zzjy6?P- zmDhgS0s@d~Jukfjt=AZJg^iaW)PM)>Ss5Ts5bAIo!>QQ zR4q8b+rR6*H$CgIr6t>*c+Ou(*5%&2yr%;@HlI1Q;^8R(c%NIJIjdhv|hpLX?wSEn9&=c|7i zH+I4IrDwb=o!j{tVf=t-(jw>8-mCW2xh1ci^7v6l{Wkw0f5_kdc*^8|JS&}36m{~f z@f*t^p@R#x_j=)o(SMTnpKBfOg!xQ7;aeDxJj{+n*`KvxenMbxehIq1{(Q zsTyqfr^2WEmc04hdruW#wfkFr-jz=8^sY&6qg?_PxP3dGbor;}ezwI`uN?UN$^rK- zeQNSsZy7zLQweoC{)_^M#s}qLxBD3zD|Q`tZ}IEB>^BA*Bl4&2^~d?Ujh0UC^fRi| z-Ay4F+y0Ze`tIX4==n|GU4FRe(uMPjl;3Xm+ygVElR7^mTEG_SxMuqveq!*4O?plL z^yAkbfBlui&slip)ep`4P&%Q@Gb)A!K&jokzU()*-g@K{=lx;skI!v+)Hergv3tLg zyQJefKcgxq9yYA2?Y-`qeA$K{{-yY+tcp9&JNoVY{_%b|c)YYs)c^CdMhgDFJ_V=d zZ`%70xjXc{x`&>9Ge7Kp#j!xsoTkH@!9tnA`nS~`$$2RoIKKnW93)&8s5k|RCeKDY z($rNyGCfFS*8=C<6=w+A6DHTDYHK4T-JELFsSl_igusBAism&VKa*LZ;&O&G9oAy? zjNm8Q>MzMwH#FvOGX(kvw;_;M6Nr`@VFCz9qbLZ75!N&4)q@bL zvkR*ORM6>Uf%eo10s1Q7c+$0cW7DA-HO(BIY;`SyeaxY7ZbX9LlRaNCQL$2tUVNZi zZUkdf`o10ugc}bm4f-@`ag968G&ar7DAOOBY*PFDYx)$o{sJRLJip$E54tlW#0E0mXQc6vG zaXqZ5mA*S87+$xUmTY?R&mpZ+QLz^iPy+>lc-{lO17eXlS`S^?8Phg>42IXh6@-@_ zfvlwi9ZV1!Tg>#H9a#(oc)UeFe_NlA%kbLRlu-=cTkV%@c(5S>wEi{%F99<>Iq_o0 z5@SDvd=X}2Fu3cvuP9!(KwHztoaqz&|I%`)&-ZyZ^}0T1HgA84*V1T@=1DIzdg4bn zB|AW*0>E@s4{lch?ErEKo`vBDZq5h;OC~Y2z8%0%VM2naBZ1n)-*c~uqKQ^DL8Fi& zP}?32P0I%ggW(OWoHD8m>Ok6RZ<|vo{YK$JVeEs3C5OajD~-~-V)h%V3~Bp83-<{ z-h15nI?@XGNP8WzAuOU)y{^|0dk8C3{B?v+@vB=$Vn6NVF=Q%TjG@+b#L1|2WYl@d zV@M7afB*;=MK0AN-4eRKfln7m=zER=$Q3>1$N`Y+(N$r7OcBXZIdUX4HwJfD=5 z5-$Qv+x=oKd7|a-^?iY|FSmO{Q(;QJYs7Qnh+A8lte!+8}MQ) za6sTV5nah?2T$mBdKPS?#i3B-M*fZR7vyb14@ifO1Hr@fJ^KspPX#o~8tgY>%>JMw zeqHV#`$4sFf31tXL-Xvvyr0qR{wL;VMDX+F-pTGCLb~Hej$Et;0{n%)QXxkiQj{?S z%GQUGBi+4&3z#_aBBaMaR8t2uDZE=As;aiDQh~SVbZ>4(U7H-#q;PiK{PJ0?Db~b4 zhX=u&aPHiMm(B@Y#KQ%#r#PexX75B+nEW-6T3WZkW^^<)8W3|}sr;nkPb zd1L3*RgEk$tLn$h_0=i-Zqf{2Z5YKB8@;hp<*JH_)f4K)Im+tx9XWosIJTr_Onq7T z#KzJERWU6XNbVuq8Hb zL#f=ACVgB7N*b213LoYy#4ocMdldnwY0zh4*SmBe|PNB+;rnq)|>+?%>G!)k>*K-R~DScx8|?dDS-43T+*1K6pTYj-v0X zGAUh06RcDrQzZ1)kkiRZWCTD*5%7LwO#V-M4*BLH_1KZY-%eY;^tiJh$A5a7-2ddRF+)p$Mkt0eV zXJx=`K=tXFAnhu9<{5c=op<5xwf#@}Y>y2ZK3}rnpwF5f$@;8)zw2N>J8j@hw?0&+ z4IDOY;NfoFVdCws%|BFs{vmH$eyH(7Mb%pVr6&$)E;0)KdrxO0aBzDWy$bnAuH0_QP#o8=pxD@@w5)d1OVs2f_u@^6Ri%!rk-Xc zVE+_cZJ2&!sC;6FS>gQgYGuVdrD`%^QQf3EUz=D>n50$iUpA_;!l>)mO8X;C3$8Iy+_DC8-Z60r{$0UDWw)di} zRbvP=pyI*z1pu7vsty_oo<>3YW4WPkYc^m_@Ai8JO`mw=$4dSmPB?Jrts!_gPSuL|s88d@1q?VRXX_&ycnlM>5D%GhqWeck6t16~em(|p+%@~T~ z*qx7|*2kMXc3R&r*=&)N(B&G#I%Ea#N7B5)Cf#Ru2n#1{kD&`AmxboGMW>eL9sJWJ zjaSYddUey#ewSSQXzhx=e}41Qth_Z^hdKd7wXCwOJ-r<`WP5<97~#&ZuB;H}d3ER- ziMX`Y(5>!}{C&x0N3ZgX2vTM_q=n6sVH4CN!QD*uvY4boXsUjw__p=%tVPRfZk}+* zrQaX%$&F1`_A_;tow3e6qqV1a-Fn8aF5@2Od3Ej?t%gqah}QQ@_B%>=#&VlEgH!>} zpC$~rkb#+nsQP*Il@<$(rWR3+WLDYp#ht&~D6iu9_hw$Zy#JBWtB?Hj*LSjt*L==c zn>excemZcRHI5V4!ewCRbv>U#l@djmObgj&`tWvpI*8s7^%Ynq*I<&+ulJ&8XBS&88`D`b2d`U8UME+pG7}jpgDz=nh>f5zDncf>t+5{x0hO zIlWb>a9sZFz15y8)8Emz{mbl|gr^=SiCvcByNT>G%|WbgjzbpIM30EpM9r1Mu;-BM z6B2H{AE^M@@=fvq3RH)JMxH(^kt{>85;72QW02I!VY}=|d$jM?E!ix=j?}`pB%bgZ zr2+)lIPv!s`swQKz>#CEjcuXAx;0GJhrmkmt3z$Yd5Xf09p(dC(bO#_YF2V-Cp)m} z*>udMwQ6Ux>e%Mzm^=zv-@}D5EV1_Phu^xstiv;lfRHNM&1k-&%}vmzs!*si$$AL- z%(x&Ja7q2n1qU46_l>*$waqOH4yk-_ovNiKJP)>ehb{NI_B>AetMj}#53PgQveiFY ztljDp$=}HyZMn)n;%GCB9kb&hrQS2oo^ zl)tW=X`H7h?zxLOveiUjaWGMP8C2odNzXRfpIhIznIn^)W9#pezc0;g>a%LOHZ{+% ze0a~9A`|Q~%#l$D=?EPUzT?AH9t7S)sI8QN_E6PMPG7(KeGq!`}I_cRayH)G^wyyZ|b8P*6@^@S@f0Zjbf#W+aEdjLS zB5q_sK4O!5EQUM}Msj8lTJzgw>g?PVlejRcDn03&UtV&AcXvjl2 z?XS)i<2=O}uBcLE8>SgUU~TPWR!n-f$!^v9zKts;J;&DHCx2h6Hzo3Uv35LECjFN` z-sSTRM)Y~Jrq_Ni&g=ip(6g*B_Rc!8{aG>LIUVd-(%PFVwona(Fjj@Vp~Jwki@kZX zOX|g%EczasBXv&Lxl48imx0wmMvQ@V#4`cbA#n(U4=t#IAs7HRRC(H(?9bOPT&7Z< zP+qB)jvZgWZ^iiPs(JyJsnyCRD>JC)sQU_-OvUKN((+olvSP~o36s-?%XGDU7S7aW z;s3dov)W@$_Se?;V*lT*|JOEmk^aA$CM_D@j6Yi3D--ZXXBB;sj6YI+kF21f(uarM zhi;&#gn|^UbEFR=FH{Z1cGATk8R*>6-_04(5(ME!J`z1%V7HNOkhc!R`EgTFT-6K0 z5zS31v6Vj-m1LGxUrDwqyDengk#I+eOH5n*hgl4E7J$H@Eo~F4r)yRAqNM*7YT2@A z$alco1Fs~=roc@Cf<_i2gKWI1Ia(5(jlyZlv_@1d$)ep&Av-fe<7YEK;_BA0c7MmfA7MH3sSX>e$S(>W#@(Wxc0ANf%bd zL(u{gTQv*0I;1}>os_E^r0uJq0O(H#-Do(#fprBPOh-%zS^zl_RB)hP*TSk}GRvwf zldXylpW!Ruh~Rxhr;3tYvFAHVF(n1EF{WWqJx_NAz^I`w`kNgj2-wz;@u7hY0U0YgIuYr|F7b zO#KvqyeU-UO&r!Y;4y>b3Pe%*x~hvtWtLTsO}46sKm`4XG=?e6c*1?~EO;Z~Zc~&1 zb~Sx1=qVuC)QJOF{>8a1_-}1Qg(1PU91^n>t^H<-m$YJlk(p)Hg~?WRRTg9uHb`h~ zhyi5iXKe?X8`&~Qh3RZ-^t5LHA6<8#`%$r)V7zmg23+w^nvfJrz;lQ*(w_lFWR_KD zCR>0V4Ve+0yLKq5J9HFM42C_{RTpyXg9K>SL~1yDkFgaA$?!nJF~1hHrc9*W+;IHhD)`P=%omP_Gv(6#uRDrD1a|C zNW0(MLkq(QAySTjRElO^0d)l=F9QlfaRlz!wDteQ{=Y}oVN#!4d!N+fVBU1U*1rXo zMoao-5Z&R*7Y8JJvO&*mXd;G8d<6uv1l=M!&R+wLaQcDJwv zwVy!Om(!g$V6;1@u9`Oqxg%&x%IECZNW zz;czQY_oz&F*{+sSVo;yEQLlfyglS^aOxls&3-LQVX_mq*Ui*H_J7@Vl{ABGRy}UT z1n=~pM#PI$HG<*%Uq-N~#j8^o8P}j9vFdhIx$}xdrwxH1)CdIwavGw=88HdMZp7D^ ziFDe7d`tE0H7QA~r`8gsv|?&a<${vN%E?M?SxxCY0gIndKCz*)V$6&Qla-pXdfHEF zrt6jEGpZ{Wj9wd9d~CN)+T)>79CxJX>T2m-5Ob zvklyUB8>rUGr(y@rLtjKR?`RZmo*1);62?XY210adSMv4zFVB0$&|7 zvug5)YEPY%82f2-bH;w!NrNaZmvVmA2x*}7K~CQ*`!4CL^zB{rN|9gW6m1}VP*|I_ zS!4UB~k|?x39V>^P`h>4(ITAuP7E zgwPHymShcSBRhn7p9MM8oj?a$q)Ojsarq++u;777wny_2UP2;*x;NZ_#hMnO80qPD z=xddUbH;$~W))@coKv1<>PRQaWF#p^aQR5+mD0wOoB2Ln$*@=@9?~=n3MQv)Z(64awn31}cY628lvxGdPi^#!NgBakx~G?i;C>}(%-Ugl?IvUlu!$VxfgA25(~_T zE?Q{CVdO_J(uJ<-TY)$w(u5HCdUP~7ET-)LA(+#+bq#*Szzh)C(N*caoW9av`o4vM zDTgW890%ZIiaMFgl_OSNmF%YUPU2(Io{T_DOFDtzP4GdIO{18EMww?&`BA0c=iDS6 zEwpDbZbU0A5gs&i5FEi1K=MtY7-vMFkqqfLj+_#QUQl|mM*+tUvMln=3isroxX4x$ ztD>wO;jk}Nwx+NWo(0`Ab|Wxa}bG83~xJe$~X|BEUEe8U2~gB&-J)YDu-Y+0)fDqLt78r z39w7qOgi|z69gzyfc@~L$HY15EM`lm`jppQs_i1(&XJU$(VGtS$kl={lpg9Kd>)A# zTcgVcr43ya=ILysgv=J2CMe%TzYy)A&2taUKAqe@X$iJuWUldFy0;jce=T^w1%4i5Eg+r#M?pInYylJ60nfe?-mET-RC7oRdt%Su)c&bT>{IfT~H{|FNw z5{xs~X4VPezLJ@3&|6cci@Raxh%s~&4;XZ~F~xQlpQ4w^fj|_2MxKA3xON=+3~7lZ zWvz(Qc`6;lkqwrQPrJHW43h-@hSM;bnv)zFs6^ZDYAG+0(CMIc8#x zi%~G#QCNr$=oq7yYabDB^m62+SkqDBQ+7QQi_d693bT<6ILD=X9`_UqF$fU4kk~|` zhd#1o>AT#Ir1E%Aks}Akdy3-fhS2RW#3U$CGCHElbSsg>Wm{?cJa5IdBk8iIK(5;Zc$TLAiqAX5kBAb1}1U4?}VOM|K0F^WNea3B%~Obg7Q zaPVrbk3bvk?UNE`G#z%zh$kV}CF(;0*8rIuqnxI9%_|aaKE0EFoD1J`cBp=#N9L5UOGBn?+GGOix;oxVTs^4I~E@lHy{6Au3LUD<0^G zfW9?wWA@m@fqa|*^@(CX;BJX%0Op{SBZ_jw><%{-1>+J2swl%M+_IBskTpsFvqoZc z!|<3!$$>e_J_T9QC9o$`lM>v=2!3+uh&zFOUm*UBzzz*H2<^nIs$nu13bz)kJ*5A|c%X%w5sKNKbgsA^!VC+YK$cu?fOx~@!I-3PG}Y3$lH$YK-@o8b z(jX*YtjJ&mBi zAW%YaAabJ)q7D8GE zM9kpGM;3f74UR+UA~iLiQb+2R9tcYn^AKv3Bp^bcMbwR0b3Iv&{Gok5k{%~`K*FA4 ztzwXA0<^$w8#sqYbv^VbS+Z2u36l#QVK0J98*WFCI26nA5Zgf($YY%kM4V_!XC;n= z@rB5MjIt>VgaKLWaVSG(d?`qRq6Hyc%f%HHU6zc-Nz?69JW5hO&yu3%imL&oQM?Y46ell zBMuY;Jzyo!XkGRhCrpO^LBc+mfTxSgq7g%EDXI=^+ztK?hqN&~#7fdhMJGxN6=0(Z z4}mF)jR!a~VqjT(Xo#2S?RV6G$t7`ZQ*(ftQm)fb`PP|r_>k0!`Eb-Q2ntyhe#03D zAr{7;2&9uQ4ix@QK8jm{i&C+4YHNYNapH^$?4dkw&`{h1lHvG;4&N;^xx>DL>DKbA z6L+J81{G;KWE0}Aq$#nuW5PiIEEUXJNT4pADo!agj7+gVV>!Vm#RIa5d5ZxA5a4M9 zK&HQG-vgw{YzvkFQsL0U>DIJ1r7u?^95*Bc_PEqjVAmbPM|T*fRp5YYsQ zS^`T!A_QgNNPkIO8|efiAWx$5j2N_x3@_jbfl~<801t9Cqc(A%9sq@6#}PTKUC3t( z(qI%9*a2t|8xc~va$*lj$z7FE`9%Ab1x$o-f?b$N&O`ax2{4A|Ntg6}Mj9k@0j|xa zF>*MO$=*wJF%$>rHxPKTP`X}e;@bF36tQ+OP23GW!{n)fSkxNx zv;Hj&$hsrH&%=E#?0Z$;r9JfANqwjF)%xc5nOAgjj+awZv`10DUdQy_KKqTVJF;)h z{$2Kr>`mD3U*E&+`9jZwa=*(xs?UTzJFfj#&u3e|dR*HhKR@VsRsL>S@8oZk_io-@ z*}LbR*>g#+?7W%1ruMo#Z**S&-koi5}RlB=E#3|?D_%`FEA+(82dPm>jQ_U%S7{-KMX3*(&xg|o(-RDf_%lG z3!M3j?QaM-4+xKdN;rJdZY13-o?aY|hNHOn$=wi}$~LyMWIPx!Owo*V!N~`Ey})VF)JI&V>@N=VLIi)vTC#A78Jcn} zn`N#YC>jhoflPVAIV@o%90a=o_+;v2*nsu4*>|##(`Tewgo&;vy_Gl;jaz0hrZOHs zpHfKFk*Z}DAl6|Yi?Dkry^(XGG}zS~Ak$3v>|>dNNQv+<7`QSBQew$~?W-!io;WAF zOOK!hqCs(WHuaQ&d51*3$4O;+GD7KJJx-90j-z)Yz1-uVp1q{OsOFK4;W4t66$=?a zITkWzR^lM$TX3?WCH*d8PZ;?%kNiLoHm5WwL$f6;R)hET%b6zwV+F~kM_?_1La{s=pn6dx=kw_^doGt2ai9Y541+rk4j+kpo{${` zn5`2Nuq#rdnqF5(`}rJWkB2{%QTZZ4ioG9967`#!N)mxd@4H` zoh1z;*`O5JM-W_**W$U~)9WcIk<$qABiASTRY)2#JXoLElJL$(zS=`)5$S|cy?RLH zV4FjTQaG)Mac~k=6^GVpV6z0q8nz-&@(o80X4#TOZwa18fIvt^V5^Y?0~bjILxzgw zL%KznDP+S#z>ldTVh6G^q(}%>*%hixiXqTZe3W}SA|&^ZcQc4(v7D-`C@vk`#X@Nz zB;iR)%9Nf+Sl%N(W{KwR#a$Tu2TCfYMzA@CVWA8mhkZEZpn!BGV$lS&gd$=fiO0xC z#YJ!?Zpd-;jS{1Sm{@}nTWk`V$)TZtauon2$rYGU=g}i#61pAgpxMvs3lZ}bDk#N0rf$c z#{d)S0oxv%=g7pXhV1oS({i;#5}z^#3)<1(7_pE=>Yu#=YXTFEf~Q0H#I=cVeMMEp zL+Y0}V;oa9AE;mhR$q-A3ckcrN$OZEOw3|zo@pX5^AiWMWAnrgKfr0RBzbu!Lm=@lI%oc zi>))(UEHad_0x!G@i*yLTF8OZX%6Xn9uhYbYaZf^n*_q74nyCNRpC8M{`@N

eJx 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 diff --git a/src/Paramore.Brighter.Outbox.DynamoDB/MessageItem.cs b/src/Paramore.Brighter.Outbox.DynamoDB/MessageItem.cs index 21cb25259e..ec9be8dfde 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; } @@ -110,7 +111,7 @@ public MessageItem(Message message) 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); @@ -130,7 +131,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, @@ -151,12 +152,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/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); From e5067dfe4d04588252d988fe2d10df6b7eaf17ba Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Wed, 3 May 2023 08:21:59 +0100 Subject: [PATCH 30/89] Fix SQL lite order by timestamp --- Docker/dynamodb/shared-local-instance.db | Bin 552960 -> 552960 bytes .../MsSqlQueries.cs | 2 +- .../MySqlQueries.cs | 2 +- .../PostgreSqlQueries.cs | 2 +- .../SqliteQueries.cs | 2 +- ...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 +++++++++--------- 8 files changed, 36 insertions(+), 31 deletions(-) diff --git a/Docker/dynamodb/shared-local-instance.db b/Docker/dynamodb/shared-local-instance.db index 7d2d85cc9720a932129c78eed4a451ccc5991caa..6b7f933c98391a2370fabcb7dcb24ccb0b91af2a 100644 GIT binary patch delta 3938 zcmZuz3v?9K8J;^kv%4hO-6Rr70wkLlNTLjx$DLPFOHf1*AK_ueM>X?o4FW-4DtL&n zfK;f7qq(A<1F5N)7Aqm>0w;`ewuueLA|Nr~1K~NIs}8OiYBQm8ga1*hEcF({J0$aw9OIaw#fjN7lGd zC%dXAck3cHhob1xnV4SP5RcmA&`!D{nKv$Y8Zof`o8-G%Xf>S{Inbd&J1`lvA1?Q` zh5f;b9(r40AfD9^9h0IijG-`g#QyZ((G!35tq2o6PE~uS-xOA<3WJ< z0AO>^q6+W%v_>`Q{sCqe4zeSC!=rm5A4b;v|LBPy8UgGUQDeOEN6MLKH3-g;aykmk zP7L=p9nqL*AN94D_t^;2q|l?hp+^h+l_xyzD5&>Q_johX!gbz#M<)X}G~Rn1RzN87 zP8=<#V=we01--GKt)wZcf699Ae5R(k&CYp0XH^a!7};N>M^F6N-fTMZX8(q;$DHKp zNaf)4ri-Q|is=#F-XlP-7d@UL+&)OS7ftk6yh-1R@R~ZCnvicrI@(L7)iT4$)`<*5 z)?PuEdh99JC+{N+_rBj==50Pz;4k<9!@UnqDVY`>+%?LZa@xJfs?%w!Vk>`o^u*6R z6=h=6s3NcQWC0T^rHTlo-B32)7ri|v3zXQ?)C1oYRzC6Qd-XLN#PAJqO#y`XaK`cH4X6-WHMvAs@)ABLDuul#zZ+V*SZs=vio zew?q}Ko6Eq6Prknz)o}uouZ~Z9!CW))}Y;piOT4$SUG&xd&a#)zcCZ8<-ooD7lz}rP%aw)uPz{MrAT2x(ua`xD8lV=#1#8A89Vq>BUuth$>@!- zm#Ct^6z9&X{;~zvO@46E?~BXq1>fDYHX4glMc2&f_%U8UULUF4{n4&ZZVp|d?EN$X zJEqQ<+`Owi?|6r|ab>|j{<`V%f9*XJ$$=Llsi;BYnAlg;1EiZpL)jr9+9|Ol4C2%! z2b!HPH@`pb(OkOd#@*hMFB+dO3c(AJbkrco$gij(>Y=}%9`o8QO_RQ#ac@^_09gF_05kOwESwx~3s++v!I3=#IMNVT~?LHQ|5uoY|FX$JA`^{+EB1 z>`~xFdjkrw<#5uKB&Pu_A5s8fsXt9V)_|Hz1K{Ljs2MfH;2f%p+_oHbrPo@}yQ#G* z(zqN|w6`KAm4vzGWC)MJLAjNMcv> z$C*f?C14dhA}|)f7pi4*x@{YrW(gwi>R5JEA}kE*e!5_?d>CDg5@gzh;CcY+%>>d# z(Xb@l;Y?8mAXB$E!!~%%7F@-0Tv3&5()LYACmYYDfhw8RC^42amNVR^%C@CJV()o5RO^3irAg5V zIh|(nug$~}6bYCJI#e5mhh<8Tu;6*y$;CtOSW{fRb+87jp>BcA3`n@ND@s$!zy6N zhQ#^4fe>0MbOKW~b<7*8rjb4CXx-c0JDRk7j|QA2_8VE6iRLOMmi+C4tV31mSmAU> z)FH=A({gnYW5|aum@3kGGm}pm-wsOst{3{zax>8&W{zfQHpkn5fmNs_Q`BWn7i7%q zvL@;#$-`lt%+o@ZWMMI_KxLx0{mg1Up9!Q3x};!U<|JE!7%DE5IJlfMbl#R!ND0>< z#*i@3N(S@EoJ+uE%L>u~sC=mBdM#-#WUnJ% z-WQPQdlF=;gCG}|O}q$6aU@HJy0A0{+N{dE5G;r~Z(v=)iUHNu@HtyY>JOth$qhpF zF4&J$I&mfGc973Y&qT9L-G&CG0S0t#48;eYm23!>W?8D>$f`r8eGYY!g4-V&gqtt` zH!c%SG!;P;ASD$QS^%_EpPcD(isK5F;aZX_kg_q%t)zK!xSA~9k0iKG>IbfqOEaNl z9y*g@Lp~}B1YXvG88%davUFS5L=mzneVx2l!i*tj$IxKp_!JZvS(pihbz6n{1^Y}D zSn-CyX*%2!P(YSsyN1e}47YA(rwll~uHREjKgxuH xOG|*(A#wD>xjq>XH(6jYn{y$Lurhotd delta 2352 zcmd5-ZBSI#8NTn`d+*-ez4x45WQlw&7jhL)WRYAx9Hw>sD1uW0(RE0SqqKtQU}7-Q znmB310VJd`1#iPiMkCrWnoOdJEaXOIqSiQVbO5z6B~=i{CinraHZx9A8tA#Zi{VG7 z{o5bs>~r3a=REtq_ncEZm|Q!U+#E00G7Qs5M+_awEopgBd$i)fHn9j*Qv;KsOSR3S zZTq5fhHrX_v}}Mp;&TYOBv=kSnYRq41=6>BW%Bq>Au3S)dTX+ir&%n%ZzgZTWB7i+ z`O#+H82nr~$dcJbkS8@*1)E=E8_Sf+O0uz?RmrX*C?Kdu*gz`Y;T3YeN0=aWGcC#F z&AMmPf>zx&1 zAanmLsJs}WsOXJQn8nZ!PevU5C#d>FFP|XG1q|AZcFP557?q-Q`4{Lk{t&OmvfLL? zF2u{+3|>L{{Qi_MsOnGAFAm1)`}*@EzoPzqe^%HzXCM-_d=k7kP!x8ZowVE@m?RMN zjB9lf5}wvCFF|%(XHXYfkK*LdBO+dqQ>8DZ21yGjgJ&EMw&X^#9^fV;UauK(KUg1} z;(NO*UXLCK`+TF5k_FPc6cY80(U60Dw3y@cZ$=wThVWvnBS}AWXNvc$J6eRFst!(3 z9i*z!2?h`2PW%|IMknwd+=KIR68Zv-Vj-a1-Ho{E77g^I5qB6+tzmF_#GPom?oCg9 zbJt&d-nX*F_aE>0^vbN91B1wP5Z9v+%}b&5ih)Shiiyln)_3Vk#`EXzE~x)(&-1BG zJFk|m)MPOfx^E&9dTb)|5!2%7vvOA#-t_0S{3>_R-<7(*Mdd%rnr8{{7@e7gcuX9& zdigDuV=OSmZz$mI^ukHdl@m5n@*ZzD+T*xmU>v^=Uki@9B8%wH&@{H8jCg-8sz%Q( zn8QCzd6FFF_b$nmjJ#Lw!+%0Iu?Kaam+*451n-n94UZEFWquy#&j8}>qNj6bBDf;! zfkaLvi7Yq;IUL~gaGD@#oIeG~g+shb3OulY%j5iMq}Br(XL?LDgYH$Ww; zhruCO!-m?Mpt{yRyJ1t+3!CPwuX=HEcPgAv;ZELS?nYXYAeYal$VPk!v{nVUI0;>5 z@W<#fR`5K01Kq(^{5Gz~>+v$fqkw@*&DvUutJW+!_kmVWw0-kCamkg>-&@fXYTlA( z_Wz)`$=QV5r@uOWrQA9#_smt<%vfGjb3Da0v|n$lbN}P=zS;NB+?7K;CaM=QL!SmY z&C|x)%;*sGgc`f0r>5$gLtkd@a^UBx3nV&sTUHJX- ze-*xUdh7ZtvtP5Aj(sP0|6$GWo}6!&zA~)1yZW;0wL&8@PS3 zl-R};Gbl%@wzo|^Cc4Y%yT0!ggVT|{T8L9?47O^kvniFx(M6z_R% zQO)--D` zzk0Cr{$cTAXJ+9nQCv)1#ZYGK?PTdg94cskDOh#ONCNGe^Fi1CS1+lesIa0PKI=iB GnDuWo$m??e diff --git a/src/Paramore.Brighter.Outbox.MsSql/MsSqlQueries.cs b/src/Paramore.Brighter.Outbox.MsSql/MsSqlQueries.cs index d0343b2fc6..6534de9e06 100644 --- a/src/Paramore.Brighter.Outbox.MsSql/MsSqlQueries.cs +++ b/src/Paramore.Brighter.Outbox.MsSql/MsSqlQueries.cs @@ -3,7 +3,7 @@ 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 ASC"; + 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 < 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, 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}"; diff --git a/src/Paramore.Brighter.Outbox.MySql/MySqlQueries.cs b/src/Paramore.Brighter.Outbox.MySql/MySqlQueries.cs index 068493e403..f74990a005 100644 --- a/src/Paramore.Brighter.Outbox.MySql/MySqlQueries.cs +++ b/src/Paramore.Brighter.Outbox.MySql/MySqlQueries.cs @@ -10,7 +10,7 @@ public class MySqlQueries : IRelationDatabaseOutboxQueries 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 GetMessagesCommand { get; } = "SELECT * FROM {0} WHERE `MessageID` IN ( {1} )ORDER BY "; public string DeleteMessagesCommand { get; } = "DELETE FROM {0} WHERE MessageId IN ( {1} )"; } } diff --git a/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlQueries.cs b/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlQueries.cs index f622b013e5..b26ed64f12 100644 --- a/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlQueries.cs +++ b/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlQueries.cs @@ -10,7 +10,7 @@ public class PostgreSqlQueries : IRelationDatabaseOutboxQueries 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 GetMessagesCommand { get; } = "SELECT * FROM {0} WHERE MessageID IN ( {1} )ORDER BY Timestamp ASC"; public string DeleteMessagesCommand { get; }= "DELETE FROM {0} WHERE MessageId IN ( {1} )"; } } diff --git a/src/Paramore.Brighter.Outbox.Sqlite/SqliteQueries.cs b/src/Paramore.Brighter.Outbox.Sqlite/SqliteQueries.cs index 085eda2401..f5244ecef4 100644 --- a/src/Paramore.Brighter.Outbox.Sqlite/SqliteQueries.cs +++ b/src/Paramore.Brighter.Outbox.Sqlite/SqliteQueries.cs @@ -10,7 +10,7 @@ public class SqliteQueries : IRelationDatabaseOutboxQueries 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 GetMessagesCommand { get; } = "SELECT * FROM {0} WHERE MessageId IN ( {1} ) ORDER BY Timestamp ASC"; public string DeleteMessagesCommand { get; } = "DELETE FROM {0} WHERE MessageId IN ( {1} )"; } } 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(); } From 82849b9e31439ba7e27f8e59760549e83f38e82d Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Wed, 3 May 2023 09:43:51 +0100 Subject: [PATCH 31/89] Missing direction on sort --- .../GreetingsSender/GreetingsSender.csproj | 1 - samples/WebAPI_Dapper/GreetingsWeb/Startup.cs | 1 - .../GreetingsWeb/GreetingsWeb.csproj | 1 + .../GreetingsWeb/Startup.cs | 23 ++++++++++-- samples/WebAPI_Dapper_Kafka/README.md | 35 ++----------------- .../MySqlQueries.cs | 2 +- 6 files changed, 25 insertions(+), 38 deletions(-) diff --git a/samples/KafkaSchemaRegistry/GreetingsSender/GreetingsSender.csproj b/samples/KafkaSchemaRegistry/GreetingsSender/GreetingsSender.csproj index 3312c60290..1204f26803 100644 --- a/samples/KafkaSchemaRegistry/GreetingsSender/GreetingsSender.csproj +++ b/samples/KafkaSchemaRegistry/GreetingsSender/GreetingsSender.csproj @@ -13,7 +13,6 @@ - diff --git a/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs b/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs index ebb07934db..5a16c8a4a3 100644 --- a/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs +++ b/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs @@ -74,7 +74,6 @@ public void ConfigureServices(IServiceCollection services) ConfigureMigration(services); ConfigureDapper(services); ConfigureBrighter(services); - ConfigureBrighter(services); ConfigureDarker(services); } diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/GreetingsWeb.csproj b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/GreetingsWeb.csproj index 3d30f65e5d..17232208c7 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/GreetingsWeb.csproj +++ b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/GreetingsWeb.csproj @@ -16,6 +16,7 @@ + diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs index ebb07934db..8bf4719b22 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs +++ b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs @@ -74,7 +74,6 @@ public void ConfigureServices(IServiceCollection services) ConfigureMigration(services); ConfigureDapper(services); ConfigureBrighter(services); - ConfigureBrighter(services); ConfigureDarker(services); } @@ -169,7 +168,7 @@ private void ConfigureBrighter(IServiceCollection services) options.CommandProcessorLifetime = ServiceLifetime.Scoped; options.MapperLifetime = ServiceLifetime.Singleton; options.PolicyRegistry = new GreetingsPolicy(); - }) + } )/* .UseExternalBus(new RmqProducerRegistryFactory( new RmqMessagingGatewayConnection { @@ -187,7 +186,25 @@ private void ConfigureBrighter(IServiceCollection services) MakeChannels = OnMissingChannel.Create } } - ).Create() + )*/ + .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() ) //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 diff --git a/samples/WebAPI_Dapper_Kafka/README.md b/samples/WebAPI_Dapper_Kafka/README.md index b78be28dd2..1fd8248fc8 100644 --- a/samples/WebAPI_Dapper_Kafka/README.md +++ b/samples/WebAPI_Dapper_Kafka/README.md @@ -21,10 +21,10 @@ This sample shows a typical scenario when using WebAPI and Brighter/Darker. It d *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. -*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 +*Production* - runs in Docker;uses Kafka 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. -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. +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 Kafka from the docker compose file. In case you are using Command Line Interface for running the project, consider adding --launch-profile: @@ -74,7 +74,7 @@ A common error is to change something, forget to run build.sh and use an old Doc 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 +docker compose up -d kafka # will just start kafka ``` ```sh @@ -90,35 +90,6 @@ A Sqlite database will only have permissions for the process that created it. Th Maintainers, please don't check the Sqlite files into source control. -#### 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. - -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 -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) - ## 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/src/Paramore.Brighter.Outbox.MySql/MySqlQueries.cs b/src/Paramore.Brighter.Outbox.MySql/MySqlQueries.cs index f74990a005..99d96f8a4d 100644 --- a/src/Paramore.Brighter.Outbox.MySql/MySqlQueries.cs +++ b/src/Paramore.Brighter.Outbox.MySql/MySqlQueries.cs @@ -10,7 +10,7 @@ public class MySqlQueries : IRelationDatabaseOutboxQueries 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 "; + public string GetMessagesCommand { get; } = "SELECT * FROM {0} WHERE `MessageID` IN ( {1} )ORDER BY ASC Timestamp"; public string DeleteMessagesCommand { get; } = "DELETE FROM {0} WHERE MessageId IN ( {1} )"; } } From 029c1dd113e9a26cc01194c2bfce4dc98b5c711b Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Wed, 3 May 2023 18:46:36 +0100 Subject: [PATCH 32/89] Make sure we can build --- .../GreetingsWeb/GreetingsWeb.csproj | 1 - .../Mappers/GreetingMadeMessageMapper.cs | 29 +++++++++-- .../GreetingsWeb/Startup.cs | 48 ++++++------------- .../Mappers/GreetingMadeMessageMapper.cs | 17 ++++++- .../SalutationAnalytics.csproj | 1 + 5 files changed, 58 insertions(+), 38 deletions(-) diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/GreetingsWeb.csproj b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/GreetingsWeb.csproj index 17232208c7..e58ab21e39 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/GreetingsWeb.csproj +++ b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/GreetingsWeb.csproj @@ -17,7 +17,6 @@ - diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Mappers/GreetingMadeMessageMapper.cs b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Mappers/GreetingMadeMessageMapper.cs index 19dc67d0a8..5d626e058b 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Mappers/GreetingMadeMessageMapper.cs +++ b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Mappers/GreetingMadeMessageMapper.cs @@ -1,18 +1,41 @@ -using System.Text.Json; +using System.Net.Mime; +using System.Text.Json; +using Confluent.Kafka; +using Confluent.Kafka.SyncOverAsync; +using Confluent.SchemaRegistry; +using Confluent.SchemaRegistry.Serdes; using GreetingsPorts.Requests; using Paramore.Brighter; +using Paramore.Brighter.MessagingGateway.Kafka; namespace GreetingsWeb.Mappers { public class GreetingMadeMessageMapper : IAmAMessageMapper { + private readonly ISchemaRegistryClient _schemaRegistryClient; + private readonly string _partitionKey = "KafkaTestQueueExample_Partition_One"; + private SerializationContext _serializationContext; + private const string Topic = "greeting.event"; + public GreetingMadeMessageMapper(ISchemaRegistryClient schemaRegistryClient) + { + _schemaRegistryClient = schemaRegistryClient; + //We care about ensuring that we serialize the body using the Confluent tooling, as it registers and validates schema + _serializationContext = new SerializationContext(MessageComponentType.Value, Topic); + } + public Message MapToMessage(GreetingMade request) { - var header = new MessageHeader(messageId: request.Id, topic: "GreetingMade", messageType: MessageType.MT_EVENT); - var body = new MessageBody(System.Text.Json.JsonSerializer.Serialize(request, new JsonSerializerOptions(JsonSerializerDefaults.General))); + var header = new MessageHeader(messageId: request.Id, topic: Topic, messageType: MessageType.MT_EVENT); + //This uses the Confluent JSON serializer, which wraps Newtonsoft but also performs schema registration and validation + var serializer = new JsonSerializer(_schemaRegistryClient, ConfluentJsonSerializationConfig.SerdesJsonSerializerConfig(), ConfluentJsonSerializationConfig.NJsonSchemaGeneratorSettings()).AsSyncOverAsync(); + var s = serializer.Serialize(request, _serializationContext); + var body = new MessageBody(s, MediaTypeNames.Application.Octet, CharacterEncoding.Raw); + header.PartitionKey = _partitionKey; + var message = new Message(header, body); return message; + } public GreetingMade MapToRequest(Message message) diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs index 8bf4719b22..c362dbf8a4 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs +++ b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs @@ -18,7 +18,7 @@ using Paramore.Brighter; using Paramore.Brighter.Dapper; using Paramore.Brighter.Extensions.DependencyInjection; -using Paramore.Brighter.MessagingGateway.RMQ; +using Paramore.Brighter.MessagingGateway.Kafka; using Paramore.Darker.AspNetCore; using Paramore.Darker.Policies; using Paramore.Darker.QueryLogging; @@ -168,43 +168,25 @@ private void ConfigureBrighter(IServiceCollection services) options.CommandProcessorLifetime = ServiceLifetime.Scoped; options.MapperLifetime = ServiceLifetime.Singleton; options.PolicyRegistry = new GreetingsPolicy(); - } )/* - .UseExternalBus(new RmqProducerRegistryFactory( - new RmqMessagingGatewayConnection + }) + .UseExternalBus( + new KafkaProducerRegistryFactory( + new KafkaMessagingGatewayConfiguration { - AmpqUri = new AmqpUriSpecification(new Uri("amqp://guest:guest@localhost:5672")), - Exchange = new Exchange("paramore.brighter.exchange"), + Name = "paramore.brighter.greetingsender", + BootStrapServers = new[] { "localhost:9092" } }, - new RmqPublication[] + new KafkaPublication[] { - new RmqPublication + new KafkaPublication { - Topic = new RoutingKey("GreetingMade"), - MaxOutStandingMessages = 5, - MaxOutStandingCheckIntervalMilliSeconds = 500, - WaitForConfirmsTimeOutInMilliseconds = 1000, - MakeChannels = OnMissingChannel.Create + Topic = new RoutingKey("greeting.event"), + MessageSendMaxRetries = 3, + MessageTimeoutMs = 1000, + MaxInFlightRequestsPerConnection = 1 } - } - )*/ - .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() + }) + .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 diff --git a/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Mappers/GreetingMadeMessageMapper.cs b/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Mappers/GreetingMadeMessageMapper.cs index f96c47a9c7..3286d4be2f 100644 --- a/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Mappers/GreetingMadeMessageMapper.cs +++ b/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Mappers/GreetingMadeMessageMapper.cs @@ -1,4 +1,7 @@ using System.Text.Json; +using Confluent.Kafka; +using Confluent.Kafka.SyncOverAsync; +using Confluent.SchemaRegistry.Serdes; using Paramore.Brighter; using SalutationPorts.Requests; @@ -6,6 +9,14 @@ namespace SalutationAnalytics.Mappers { public class GreetingMadeMessageMapper : IAmAMessageMapper { + private readonly SerializationContext _serializationContext; + private const string Topic = "greeting.event"; + + public GreetingMadeMessageMapper() + { + //We care about ensuring that we serialize the body using the Confluent tooling, as it registers and validates schema + _serializationContext = new SerializationContext(MessageComponentType.Value, Topic); + } public Message MapToMessage(GreetingMade request) { throw new System.NotImplementedException(); @@ -13,7 +24,11 @@ public Message MapToMessage(GreetingMade request) public GreetingMade MapToRequest(Message message) { - return JsonSerializer.Deserialize(message.Body.Value, JsonSerialisationOptions.Options); + var deserializer = new JsonDeserializer().AsSyncOverAsync(); + //This uses the Confluent JSON serializer, which wraps Newtonsoft but also performs schema registration and validation + var greetingCommand = deserializer.Deserialize(message.Body.Bytes, message.Body.Bytes is null, _serializationContext); + + return greetingCommand; } } } diff --git a/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/SalutationAnalytics.csproj b/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/SalutationAnalytics.csproj index a59fc78023..6b21685870 100644 --- a/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/SalutationAnalytics.csproj +++ b/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/SalutationAnalytics.csproj @@ -23,6 +23,7 @@ + From 94f86f69633b3a338fecd3aa8a5653f6ebbe9d39 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Fri, 5 May 2023 08:37:20 +0100 Subject: [PATCH 33/89] Kafka and Sqlite working with Serdes and Binary --- .../.idea/httpRequests/http-requests-log.http | 318 ++++++++++-------- .../GreetingsWeb/Database/OutboxExtensions.cs | 14 +- .../GreetingsWeb/Startup.cs | 22 +- .../SalutationReceivedMessageMapper.cs | 2 +- .../SalutationAnalytics/Program.cs | 73 ++-- .../SalutationAnalytics.csproj | 2 +- .../Handlers/GreetingMadeHandler.cs | 28 +- .../SalutationPorts/Policies/Retry.cs | 12 +- .../Policies/SalutationPolicy.cs | 4 +- .../WebAPI_Dapper_Kafka/docker-compose.yml | 67 +++- 10 files changed, 333 insertions(+), 209 deletions(-) diff --git a/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http b/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http index 1a68ebc312..27f24a48ac 100644 --- a/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http +++ b/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http @@ -1,229 +1,264 @@ 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-08T211212.200.json +<> 2023-05-05T082700.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-05-05T082658.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 - -### - -POST http://localhost:5000/People/new -Content-Type: application/json - -{ - "Name" : "Tyrion" -} - -<> 2022-11-08T211124.200.json +<> 2023-05-05T082656.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-05-05T082325.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-05-05T081253.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-21T115848.200.json +<> 2023-05-05T081244.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-10-21T115844.200.json - -### - -GET http://localhost:5000/People/Tyrion - -<> 2022-10-21T115838.500.json +<> 2023-05-05T081241.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 +<> 2023-05-04T205523.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-19T214755.500.json +<> 2023-05-04T205517.200.json ### -POST http://localhost:5000/Greetings/Tyrion/new +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-10-19T204831.200.json +<> 2023-05-04T205509.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-10-19T204823.200.json +<> 2023-05-03T190816.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 - -### - -POST http://localhost:5000/People/new -Content-Type: application/json - -{ - "Name" : "Tyrion" -} - -<> 2022-10-19T201347.200.json +<> 2023-05-03T190627.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-19T201322.500.json +<> 2023-05-03T190622.200.json ### -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 -<> 2022-10-19T195123.200.json +{ + "Greeting" : "I drink, and I know things" +} + +<> 2023-05-03T190440.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-19T194030.200.json +<> 2023-05-03T190431.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-10-19T194012.500.json +<> 2023-05-03T190407.500.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-10-19T190032.500.json +<> 2023-05-03T190358.200.json ### -POST http://localhost:5000/People/new +POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json { - "Name" : "Tyrion" + "Greeting" : "I drink, and I know things" } -<> 2022-10-19T185850.500.json +<> 2022-11-08T211212.200.json ### -POST http://localhost:5000/People/new +POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json { - "Name" : "Tyrion" + "Greeting" : "I drink, and I know things" } -<> 2022-10-19T184041.500.json +<> 2022-11-08T211208.200.json ### -POST http://localhost:5000/People/new +POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json { - "Name" : "Tyrion" + "Greeting" : "I drink, and I know things" } -<> 2022-10-19T183305.500.json - -### - -GET http://localhost:5000/People/Tyrion - -<> 2022-10-19T180348.500.html +<> 2022-11-08T211200.200.json ### @@ -234,13 +269,18 @@ Content-Type: application/json "Name" : "Tyrion" } -<> 2022-10-19T180248.500.html +<> 2022-11-08T211124.200.json ### -DELETE http://localhost:5000/People/Tyrion +POST http://localhost:5000/People/new +Content-Type: application/json -<> 2022-10-19T180240.500.html +{ + "Name" : "Tyrion" +} + +<> 2022-11-08T200350.200.json ### @@ -251,7 +291,13 @@ Content-Type: application/json "Greeting" : "I drink, and I know things" } -<> 2022-06-25T125829.200.json +<> 2022-10-21T115924.200.json + +### + +GET http://localhost:5000/People/Tyrion + +<> 2022-10-21T115848.200.json ### @@ -262,18 +308,13 @@ Content-Type: application/json "Name" : "Tyrion" } -<> 2022-06-25T125806.200.json +<> 2022-10-21T115844.200.json ### -POST http://localhost:5000/Greetings/Tyrion/new -Content-Type: application/json - -{ - "Greeting" : "I drink, and I know things" -} +GET http://localhost:5000/People/Tyrion -<> 2022-06-24T234446.200.json +<> 2022-10-21T115838.500.json ### @@ -284,13 +325,13 @@ Content-Type: application/json "Name" : "Tyrion" } -<> 2022-06-24T224322.200.json +<> 2022-10-19T215040.200.json ### GET http://localhost:5000/People/Tyrion -<> 2022-06-24T224317.500.json +<> 2022-10-19T214755.500.json ### @@ -301,6 +342,8 @@ Content-Type: application/json "Greeting" : "I drink, and I know things" } +<> 2022-10-19T204831.200.json + ### POST http://localhost:5000/Greetings/Tyrion/new @@ -310,6 +353,8 @@ Content-Type: application/json "Greeting" : "I drink, and I know things" } +<> 2022-10-19T204823.200.json + ### POST http://localhost:5000/Greetings/Tyrion/new @@ -319,102 +364,114 @@ Content-Type: application/json "Greeting" : "I drink, and I know things" } +<> 2022-10-19T204723.200.json + ### -POST http://localhost:5000/Greetings/Tyrion/new +POST http://localhost:5000/People/new Content-Type: application/json { - "Greeting" : "I drink, and I know things" + "Name" : "Tyrion" } -### +<> 2022-10-19T201347.200.json -POST http://localhost:5000/Greetings/Tyrion/new -Content-Type: application/json +### -{ - "Greeting" : "I drink, and I know things" -} +GET http://localhost:5000/People/Tyrion -<> 2022-06-20T193453.200.json +<> 2022-10-19T201322.500.json ### -POST http://localhost:5000/Greetings/Tyrion/new -Content-Type: application/json +GET http://localhost:5000/People/Tyrion -{ - "Greeting" : "I drink, and I know things" -} +<> 2022-10-19T195123.200.json + +### -<> 2022-06-20T193452.200.json +GET http://localhost:5000/People/Tyrion + +<> 2022-10-19T194030.200.json ### -POST http://localhost:5000/Greetings/Tyrion/new +POST http://localhost:5000/People/new Content-Type: application/json { - "Greeting" : "I drink, and I know things" + "Name" : "Tyrion" } -<> 2022-06-20T193451.200.json +<> 2022-10-19T194012.500.json ### -POST http://localhost:5000/Greetings/Tyrion/new +POST http://localhost:5000/People/new Content-Type: application/json { - "Greeting" : "I drink, and I know things" + "Name" : "Tyrion" } -<> 2022-06-20T193450.200.json +<> 2022-10-19T190032.500.json ### -POST http://localhost:5000/Greetings/Tyrion/new +POST http://localhost:5000/People/new Content-Type: application/json { - "Greeting" : "I drink, and I know things" + "Name" : "Tyrion" } -<> 2022-06-20T193431.200.json +<> 2022-10-19T185850.500.json ### -POST http://localhost:5000/Greetings/Tyrion/new +POST http://localhost:5000/People/new Content-Type: application/json { - "Greeting" : "I drink, and I know things" + "Name" : "Tyrion" } -<> 2022-06-20T193430.200.json +<> 2022-10-19T184041.500.json ### -POST http://localhost:5000/Greetings/Tyrion/new +POST http://localhost:5000/People/new Content-Type: application/json { - "Greeting" : "I drink, and I know things" + "Name" : "Tyrion" } -<> 2022-06-20T193429.200.json +<> 2022-10-19T183305.500.json ### -POST http://localhost:5000/Greetings/Tyrion/new +GET http://localhost:5000/People/Tyrion + +<> 2022-10-19T180348.500.html + +### + +POST http://localhost:5000/People/new Content-Type: application/json { - "Greeting" : "I drink, and I know things" + "Name" : "Tyrion" } -<> 2022-06-20T193428.200.json +<> 2022-10-19T180248.500.html + +### + +DELETE http://localhost:5000/People/Tyrion + +<> 2022-10-19T180240.500.html ### @@ -425,18 +482,18 @@ Content-Type: application/json "Greeting" : "I drink, and I know things" } -<> 2022-06-20T193427.200.json +<> 2022-06-25T125829.200.json ### -POST http://localhost:5000/Greetings/Tyrion/new +POST http://localhost:5000/People/new Content-Type: application/json { - "Greeting" : "I drink, and I know things" + "Name" : "Tyrion" } -<> 2022-06-20T193426.200.json +<> 2022-06-25T125806.200.json ### @@ -447,46 +504,41 @@ Content-Type: application/json "Greeting" : "I drink, and I know things" } -<> 2022-06-20T193423.200.json +<> 2022-06-24T234446.200.json ### -POST http://localhost:5000/Greetings/Tyrion/new +POST http://localhost:5000/People/new Content-Type: application/json { - "Greeting" : "I drink, and I know things" + "Name" : "Tyrion" } -<> 2022-06-20T193420.200.json +<> 2022-06-24T224322.200.json ### -POST http://localhost:5000/Greetings/Tyrion/new -Content-Type: application/json +GET http://localhost:5000/People/Tyrion -{ - "Greeting" : "I drink, and I know things" -} +<> 2022-06-24T224317.500.json ### -POST http://localhost:5000/People/new +POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json { - "Name" : "Tyrion" + "Greeting" : "I drink, and I know things" } -<> 2022-06-20T192613.200.json - ### -POST http://localhost:5000/People/new +POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json { - "Name" : "Tyrion" + "Greeting" : "I drink, and I know things" } ### diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/OutboxExtensions.cs b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/OutboxExtensions.cs index 80f172a2be..135a162d8a 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/OutboxExtensions.cs +++ b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/OutboxExtensions.cs @@ -17,18 +17,18 @@ namespace GreetingsWeb.Database; public static class OutboxExtensions { public static IBrighterBuilder AddOutbox(this IBrighterBuilder brighterBuilder, IWebHostEnvironment env, DatabaseType databaseType, - string dbConnectionString, string outBoxTableName) + string dbConnectionString, string outBoxTableName, bool binaryMessagePayload = false) { if (env.IsDevelopment()) { - AddSqliteOutBox(brighterBuilder, dbConnectionString, outBoxTableName); + AddSqliteOutBox(brighterBuilder, dbConnectionString, outBoxTableName, binaryMessagePayload); } else { switch (databaseType) { case DatabaseType.MySql: - AddMySqlOutbox(brighterBuilder, dbConnectionString, outBoxTableName); + AddMySqlOutbox(brighterBuilder, dbConnectionString, outBoxTableName, binaryMessagePayload); break; default: throw new InvalidOperationException("Unknown Db type for Outbox configuration"); @@ -37,20 +37,20 @@ public static IBrighterBuilder AddOutbox(this IBrighterBuilder brighterBuilder, return brighterBuilder; } - private static void AddMySqlOutbox(IBrighterBuilder brighterBuilder, string dbConnectionString, string outBoxTableName) + private static void AddMySqlOutbox(IBrighterBuilder brighterBuilder, string dbConnectionString, string outBoxTableName, bool binaryMessagePayload) { brighterBuilder.UseMySqlOutbox( - new MySqlConfiguration(dbConnectionString, outBoxTableName), + new MySqlConfiguration(dbConnectionString, outBoxTableName, binaryMessagePayload: binaryMessagePayload), typeof(MySqlConnectionProvider), ServiceLifetime.Singleton) .UseMySqTransactionConnectionProvider(typeof(Paramore.Brighter.MySql.Dapper.MySqlDapperConnectionProvider), ServiceLifetime.Scoped) .UseOutboxSweeper(); } - private static void AddSqliteOutBox(IBrighterBuilder brighterBuilder, string dbConnectionString, string outBoxTableName) + private static void AddSqliteOutBox(IBrighterBuilder brighterBuilder, string dbConnectionString, string outBoxTableName, bool binaryMessagePayload) { brighterBuilder.UseSqliteOutbox( - new SqliteConfiguration(dbConnectionString, outBoxTableName), + new SqliteConfiguration(dbConnectionString, outBoxTableName, binaryMessagePayload: binaryMessagePayload), typeof(SqliteConnectionProvider), ServiceLifetime.Singleton) .UseSqliteTransactionConnectionProvider(typeof(Paramore.Brighter.Sqlite.Dapper.SqliteDapperConnectionProvider), ServiceLifetime.Scoped) diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs index c362dbf8a4..629e8e0c6f 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs +++ b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs @@ -1,4 +1,5 @@ using System; +using Confluent.SchemaRegistry; using DapperExtensions; using DapperExtensions.Sql; using FluentMigrator.Runner; @@ -160,22 +161,28 @@ private static void ConfigureDapperMySql(IServiceCollection services) private void ConfigureBrighter(IServiceCollection services) { services.AddSingleton(new DbConnectionStringProvider(DbConnectionString())); + + var schemaRegistryConfig = new SchemaRegistryConfig { Url = "http://localhost:8081"}; + var cachedSchemaRegistryClient = new CachedSchemaRegistryClient(schemaRegistryConfig); + services.AddSingleton(cachedSchemaRegistryClient); + var configuration = new KafkaMessagingGatewayConfiguration + { + Name = "paramore.brighter.greetingsender", + BootStrapServers = new[] { "localhost:9092" } + }; services.AddBrighter(options => { //we want to use scoped, so make sure everything understands that which needs to options.HandlerLifetime = ServiceLifetime.Scoped; + options.ChannelFactory = new ChannelFactory(new KafkaMessageConsumerFactory(configuration)); options.CommandProcessorLifetime = ServiceLifetime.Scoped; options.MapperLifetime = ServiceLifetime.Singleton; options.PolicyRegistry = new GreetingsPolicy(); }) .UseExternalBus( new KafkaProducerRegistryFactory( - new KafkaMessagingGatewayConfiguration - { - Name = "paramore.brighter.greetingsender", - BootStrapServers = new[] { "localhost:9092" } - }, + configuration, new KafkaPublication[] { new KafkaPublication @@ -183,7 +190,8 @@ private void ConfigureBrighter(IServiceCollection services) Topic = new RoutingKey("greeting.event"), MessageSendMaxRetries = 3, MessageTimeoutMs = 1000, - MaxInFlightRequestsPerConnection = 1 + MaxInFlightRequestsPerConnection = 1, + MakeChannels = OnMissingChannel.Create } }) .Create() @@ -192,7 +200,7 @@ private void ConfigureBrighter(IServiceCollection services) //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) + .AddOutbox(_env, GetDatabaseType(), DbConnectionString(), _outBoxTableName, binaryMessagePayload: true) .AutoFromAssemblies(typeof(AddPersonHandlerAsync).Assembly); } diff --git a/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Mappers/SalutationReceivedMessageMapper.cs b/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Mappers/SalutationReceivedMessageMapper.cs index bdbb2c1061..d4e3fe6616 100644 --- a/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Mappers/SalutationReceivedMessageMapper.cs +++ b/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Mappers/SalutationReceivedMessageMapper.cs @@ -8,7 +8,7 @@ public class SalutationReceivedMessageMapper : IAmAMessageMapper private static void ConfigureBrighter(HostBuilderContext hostContext, IServiceCollection services) { - var subscriptions = new Subscription[] + var subscriptions = new KafkaSubscription[] { - new RmqSubscription( + new KafkaSubscription( 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 + channelName: new ChannelName("SalutationAnalytics"), + routingKey: new RoutingKey("greeting.event"), + groupId: "kafka-GreetingsReceiverConsole-Sample", + timeoutInMilliseconds: 100, + offsetDefault: AutoOffsetReset.Earliest, + commitBatchSize: 5, + sweepUncommittedOffsetsIntervalMs: 10000, + makeChannels: OnMissingChannel.Create) }; + + //We take a direct dependency on the schema registry in the message mapper + var schemaRegistryConfig = new SchemaRegistryConfig { Url = "http://localhost:8081"}; + var cachedSchemaRegistryClient = new CachedSchemaRegistryClient(schemaRegistryConfig); + services.AddSingleton(cachedSchemaRegistryClient); - var host = hostContext.HostingEnvironment.IsDevelopment() ? "localhost" : "rabbitmq"; + //create the gateway + var consumerFactory = new KafkaMessageConsumerFactory( + new KafkaMessagingGatewayConfiguration { Name = "paramore.brighter", BootStrapServers = new[] { "localhost:9092" } } + ); - 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 host = hostContext.HostingEnvironment.IsDevelopment() ? "localhost" : "rabbitmq"; services.AddServiceActivator(options => { options.Subscriptions = subscriptions; - options.ChannelFactory = new ChannelFactory(rmqMessageConsumerFactory); options.UseScoped = true; + options.ChannelFactory = new ChannelFactory(consumerFactory); options.HandlerLifetime = ServiceLifetime.Scoped; options.MapperLifetime = ServiceLifetime.Singleton; options.CommandProcessorLifetime = ServiceLifetime.Scoped; @@ -99,19 +105,24 @@ private static void ConfigureBrighter(HostBuilderContext hostContext, IServiceCo //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 - } - } + .UseExternalBus( + new KafkaProducerRegistryFactory( + new KafkaMessagingGatewayConfiguration + { + Name = "paramore.brighter.greetingsender", + BootStrapServers = new[] { "localhost:9092" } + }, + new KafkaPublication[] + { + new KafkaPublication + { + Topic = new RoutingKey("salutationrecieved.event"), + MessageSendMaxRetries = 3, + MessageTimeoutMs = 1000, + MaxInFlightRequestsPerConnection = 1, + MakeChannels = OnMissingChannel.Create + } + } ).Create() ) .AutoFromAssemblies() diff --git a/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/SalutationAnalytics.csproj b/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/SalutationAnalytics.csproj index 6b21685870..afb273e6ab 100644 --- a/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/SalutationAnalytics.csproj +++ b/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/SalutationAnalytics.csproj @@ -8,7 +8,7 @@ - + diff --git a/samples/WebAPI_Dapper_Kafka/SalutationPorts/Handlers/GreetingMadeHandler.cs b/samples/WebAPI_Dapper_Kafka/SalutationPorts/Handlers/GreetingMadeHandler.cs index 4d9894b23d..092e2d0242 100644 --- a/samples/WebAPI_Dapper_Kafka/SalutationPorts/Handlers/GreetingMadeHandler.cs +++ b/samples/WebAPI_Dapper_Kafka/SalutationPorts/Handlers/GreetingMadeHandler.cs @@ -13,13 +13,13 @@ namespace SalutationPorts.Handlers { - public class GreetingMadeHandlerAsync : RequestHandlerAsync + public class GreetingMadeHandler : RequestHandler { private readonly IUnitOfWork _uow; private readonly IAmACommandProcessor _postBox; - private readonly ILogger _logger; + private readonly ILogger _logger; - public GreetingMadeHandlerAsync(IUnitOfWork uow, IAmACommandProcessor postBox, ILogger logger) + public GreetingMadeHandler(IUnitOfWork uow, IAmACommandProcessor postBox, ILogger logger) { _uow = uow; _postBox = postBox; @@ -27,36 +27,36 @@ public GreetingMadeHandlerAsync(IUnitOfWork uow, IAmACommandProcessor postBox, I } //[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) + [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 = _uow.BeginOrGetTransaction(); try { var salutation = new Salutation(@event.Greeting); - await _uow.Database.InsertAsync(salutation, tx); + _uow.Database.Insert(salutation, tx); - posts.Add(await _postBox.DepositPostAsync(new SalutationReceived(DateTimeOffset.Now), cancellationToken: cancellationToken)); + posts.Add(_postBox.DepositPost(new SalutationReceived(DateTimeOffset.Now))); - await tx.CommitAsync(cancellationToken); + tx.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); + tx.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_Kafka/SalutationPorts/Policies/Retry.cs b/samples/WebAPI_Dapper_Kafka/SalutationPorts/Policies/Retry.cs index 4db47aa42d..58013a5a3f 100644 --- a/samples/WebAPI_Dapper_Kafka/SalutationPorts/Policies/Retry.cs +++ b/samples/WebAPI_Dapper_Kafka/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_Kafka/SalutationPorts/Policies/SalutationPolicy.cs b/samples/WebAPI_Dapper_Kafka/SalutationPorts/Policies/SalutationPolicy.cs index ddf21c324f..28024f22a7 100644 --- a/samples/WebAPI_Dapper_Kafka/SalutationPorts/Policies/SalutationPolicy.cs +++ b/samples/WebAPI_Dapper_Kafka/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_Kafka/docker-compose.yml b/samples/WebAPI_Dapper_Kafka/docker-compose.yml index 703b1a2372..fc66877111 100644 --- a/samples/WebAPI_Dapper_Kafka/docker-compose.yml +++ b/samples/WebAPI_Dapper_Kafka/docker-compose.yml @@ -34,17 +34,70 @@ services: 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 + zookeeper: + image: confluentinc/cp-zookeeper:latest + hostname: zookeeper + container_name: zookeeper + environment: + ZOOKEEPER_CLIENT_PORT: 2181 + ZOOKEEPER_TICK_TIME: 2000 + volumes: + - ./zoo/data:/var/lib/zookeeper/data + - ./zoo/log:/var/lib/zookeeper/log + kafka: + image: confluentinc/cp-enterprise-kafka:latest + hostname: kafka + container_name: kafka + depends_on: + - zookeeper ports: - - "5672:5672" - - "15672:15672" + - "9092:9092" + - "9101:9101" + environment: + KAFKA_BROKER_ID: 1 + KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 + KAFKA_AUTO_CREATE_TOPICS_ENABLE: "false" + KAFKA_DELETE_TOPIC_ENABLE: "true" + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT + KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092 + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0 + KAFKA_CONFLUENT_SCHEMA_REGISTRY_URL: http://schema-registry:8081 volumes: - - rabbitmq-home:/var/lib/rabbitmq + - ./broker/data:/var/lib/kafka/data + + schema-registry: + image: confluentinc/cp-schema-registry:latest + hostname: schema-registry + container_name: schema-registry + depends_on: + - kafka + ports: + - "8081:8081" + environment: + SCHEMA_REGISTRY_HOST_NAME: schema-registry + SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS: 'kafka:29092' + SCHEMA_REGISTRY_LISTENERS: http://0.0.0.0:8081 + + control-center: + image: confluentinc/cp-enterprise-control-center:latest + hostname: control-center + container_name: control-center + depends_on: + - kafka + - schema-registry + ports: + - "9021:9021" + environment: + CONTROL_CENTER_BOOTSTRAP_SERVERS: 'kafka:29092' + CONTROL_CENTER_SCHEMA_REGISTRY_URL: "http://schema-registry:8081" + CONTROL_CENTER_REPLICATION_FACTOR: 1 + CONTROL_CENTER_INTERNAL_TOPICS_PARTITIONS: 1 + CONTROL_CENTER_MONITORING_INTERCEPTOR_TOPIC_PARTITIONS: 1 + CONFLUENT_METRICS_TOPIC_REPLICATION: 1 + PORT: 9021 volumes: - rabbitmq-home: - driver: local my-db: driver: local From c6ec8608096dd09d93e1cade8368f9c7509e495e Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Sat, 6 May 2023 16:39:45 +0100 Subject: [PATCH 34/89] Use RelationalDatebaseConfiguration not derived classes --- .../.idea/httpRequests/http-requests-log.http | 75 ++++++++++--------- .../GreetingsSender.Web/Program.cs | 2 +- .../ASBTaskQueue/GreetingsWorker/Program.cs | 2 +- .../CompetingReceiverConsole/Program.cs | 2 +- .../CompetingSender/Program.cs | 2 +- .../GreetingsReceiverConsole/Program.cs | 2 +- .../GreetingsSender/Program.cs | 2 +- .../GreetingsWeb/Database/OutboxExtensions.cs | 5 +- .../SalutationAnalytics/Program.cs | 4 +- .../GreetingsWeb/Database/OutboxExtensions.cs | 21 ++++-- .../GreetingsWeb/Database/SchemaCreation.cs | 2 +- .../GreetingsWeb/Startup.cs | 33 ++++++-- samples/WebAPI_Dapper_Kafka/README.md | 2 +- .../SalutationAnalytics/Program.cs | 4 +- .../Handlers/GreetingMadeHandler.cs | 9 +++ samples/WebAPI_EFCore/GreetingsWeb/Startup.cs | 4 +- .../SalutationAnalytics/Program.cs | 4 +- .../Orders.API/Program.cs | 2 +- .../Extensions/BrighterExtensions.cs | 2 +- .../Orders.Worker/Program.cs | 2 +- .../MsSqlInbox.cs | 6 +- .../MySqlInbox.cs | 4 +- .../MySqlInboxConfiguration.cs | 54 ------------- .../PostgresSqlInbox.cs | 4 +- .../PostgresSqlInboxConfiguration.cs | 49 ------------ .../SqliteInbox.cs | 4 +- .../SqliteInboxConfiguration.cs | 54 ------------- .../MsSqlMessageConsumer.cs | 4 +- .../MsSqlMessageConsumerFactory.cs | 4 +- .../MsSqlMessageProducer.cs | 4 +- .../MsSqlProducerRegistryFactory.cs | 4 +- .../SqlQueues/MsSqlMessageQueue.cs | 4 +- .../MsSqlAzureConnectionProviderBase.cs | 2 +- .../MsSqlChainedConnectionProvider.cs | 2 +- .../MsSqlDefaultAzureConnectionProvider.cs | 2 +- .../MsSqlManagedIdentityConnectionProvider.cs | 2 +- ...MsSqlSharedTokenCacheConnectionProvider.cs | 4 +- .../MsSqlVisualStudioConnectionProvider.cs | 2 +- .../MsSqlConfiguration.cs | 31 -------- .../MsSqlSqlAuthConnectionProvider.cs | 2 +- .../MySqlConfiguration.cs | 56 -------------- .../MySqlConnectionProvider.cs | 2 +- .../MsSqlOutbox.cs | 6 +- .../ServiceCollectionExtensions.cs | 6 +- .../MySqlOutbox.cs | 6 +- .../ServiceCollectionExtensions.cs | 6 +- .../PostgreSqlConfiguration.cs | 54 ------------- .../PostgreSqlOutbox.cs | 6 +- .../ServiceCollectionExtensions.cs | 6 +- .../ServiceCollectionExtensions.cs | 6 +- .../SqliteOutbox.cs | 6 +- .../PostgreSqlNpgsqlConnectionProvider.cs | 2 +- .../SqliteConfiguration.cs | 55 -------------- .../SqliteConnectionProvider.cs | 2 +- ....cs => RelationalDatabaseConfiguration.cs} | 13 +++- .../MsSqlTestHelper.cs | 14 ++-- .../MySqlTestHelper.cs | 4 +- .../PostgresSqlTestHelper.cs | 8 +- ...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 | 2 +- .../When_Removing_Messages_From_The_Outbox.cs | 2 +- ...en_The_Message_Is_Already_In_The_Outbox.cs | 2 +- ..._Message_Is_Already_In_The_Outbox_Async.cs | 2 +- ...es_In_The_Outbox_And_A_Range_Is_Fetched.cs | 2 +- ...The_Outbox_And_A_Range_Is_Fetched_Async.cs | 2 +- ...n_There_Is_No_Message_In_The_Sql_Outbox.cs | 2 +- ...e_Is_No_Message_In_The_Sql_Outbox_Async.cs | 2 +- ...iting_A_Message_To_A_Binary_Body_Outbox.cs | 2 +- .../When_Writing_A_Message_To_The_Outbox.cs | 2 +- ...n_Writing_A_Message_To_The_Outbox_Async.cs | 2 +- .../When_Writing_Messages_To_The_Outbox.cs | 2 +- ...en_Writing_Messages_To_The_Outbox_Async.cs | 2 +- ..._are_received_and_Dispatched_bulk_Async.cs | 2 +- 78 files changed, 209 insertions(+), 512 deletions(-) delete mode 100644 src/Paramore.Brighter.Inbox.MySql/MySqlInboxConfiguration.cs delete mode 100644 src/Paramore.Brighter.Inbox.Postgres/PostgresSqlInboxConfiguration.cs delete mode 100644 src/Paramore.Brighter.Inbox.Sqlite/SqliteInboxConfiguration.cs delete mode 100644 src/Paramore.Brighter.MsSql/MsSqlConfiguration.cs delete mode 100644 src/Paramore.Brighter.MySql/MySqlConfiguration.cs delete mode 100644 src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlConfiguration.cs delete mode 100644 src/Paramore.Brighter.Sqlite/SqliteConfiguration.cs rename src/Paramore.Brighter/{RelationalDatabaseOutboxConfiguration.cs => RelationalDatabaseConfiguration.cs} (81%) diff --git a/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http b/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http index 27f24a48ac..44a1221a77 100644 --- a/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http +++ b/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http @@ -1,3 +1,43 @@ +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" +} + +### + +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" +} + +### + +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 + +### + POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json Content-Length: 47 @@ -508,38 +548,3 @@ Content-Type: application/json ### -POST http://localhost:5000/People/new -Content-Type: application/json - -{ - "Name" : "Tyrion" -} - -<> 2022-06-24T224322.200.json - -### - -GET http://localhost:5000/People/Tyrion - -<> 2022-06-24T224317.500.json - -### - -POST http://localhost:5000/Greetings/Tyrion/new -Content-Type: application/json - -{ - "Greeting" : "I drink, and I know things" -} - -### - -POST http://localhost:5000/Greetings/Tyrion/new -Content-Type: application/json - -{ - "Greeting" : "I drink, and I know things" -} - -### - diff --git a/samples/ASBTaskQueue/GreetingsSender.Web/Program.cs b/samples/ASBTaskQueue/GreetingsSender.Web/Program.cs index ee57d2da39..d626a014e2 100644 --- a/samples/ASBTaskQueue/GreetingsSender.Web/Program.cs +++ b/samples/ASBTaskQueue/GreetingsSender.Web/Program.cs @@ -36,7 +36,7 @@ var asbConnection = new ServiceBusVisualStudioCredentialClientProvider(asbEndpoint); -var outboxConfig = new MsSqlConfiguration(dbConnString, outBoxTableName: "BrighterOutbox"); +var outboxConfig = new RelationalDatabaseConfiguration(dbConnString, outBoxTableName: "BrighterOutbox"); builder.Services .AddBrighter(opt => diff --git a/samples/ASBTaskQueue/GreetingsWorker/Program.cs b/samples/ASBTaskQueue/GreetingsWorker/Program.cs index aa5c99d6cb..f93a1d9e14 100644 --- a/samples/ASBTaskQueue/GreetingsWorker/Program.cs +++ b/samples/ASBTaskQueue/GreetingsWorker/Program.cs @@ -69,7 +69,7 @@ o.UseSqlServer(dbConnString); }); -var outboxConfig = new MsSqlConfiguration(dbConnString, outBoxTableName: "BrighterOutbox"); +var outboxConfig = new RelationalDatabaseConfiguration(dbConnString, outBoxTableName: "BrighterOutbox"); //TODO: add your ASB qualified name here var clientProvider = new ServiceBusVisualStudioCredentialClientProvider(".servicebus.windows.net"); diff --git a/samples/MsSqlMessagingGateway/CompetingReceiverConsole/Program.cs b/samples/MsSqlMessagingGateway/CompetingReceiverConsole/Program.cs index e923358e52..c15985be38 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 => diff --git a/samples/MsSqlMessagingGateway/CompetingSender/Program.cs b/samples/MsSqlMessagingGateway/CompetingSender/Program.cs index 7eb99fbea2..a197b6b026 100644 --- a/samples/MsSqlMessagingGateway/CompetingSender/Program.cs +++ b/samples/MsSqlMessagingGateway/CompetingSender/Program.cs @@ -40,7 +40,7 @@ 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() diff --git a/samples/MsSqlMessagingGateway/GreetingsReceiverConsole/Program.cs b/samples/MsSqlMessagingGateway/GreetingsReceiverConsole/Program.cs index 344616544c..e04760ae03 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 => diff --git a/samples/MsSqlMessagingGateway/GreetingsSender/Program.cs b/samples/MsSqlMessagingGateway/GreetingsSender/Program.cs index 834906986e..11d2935386 100644 --- a/samples/MsSqlMessagingGateway/GreetingsSender/Program.cs +++ b/samples/MsSqlMessagingGateway/GreetingsSender/Program.cs @@ -23,7 +23,7 @@ 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() diff --git a/samples/WebAPI_Dapper/GreetingsWeb/Database/OutboxExtensions.cs b/samples/WebAPI_Dapper/GreetingsWeb/Database/OutboxExtensions.cs index 80f172a2be..0249b41b48 100644 --- a/samples/WebAPI_Dapper/GreetingsWeb/Database/OutboxExtensions.cs +++ b/samples/WebAPI_Dapper/GreetingsWeb/Database/OutboxExtensions.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Paramore.Brighter; using Paramore.Brighter.Extensions.DependencyInjection; using Paramore.Brighter.Extensions.Hosting; using Paramore.Brighter.MySql; @@ -40,7 +41,7 @@ public static IBrighterBuilder AddOutbox(this IBrighterBuilder brighterBuilder, private static void AddMySqlOutbox(IBrighterBuilder brighterBuilder, string dbConnectionString, string outBoxTableName) { brighterBuilder.UseMySqlOutbox( - new MySqlConfiguration(dbConnectionString, outBoxTableName), + new RelationalDatabaseConfiguration(dbConnectionString, outBoxTableName), typeof(MySqlConnectionProvider), ServiceLifetime.Singleton) .UseMySqTransactionConnectionProvider(typeof(Paramore.Brighter.MySql.Dapper.MySqlDapperConnectionProvider), ServiceLifetime.Scoped) @@ -50,7 +51,7 @@ private static void AddMySqlOutbox(IBrighterBuilder brighterBuilder, string dbCo private static void AddSqliteOutBox(IBrighterBuilder brighterBuilder, string dbConnectionString, string outBoxTableName) { brighterBuilder.UseSqliteOutbox( - new SqliteConfiguration(dbConnectionString, outBoxTableName), + new RelationalDatabaseConfiguration(dbConnectionString, outBoxTableName), typeof(SqliteConnectionProvider), ServiceLifetime.Singleton) .UseSqliteTransactionConnectionProvider(typeof(Paramore.Brighter.Sqlite.Dapper.SqliteDapperConnectionProvider), ServiceLifetime.Scoped) diff --git a/samples/WebAPI_Dapper/SalutationAnalytics/Program.cs b/samples/WebAPI_Dapper/SalutationAnalytics/Program.cs index a221b2604d..2e14e8976d 100644 --- a/samples/WebAPI_Dapper/SalutationAnalytics/Program.cs +++ b/samples/WebAPI_Dapper/SalutationAnalytics/Program.cs @@ -193,10 +193,10 @@ 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 string DbConnectionString(HostBuilderContext hostContext) diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/OutboxExtensions.cs b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/OutboxExtensions.cs index 135a162d8a..62e7710a79 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/OutboxExtensions.cs +++ b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/OutboxExtensions.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Paramore.Brighter; using Paramore.Brighter.Extensions.DependencyInjection; using Paramore.Brighter.Extensions.Hosting; using Paramore.Brighter.MySql; @@ -16,19 +17,22 @@ namespace GreetingsWeb.Database; public static class OutboxExtensions { - public static IBrighterBuilder AddOutbox(this IBrighterBuilder brighterBuilder, IWebHostEnvironment env, DatabaseType databaseType, - string dbConnectionString, string outBoxTableName, bool binaryMessagePayload = false) + public static IBrighterBuilder AddOutbox( + this IBrighterBuilder brighterBuilder, + IWebHostEnvironment env, + DatabaseType databaseType, + RelationalDatabaseConfiguration configuration) { if (env.IsDevelopment()) { - AddSqliteOutBox(brighterBuilder, dbConnectionString, outBoxTableName, binaryMessagePayload); + AddSqliteOutBox(brighterBuilder, configuration); } else { switch (databaseType) { case DatabaseType.MySql: - AddMySqlOutbox(brighterBuilder, dbConnectionString, outBoxTableName, binaryMessagePayload); + AddMySqlOutbox(brighterBuilder, configuration); break; default: throw new InvalidOperationException("Unknown Db type for Outbox configuration"); @@ -37,20 +41,21 @@ public static IBrighterBuilder AddOutbox(this IBrighterBuilder brighterBuilder, return brighterBuilder; } - private static void AddMySqlOutbox(IBrighterBuilder brighterBuilder, string dbConnectionString, string outBoxTableName, bool binaryMessagePayload) + private static void AddMySqlOutbox(IBrighterBuilder brighterBuilder, RelationalDatabaseConfiguration configuration) { brighterBuilder.UseMySqlOutbox( - new MySqlConfiguration(dbConnectionString, outBoxTableName, binaryMessagePayload: binaryMessagePayload), + //new MySqlConfiguration(dbConnectionString, outBoxTableName, binaryMessagePayload: binaryMessagePayload), + configuration, typeof(MySqlConnectionProvider), ServiceLifetime.Singleton) .UseMySqTransactionConnectionProvider(typeof(Paramore.Brighter.MySql.Dapper.MySqlDapperConnectionProvider), ServiceLifetime.Scoped) .UseOutboxSweeper(); } - private static void AddSqliteOutBox(IBrighterBuilder brighterBuilder, string dbConnectionString, string outBoxTableName, bool binaryMessagePayload) + private static void AddSqliteOutBox(IBrighterBuilder brighterBuilder, RelationalDatabaseConfiguration configuration) { brighterBuilder.UseSqliteOutbox( - new SqliteConfiguration(dbConnectionString, outBoxTableName, binaryMessagePayload: binaryMessagePayload), + configuration, typeof(SqliteConnectionProvider), ServiceLifetime.Singleton) .UseSqliteTransactionConnectionProvider(typeof(Paramore.Brighter.Sqlite.Dapper.SqliteDapperConnectionProvider), ServiceLifetime.Scoped) diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/SchemaCreation.cs b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/SchemaCreation.cs index d628c0bdb4..08dc59bc2a 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/SchemaCreation.cs +++ b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/SchemaCreation.cs @@ -187,7 +187,7 @@ private static string GetProductionConnectionString(IConfiguration config, Datab { return databaseType switch { - DatabaseType.MySql => config.GetConnectionString("GreetingsMySql"), + DatabaseType.MySql => config.GetConnectionString("GreetingsMySqlDb"), _ => throw new InvalidOperationException("Could not determine the database type") }; } diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs index 629e8e0c6f..aaf7574b9b 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs +++ b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs @@ -20,6 +20,8 @@ using Paramore.Brighter.Dapper; using Paramore.Brighter.Extensions.DependencyInjection; using Paramore.Brighter.MessagingGateway.Kafka; +using Paramore.Brighter.MySql; +using Paramore.Brighter.Sqlite; using Paramore.Darker.AspNetCore; using Paramore.Darker.Policies; using Paramore.Darker.QueryLogging; @@ -166,6 +168,9 @@ private void ConfigureBrighter(IServiceCollection services) var cachedSchemaRegistryClient = new CachedSchemaRegistryClient(schemaRegistryConfig); services.AddSingleton(cachedSchemaRegistryClient); + var outboxConfiguration = GetOutboxConfiguration(); + services.AddSingleton(outboxConfiguration); + var configuration = new KafkaMessagingGatewayConfiguration { Name = "paramore.brighter.greetingsender", @@ -196,14 +201,21 @@ private void ConfigureBrighter(IServiceCollection services) }) .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, binaryMessagePayload: true) + /* + * 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 + + * KAFKA and BINARY: Because Kafka here uses the Serdes serializer which sets magic bytes in the first + * five bytes of the the payload we have binary payload messages, and we need to set the binaryMessagePayload to true + *if we don't do that, and use text the database will corrupt the leading bytes + */ + .AddOutbox(_env, GetDatabaseType(), outboxConfiguration) .AutoFromAssemblies(typeof(AddPersonHandlerAsync).Assembly); } + private void ConfigureDarker(IServiceCollection services) { services.AddDarker(options => @@ -247,5 +259,16 @@ private string GetConnectionString(DatabaseType databaseType) _ => throw new InvalidOperationException("Could not determine the database type") }; } + + private RelationalDatabaseConfiguration GetOutboxConfiguration() + { + if (_env.IsDevelopment()) + { + return new RelationalDatabaseConfiguration(DbConnectionString(), _outBoxTableName, binaryMessagePayload: true); + } + + return new RelationalDatabaseConfiguration(DbConnectionString(), _outBoxTableName, binaryMessagePayload: true); + } + } } diff --git a/samples/WebAPI_Dapper_Kafka/README.md b/samples/WebAPI_Dapper_Kafka/README.md index 1fd8248fc8..c095897e7e 100644 --- a/samples/WebAPI_Dapper_Kafka/README.md +++ b/samples/WebAPI_Dapper_Kafka/README.md @@ -72,7 +72,7 @@ A common error is to change something, forget to run build.sh and use an old Doc ### Deploy -We provide a docker compose file to allow you to run a 'Production' environment or to startup RabbitMQ for production: +We provide a docker compose file to allow you to run a 'Production' environment or to startup Kafka and PostgreSQL for production: ```sh docker compose up -d kafka # will just start kafka ``` diff --git a/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Program.cs b/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Program.cs index 745de1ee74..99ed618d26 100644 --- a/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Program.cs +++ b/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Program.cs @@ -204,10 +204,10 @@ 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 string DbConnectionString(HostBuilderContext hostContext) diff --git a/samples/WebAPI_Dapper_Kafka/SalutationPorts/Handlers/GreetingMadeHandler.cs b/samples/WebAPI_Dapper_Kafka/SalutationPorts/Handlers/GreetingMadeHandler.cs index 092e2d0242..adc966d961 100644 --- a/samples/WebAPI_Dapper_Kafka/SalutationPorts/Handlers/GreetingMadeHandler.cs +++ b/samples/WebAPI_Dapper_Kafka/SalutationPorts/Handlers/GreetingMadeHandler.cs @@ -19,6 +19,15 @@ public class GreetingMadeHandler : RequestHandler private readonly IAmACommandProcessor _postBox; private readonly ILogger _logger; + /* + * KAFKA and ASYNC: Kafka is oriented around the idea of an ordered append log of events. You will lose that ordering + * if you use an async handler, because your handlers will not necessarily complete in order. Because a Brighter consumer + * is single-threaded we guarantee your ordering, provided you don't use async handlers. If you do, there are no + * guarantees about order. + * Generally, you should not use async handlers with Kafka, unless you are happy to lose ordering. + * Instead, rely on being able to partition your topic such that a single thread can handle the number of messages + * arriving on that thread with an acceptable latency. + */ public GreetingMadeHandler(IUnitOfWork uow, IAmACommandProcessor postBox, ILogger logger) { _uow = uow; diff --git a/samples/WebAPI_EFCore/GreetingsWeb/Startup.cs b/samples/WebAPI_EFCore/GreetingsWeb/Startup.cs index 3028c0341e..f75796edcb 100644 --- a/samples/WebAPI_EFCore/GreetingsWeb/Startup.cs +++ b/samples/WebAPI_EFCore/GreetingsWeb/Startup.cs @@ -136,7 +136,7 @@ private void ConfigureBrighter(IServiceCollection services) }} ).Create() ) - .UseSqliteOutbox(new SqliteConfiguration(DbConnectionString(), _outBoxTableName), typeof(SqliteConnectionProvider), ServiceLifetime.Singleton) + .UseSqliteOutbox(new RelationalDatabaseConfiguration(DbConnectionString(), _outBoxTableName), typeof(SqliteConnectionProvider), ServiceLifetime.Singleton) .UseSqliteTransactionConnectionProvider(typeof(SqliteEntityFrameworkConnectionProvider), ServiceLifetime.Scoped) .UseOutboxSweeper(options => { @@ -170,7 +170,7 @@ private void ConfigureBrighter(IServiceCollection services) }} ).Create() ) - .UseMySqlOutbox(new MySqlConfiguration(DbConnectionString(), _outBoxTableName), typeof(MySqlConnectionProvider), ServiceLifetime.Singleton) + .UseMySqlOutbox(new RelationalDatabaseConfiguration(DbConnectionString(), _outBoxTableName), typeof(MySqlConnectionProvider), ServiceLifetime.Singleton) .UseMySqTransactionConnectionProvider(typeof(MySqlEntityFrameworkConnectionProvider), ServiceLifetime.Scoped) .UseOutboxSweeper() .AutoFromAssemblies(); diff --git a/samples/WebAPI_EFCore/SalutationAnalytics/Program.cs b/samples/WebAPI_EFCore/SalutationAnalytics/Program.cs index a1afcb3d0b..6a7e895cfc 100644 --- a/samples/WebAPI_EFCore/SalutationAnalytics/Program.cs +++ b/samples/WebAPI_EFCore/SalutationAnalytics/Program.cs @@ -160,10 +160,10 @@ 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 string DbConnectionString(HostBuilderContext hostContext) diff --git a/samples/WebApiWithWorkerAndSweeper/Orders.API/Program.cs b/samples/WebApiWithWorkerAndSweeper/Orders.API/Program.cs index 7522f99c4e..833740c277 100644 --- a/samples/WebApiWithWorkerAndSweeper/Orders.API/Program.cs +++ b/samples/WebApiWithWorkerAndSweeper/Orders.API/Program.cs @@ -31,7 +31,7 @@ var asbConnection = new ServiceBusVisualStudioCredentialClientProvider(asbEndpoint); -var outboxConfig = new MsSqlConfiguration(dbConnString, outBoxTableName: "BrighterOutbox"); +var outboxConfig = new RelationalDatabaseConfiguration(dbConnString, outBoxTableName: "BrighterOutbox"); builder.Services .AddBrighter(opt => diff --git a/samples/WebApiWithWorkerAndSweeper/Orders.Sweeper/Extensions/BrighterExtensions.cs b/samples/WebApiWithWorkerAndSweeper/Orders.Sweeper/Extensions/BrighterExtensions.cs index 2db907ae39..cd9de28917 100644 --- a/samples/WebApiWithWorkerAndSweeper/Orders.Sweeper/Extensions/BrighterExtensions.cs +++ b/samples/WebApiWithWorkerAndSweeper/Orders.Sweeper/Extensions/BrighterExtensions.cs @@ -32,7 +32,7 @@ public static WebApplicationBuilder AddBrighter(this WebApplicationBuilder build new() {MakeChannels = OnMissingChannel.Validate, Topic = new RoutingKey("default")} }, boxSettings.BatchChunkSize).Create(); - var outboxSettings = new MsSqlConfiguration(boxSettings.ConnectionString, outBoxTableName: boxSettings.OutboxTableName); + var outboxSettings = new RelationalDatabaseConfiguration(boxSettings.ConnectionString, outBoxTableName: boxSettings.OutboxTableName); Type outboxType; if (boxSettings.UseMsi) diff --git a/samples/WebApiWithWorkerAndSweeper/Orders.Worker/Program.cs b/samples/WebApiWithWorkerAndSweeper/Orders.Worker/Program.cs index 74ac4ce6f2..1e9868c4dd 100644 --- a/samples/WebApiWithWorkerAndSweeper/Orders.Worker/Program.cs +++ b/samples/WebApiWithWorkerAndSweeper/Orders.Worker/Program.cs @@ -50,7 +50,7 @@ -var outboxConfig = new MsSqlConfiguration(dbConnString, outBoxTableName: "BrighterOutbox"); +var outboxConfig = new RelationalDatabaseConfiguration(dbConnString, outBoxTableName: "BrighterOutbox"); //TODO: add your ASB qualified name here var clientProvider = new ServiceBusVisualStudioCredentialClientProvider(".servicebus.windows.net"); diff --git a/src/Paramore.Brighter.Inbox.MsSql/MsSqlInbox.cs b/src/Paramore.Brighter.Inbox.MsSql/MsSqlInbox.cs index abda3e134f..a27a9c6446 100644 --- a/src/Paramore.Brighter.Inbox.MsSql/MsSqlInbox.cs +++ b/src/Paramore.Brighter.Inbox.MsSql/MsSqlInbox.cs @@ -44,7 +44,7 @@ public class MsSqlInbox : IAmAnInboxSync, IAmAnInboxAsync private const int MsSqlDuplicateKeyError_UniqueIndexViolation = 2601; private const int MsSqlDuplicateKeyError_UniqueConstraintViolation = 2627; - private readonly MsSqlConfiguration _configuration; + private readonly RelationalDatabaseConfiguration _configuration; private readonly IMsSqlConnectionProvider _connectionProvider; /// @@ -52,7 +52,7 @@ public class MsSqlInbox : IAmAnInboxSync, IAmAnInboxAsync /// /// The configuration. /// The Connection Provider. - public MsSqlInbox(MsSqlConfiguration configuration, IMsSqlConnectionProvider connectionProvider) + public MsSqlInbox(RelationalDatabaseConfiguration configuration, IMsSqlConnectionProvider connectionProvider) { _configuration = configuration; ContinueOnCapturedContext = false; @@ -63,7 +63,7 @@ public MsSqlInbox(MsSqlConfiguration configuration, IMsSqlConnectionProvider con /// Initializes a new instance of the class. ///
/// The configuration. - public MsSqlInbox(MsSqlConfiguration configuration) : this(configuration, + public MsSqlInbox(RelationalDatabaseConfiguration configuration) : this(configuration, new MsSqlSqlAuthConnectionProvider(configuration)) { } diff --git a/src/Paramore.Brighter.Inbox.MySql/MySqlInbox.cs b/src/Paramore.Brighter.Inbox.MySql/MySqlInbox.cs index 0e6a492a56..c40ca8b2ed 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 RelationalDatabaseConfiguration _configuration; /// /// Initializes a new instance of the class. /// /// The configuration. - public MySqlInbox(MySqlInboxConfiguration configuration) + public MySqlInbox(RelationalDatabaseConfiguration configuration) { _configuration = configuration; ContinueOnCapturedContext = false; 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/PostgresSqlInbox.cs b/src/Paramore.Brighter.Inbox.Postgres/PostgresSqlInbox.cs index 943865fcee..85c07949ef 100644 --- a/src/Paramore.Brighter.Inbox.Postgres/PostgresSqlInbox.cs +++ b/src/Paramore.Brighter.Inbox.Postgres/PostgresSqlInbox.cs @@ -40,7 +40,7 @@ namespace Paramore.Brighter.Inbox.Postgres { public class PostgresSqlInbox : IAmAnInboxSync, IAmAnInboxAsync { - private readonly PostgresSqlInboxConfiguration _configuration; + private readonly RelationalDatabaseConfiguration _configuration; private readonly IPostgreSqlConnectionProvider _connectionProvider; private static readonly ILogger s_logger = ApplicationLogging.CreateLogger(); /// @@ -53,7 +53,7 @@ public class PostgresSqlInbox : IAmAnInboxSync, IAmAnInboxAsync /// public bool ContinueOnCapturedContext { get; set; } - public PostgresSqlInbox(PostgresSqlInboxConfiguration configuration, IPostgreSqlConnectionProvider connectionProvider = null) + public PostgresSqlInbox(RelationalDatabaseConfiguration configuration, IPostgreSqlConnectionProvider connectionProvider = null) { _configuration = configuration; _connectionProvider = connectionProvider; diff --git a/src/Paramore.Brighter.Inbox.Postgres/PostgresSqlInboxConfiguration.cs b/src/Paramore.Brighter.Inbox.Postgres/PostgresSqlInboxConfiguration.cs deleted file mode 100644 index e36fffdb7d..0000000000 --- a/src/Paramore.Brighter.Inbox.Postgres/PostgresSqlInboxConfiguration.cs +++ /dev/null @@ -1,49 +0,0 @@ -#region Licence - -/* The MIT License (MIT) -Copyright © 2020 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 Paramore.Brighter.PostgreSql; - -namespace Paramore.Brighter.Inbox.Postgres -{ - public class PostgresSqlInboxConfiguration - { - public PostgresSqlInboxConfiguration(string connectionString, string tableName) - { - ConnectionString = connectionString; - InBoxTableName = tableName; - } - - /// - /// The connection string to the PostgresSql database - /// - public string ConnectionString { get; } - - /// - /// Gets the name of the outbox table. - /// - /// The name of the outbox table. - public string InBoxTableName { get; } - } -} diff --git a/src/Paramore.Brighter.Inbox.Sqlite/SqliteInbox.cs b/src/Paramore.Brighter.Inbox.Sqlite/SqliteInbox.cs index 5dd83bc4bb..5411e30547 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(RelationalDatabaseConfiguration 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 RelationalDatabaseConfiguration 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.MsSql/MsSqlMessageConsumer.cs b/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlMessageConsumer.cs index f1acd15fc2..570c8d73f8 100644 --- a/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlMessageConsumer.cs +++ b/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlMessageConsumer.cs @@ -14,7 +14,7 @@ public class MsSqlMessageConsumer : IAmAMessageConsumer private readonly MsSqlMessageQueue _sqlQ; public MsSqlMessageConsumer( - MsSqlConfiguration msSqlConfiguration, + RelationalDatabaseConfiguration msSqlConfiguration, string topic, IMsSqlConnectionProvider connectionProvider) { _topic = topic ?? throw new ArgumentNullException(nameof(topic)); @@ -22,7 +22,7 @@ public MsSqlMessageConsumer( } public MsSqlMessageConsumer( - MsSqlConfiguration msSqlConfiguration, + RelationalDatabaseConfiguration msSqlConfiguration, string topic) :this(msSqlConfiguration, topic, new MsSqlSqlAuthConnectionProvider(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..91eb5e70fc 100644 --- a/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlMessageProducer.cs +++ b/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlMessageProducer.cs @@ -58,7 +58,7 @@ public class MsSqlMessageProducer : IAmAMessageProducerSync, IAmAMessageProducer private Publication _publication; // -- placeholder for future use public MsSqlMessageProducer( - MsSqlConfiguration msSqlConfiguration, + RelationalDatabaseConfiguration msSqlConfiguration, IMsSqlConnectionProvider connectionProvider, Publication publication = null) { @@ -70,7 +70,7 @@ public MsSqlMessageProducer( } public MsSqlMessageProducer( - MsSqlConfiguration msSqlConfiguration, + RelationalDatabaseConfiguration msSqlConfiguration, Publication publication = null) : this(msSqlConfiguration, new MsSqlSqlAuthConnectionProvider(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..1739b4046e 100644 --- a/src/Paramore.Brighter.MessagingGateway.MsSql/SqlQueues/MsSqlMessageQueue.cs +++ b/src/Paramore.Brighter.MessagingGateway.MsSql/SqlQueues/MsSqlMessageQueue.cs @@ -17,7 +17,7 @@ public class MsSqlMessageQueue { private const int RetryDelay = 100; private static readonly ILogger s_logger = ApplicationLogging.CreateLogger>(); - private readonly MsSqlConfiguration _configuration; + private readonly RelationalDatabaseConfiguration _configuration; private readonly IMsSqlConnectionProvider _connectionProvider; /// @@ -25,7 +25,7 @@ public class MsSqlMessageQueue /// /// /// - public MsSqlMessageQueue(MsSqlConfiguration configuration, IMsSqlConnectionProvider connectionProvider) + public MsSqlMessageQueue(RelationalDatabaseConfiguration configuration, IMsSqlConnectionProvider connectionProvider) { _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); _connectionProvider = connectionProvider; diff --git a/src/Paramore.Brighter.MsSql.Azure/MsSqlAzureConnectionProviderBase.cs b/src/Paramore.Brighter.MsSql.Azure/MsSqlAzureConnectionProviderBase.cs index b26b4c86c5..887b2952be 100644 --- a/src/Paramore.Brighter.MsSql.Azure/MsSqlAzureConnectionProviderBase.cs +++ b/src/Paramore.Brighter.MsSql.Azure/MsSqlAzureConnectionProviderBase.cs @@ -24,7 +24,7 @@ public abstract class MsSqlAzureConnectionProviderBase : IMsSqlConnectionProvide ///
/// 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; diff --git a/src/Paramore.Brighter.MsSql.Azure/MsSqlChainedConnectionProvider.cs b/src/Paramore.Brighter.MsSql.Azure/MsSqlChainedConnectionProvider.cs index 307e4ee481..265a65b213 100644 --- a/src/Paramore.Brighter.MsSql.Azure/MsSqlChainedConnectionProvider.cs +++ b/src/Paramore.Brighter.MsSql.Azure/MsSqlChainedConnectionProvider.cs @@ -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 ServiceBusChainedClientProvider(RelationalDatabaseConfiguration configuration, params TokenCredential[] credentialSources) : base(configuration) { if (credentialSources == null || credentialSources.Length < 1) diff --git a/src/Paramore.Brighter.MsSql.Azure/MsSqlDefaultAzureConnectionProvider.cs b/src/Paramore.Brighter.MsSql.Azure/MsSqlDefaultAzureConnectionProvider.cs index b35eda2e19..07993e598c 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() { diff --git a/src/Paramore.Brighter.MsSql.Azure/MsSqlManagedIdentityConnectionProvider.cs b/src/Paramore.Brighter.MsSql.Azure/MsSqlManagedIdentityConnectionProvider.cs index b2b3f93b9f..85d589e6e3 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) { } diff --git a/src/Paramore.Brighter.MsSql.Azure/MsSqlSharedTokenCacheConnectionProvider.cs b/src/Paramore.Brighter.MsSql.Azure/MsSqlSharedTokenCacheConnectionProvider.cs index 9e131aa7f8..b12c0344df 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; diff --git a/src/Paramore.Brighter.MsSql.Azure/MsSqlVisualStudioConnectionProvider.cs b/src/Paramore.Brighter.MsSql.Azure/MsSqlVisualStudioConnectionProvider.cs index 80728c509e..135552487b 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) { } diff --git a/src/Paramore.Brighter.MsSql/MsSqlConfiguration.cs b/src/Paramore.Brighter.MsSql/MsSqlConfiguration.cs deleted file mode 100644 index 7d9c14aa9a..0000000000 --- a/src/Paramore.Brighter.MsSql/MsSqlConfiguration.cs +++ /dev/null @@ -1,31 +0,0 @@ -namespace Paramore.Brighter.MsSql -{ - public class MsSqlConfiguration : RelationalDatabaseOutboxConfiguration - { - /// - /// 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, - bool binaryMessagePayload = false) - : base(connectionString, outBoxTableName, queueStoreTable, binaryMessagePayload) - { - InBoxTableName = inboxTableName; - } - - /// - /// Gets the name of the inbox table. - /// - /// The name of the inbox table. - public string InBoxTableName { get; private set; } - } -} diff --git a/src/Paramore.Brighter.MsSql/MsSqlSqlAuthConnectionProvider.cs b/src/Paramore.Brighter.MsSql/MsSqlSqlAuthConnectionProvider.cs index 84a028ab9d..55d880371d 100644 --- a/src/Paramore.Brighter.MsSql/MsSqlSqlAuthConnectionProvider.cs +++ b/src/Paramore.Brighter.MsSql/MsSqlSqlAuthConnectionProvider.cs @@ -12,7 +12,7 @@ public class MsSqlSqlAuthConnectionProvider : IMsSqlConnectionProvider /// Initialise a new instance of Ms Sql Connection provider using Sql Authentication. /// /// Ms Sql Configuration - public MsSqlSqlAuthConnectionProvider(MsSqlConfiguration configuration) + public MsSqlSqlAuthConnectionProvider(RelationalDatabaseConfiguration configuration) { _connectionString = configuration.ConnectionString; } diff --git a/src/Paramore.Brighter.MySql/MySqlConfiguration.cs b/src/Paramore.Brighter.MySql/MySqlConfiguration.cs deleted file mode 100644 index b356b0a45b..0000000000 --- a/src/Paramore.Brighter.MySql/MySqlConfiguration.cs +++ /dev/null @@ -1,56 +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 : RelationalDatabaseOutboxConfiguration - { - /// - /// 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 MySqlConfiguration( - string connectionString, - string outBoxTableName = null, - string inboxTableName = null, - string queueStoreTable = null, - bool binaryMessagePayload = false - ) - : base(connectionString, outBoxTableName, queueStoreTable, binaryMessagePayload) - { - InBoxTableName = inboxTableName; - } - - /// - /// Gets the name of the inbox table. - /// - /// The name of the inbox table. - public string InBoxTableName { get; private set; } - - } -} diff --git a/src/Paramore.Brighter.MySql/MySqlConnectionProvider.cs b/src/Paramore.Brighter.MySql/MySqlConnectionProvider.cs index 3b9c06f03b..791d41f6a1 100644 --- a/src/Paramore.Brighter.MySql/MySqlConnectionProvider.cs +++ b/src/Paramore.Brighter.MySql/MySqlConnectionProvider.cs @@ -39,7 +39,7 @@ public class MySqlConnectionProvider : IMySqlConnectionProvider /// Initialise a new instance of Sqlte Connection provider from a connection string /// /// Ms Sql Configuration - public MySqlConnectionProvider(RelationalDatabaseOutboxConfiguration configuration) + public MySqlConnectionProvider(RelationalDatabaseConfiguration configuration) { _connectionString = configuration.ConnectionString; } diff --git a/src/Paramore.Brighter.Outbox.MsSql/MsSqlOutbox.cs b/src/Paramore.Brighter.Outbox.MsSql/MsSqlOutbox.cs index 8d59ace03a..75c81c68f0 100644 --- a/src/Paramore.Brighter.Outbox.MsSql/MsSqlOutbox.cs +++ b/src/Paramore.Brighter.Outbox.MsSql/MsSqlOutbox.cs @@ -44,7 +44,7 @@ public class MsSqlOutbox : { private const int MsSqlDuplicateKeyError_UniqueIndexViolation = 2601; private const int MsSqlDuplicateKeyError_UniqueConstraintViolation = 2627; - private readonly MsSqlConfiguration _configuration; + private readonly RelationalDatabaseConfiguration _configuration; private readonly IMsSqlConnectionProvider _connectionProvider; /// @@ -52,7 +52,7 @@ public class MsSqlOutbox : /// /// The configuration. /// The connection factory. - public MsSqlOutbox(MsSqlConfiguration configuration, IMsSqlConnectionProvider connectionProvider) : base( + public MsSqlOutbox(RelationalDatabaseConfiguration configuration, IMsSqlConnectionProvider connectionProvider) : base( configuration.OutBoxTableName, new MsSqlQueries(), ApplicationLogging.CreateLogger()) { _configuration = configuration; @@ -64,7 +64,7 @@ public MsSqlOutbox(MsSqlConfiguration configuration, IMsSqlConnectionProvider co /// Initializes a new instance of the class. /// /// The configuration. - public MsSqlOutbox(MsSqlConfiguration configuration) : this(configuration, + public MsSqlOutbox(RelationalDatabaseConfiguration configuration) : this(configuration, new MsSqlSqlAuthConnectionProvider(configuration)) { } diff --git a/src/Paramore.Brighter.Outbox.MsSql/ServiceCollectionExtensions.cs b/src/Paramore.Brighter.Outbox.MsSql/ServiceCollectionExtensions.cs index c4d7368904..cf0da720d5 100644 --- a/src/Paramore.Brighter.Outbox.MsSql/ServiceCollectionExtensions.cs +++ b/src/Paramore.Brighter.Outbox.MsSql/ServiceCollectionExtensions.cs @@ -24,9 +24,9 @@ public static class ServiceCollectionExtensions /// -- 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) + this IBrighterBuilder brighterBuilder, RelationalDatabaseConfiguration configuration, Type connectionProvider, ServiceLifetime serviceLifetime = ServiceLifetime.Singleton, int outboxBulkChunkSize = 100) { - brighterBuilder.Services.AddSingleton(configuration); + brighterBuilder.Services.AddSingleton(configuration); brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IMsSqlConnectionProvider), connectionProvider, serviceLifetime)); brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmAnOutboxSync), BuildMsSqlOutbox, serviceLifetime)); @@ -60,7 +60,7 @@ public static IBrighterBuilder UseMsSqlTransactionConnectionProvider( private static MsSqlOutbox BuildMsSqlOutbox(IServiceProvider provider) { var connectionProvider = provider.GetService(); - var config = provider.GetService(); + var config = provider.GetService(); return new MsSqlOutbox(config, connectionProvider); } diff --git a/src/Paramore.Brighter.Outbox.MySql/MySqlOutbox.cs b/src/Paramore.Brighter.Outbox.MySql/MySqlOutbox.cs index 66faafe2e9..ebf53573cc 100644 --- a/src/Paramore.Brighter.Outbox.MySql/MySqlOutbox.cs +++ b/src/Paramore.Brighter.Outbox.MySql/MySqlOutbox.cs @@ -44,10 +44,10 @@ public class MySqlOutbox : RelationDatabaseOutbox(); private const int MySqlDuplicateKeyError = 1062; - private readonly MySqlConfiguration _configuration; + private readonly RelationalDatabaseConfiguration _configuration; private readonly IMySqlConnectionProvider _connectionProvider; - public MySqlOutbox(MySqlConfiguration configuration, IMySqlConnectionProvider connectionProvider) : base( + public MySqlOutbox(RelationalDatabaseConfiguration configuration, IMySqlConnectionProvider connectionProvider) : base( configuration.OutBoxTableName, new MySqlQueries(), ApplicationLogging.CreateLogger()) { _configuration = configuration; @@ -55,7 +55,7 @@ public MySqlOutbox(MySqlConfiguration configuration, IMySqlConnectionProvider co ContinueOnCapturedContext = false; } - public MySqlOutbox(MySqlConfiguration configuration) : this(configuration, + public MySqlOutbox(RelationalDatabaseConfiguration configuration) : this(configuration, new MySqlConnectionProvider(configuration)) { } diff --git a/src/Paramore.Brighter.Outbox.MySql/ServiceCollectionExtensions.cs b/src/Paramore.Brighter.Outbox.MySql/ServiceCollectionExtensions.cs index 6866bd12c2..30511651f7 100644 --- a/src/Paramore.Brighter.Outbox.MySql/ServiceCollectionExtensions.cs +++ b/src/Paramore.Brighter.Outbox.MySql/ServiceCollectionExtensions.cs @@ -23,9 +23,9 @@ public static class ServiceCollectionExtensions /// -- 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, RelationalDatabaseOutboxConfiguration configuration, Type connectionProvider, ServiceLifetime serviceLifetime = ServiceLifetime.Singleton) + this IBrighterBuilder brighterBuilder, RelationalDatabaseConfiguration configuration, Type connectionProvider, ServiceLifetime serviceLifetime = ServiceLifetime.Singleton) { - brighterBuilder.Services.AddSingleton(configuration); + brighterBuilder.Services.AddSingleton(configuration); brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IMySqlConnectionProvider), connectionProvider, serviceLifetime)); brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmAnOutboxSync), BuildMySqlOutboxOutbox, serviceLifetime)); @@ -55,7 +55,7 @@ public static IBrighterBuilder UseMySqTransactionConnectionProvider( private static MySqlOutbox BuildMySqlOutboxOutbox(IServiceProvider provider) { - var config = provider.GetService(); + var config = provider.GetService(); var connectionProvider = provider.GetService(); return new MySqlOutbox(config, connectionProvider); diff --git a/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlConfiguration.cs b/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlConfiguration.cs deleted file mode 100644 index d54f59302c..0000000000 --- a/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlConfiguration.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.Outbox.PostgreSql -{ - public class PostgreSqlConfiguration : RelationalDatabaseOutboxConfiguration - { - /// - /// Initialises a new instance of - /// - /// The connection string to the database - /// The name of the outbox within the table - /// The name of the inbox table - /// A store for messages to be written - /// Do we store the payload as text or binary - public PostgreSqlConfiguration( - string connectionString, - string outBoxTableName = null, - string inboxTableName = null, - string queueStoreTable = null, - bool binaryMessagePayload = false) - : base(connectionString, outBoxTableName, queueStoreTable, binaryMessagePayload) - { - InBoxTableName = inboxTableName; - } - - /// - /// Gets the name of the inbox table. - /// - /// The name of the inbox table. - public string InBoxTableName { get; private set; } - } -} diff --git a/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutbox.cs b/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutbox.cs index 9c930b72d7..6e83ab6fb0 100644 --- a/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutbox.cs +++ b/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutbox.cs @@ -42,12 +42,12 @@ public class { private static readonly ILogger s_logger = ApplicationLogging.CreateLogger(); - private readonly PostgreSqlConfiguration _configuration; + private readonly RelationalDatabaseConfiguration _configuration; private readonly IPostgreSqlConnectionProvider _connectionProvider; public PostgreSqlOutbox( - PostgreSqlConfiguration configuration, + RelationalDatabaseConfiguration configuration, IPostgreSqlConnectionProvider connectionProvider) : base( configuration.OutBoxTableName, new PostgreSqlQueries(), ApplicationLogging.CreateLogger()) { @@ -55,7 +55,7 @@ public PostgreSqlOutbox( _connectionProvider = connectionProvider; } - public PostgreSqlOutbox(PostgreSqlConfiguration configuration) + public PostgreSqlOutbox(RelationalDatabaseConfiguration configuration) : this(configuration, new PostgreSqlNpgsqlConnectionProvider(configuration)) { } diff --git a/src/Paramore.Brighter.Outbox.PostgreSql/ServiceCollectionExtensions.cs b/src/Paramore.Brighter.Outbox.PostgreSql/ServiceCollectionExtensions.cs index d16e07b1eb..7916f08000 100644 --- a/src/Paramore.Brighter.Outbox.PostgreSql/ServiceCollectionExtensions.cs +++ b/src/Paramore.Brighter.Outbox.PostgreSql/ServiceCollectionExtensions.cs @@ -8,7 +8,7 @@ namespace Paramore.Brighter.Outbox.PostgreSql public static class ServiceCollectionExtensions { public static IBrighterBuilder UsePostgreSqlOutbox( - this IBrighterBuilder brighterBuilder, PostgreSqlConfiguration configuration, Type connectionProvider = null, ServiceLifetime serviceLifetime = ServiceLifetime.Singleton) + this IBrighterBuilder brighterBuilder, RelationalDatabaseConfiguration configuration, Type connectionProvider = null, ServiceLifetime serviceLifetime = ServiceLifetime.Singleton) { if (brighterBuilder is null) throw new ArgumentNullException($"{nameof(brighterBuilder)} cannot be null.", nameof(brighterBuilder)); @@ -16,7 +16,7 @@ public static IBrighterBuilder UsePostgreSqlOutbox( if (configuration is null) throw new ArgumentNullException($"{nameof(configuration)} cannot be null.", nameof(configuration)); - brighterBuilder.Services.AddSingleton(configuration); + brighterBuilder.Services.AddSingleton(configuration); if (connectionProvider is object) { @@ -61,7 +61,7 @@ public static IBrighterBuilder UsePostgreSqlTransactionConnectionProvider( private static PostgreSqlOutbox BuildPostgreSqlOutboxSync(IServiceProvider provider) { - var config = provider.GetService(); + var config = provider.GetService(); var connectionProvider = provider.GetService(); return new PostgreSqlOutbox(config, connectionProvider); diff --git a/src/Paramore.Brighter.Outbox.Sqlite/ServiceCollectionExtensions.cs b/src/Paramore.Brighter.Outbox.Sqlite/ServiceCollectionExtensions.cs index 350245a0ad..b4a7e4a173 100644 --- a/src/Paramore.Brighter.Outbox.Sqlite/ServiceCollectionExtensions.cs +++ b/src/Paramore.Brighter.Outbox.Sqlite/ServiceCollectionExtensions.cs @@ -23,9 +23,9 @@ public static class ServiceCollectionExtensions /// -- 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) + this IBrighterBuilder brighterBuilder, RelationalDatabaseConfiguration configuration, Type connectionProvider, ServiceLifetime serviceLifetime = ServiceLifetime.Singleton) { - brighterBuilder.Services.AddSingleton(configuration); + brighterBuilder.Services.AddSingleton(configuration); brighterBuilder.Services.Add(new ServiceDescriptor(typeof(ISqliteConnectionProvider), connectionProvider, serviceLifetime)); brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmAnOutboxSync), BuildSqliteOutbox, serviceLifetime)); @@ -55,7 +55,7 @@ public static IBrighterBuilder UseSqliteTransactionConnectionProvider( private static SqliteOutbox BuildSqliteOutbox(IServiceProvider provider) { - var config = provider.GetService(); + var config = provider.GetService(); var connectionProvider = provider.GetService(); return new SqliteOutbox(config, connectionProvider); diff --git a/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutbox.cs b/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutbox.cs index 2c6d39a779..ebf9c9249a 100644 --- a/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutbox.cs +++ b/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutbox.cs @@ -46,7 +46,7 @@ public class SqliteOutbox : RelationDatabaseOutbox @@ -54,7 +54,7 @@ public class SqliteOutbox : RelationDatabaseOutbox /// The configuration to connect to this data store /// Provides a connection to the Db that allows us to enlist in an ambient transaction - public SqliteOutbox(SqliteConfiguration configuration, ISqliteConnectionProvider connectionProvider) : base( + public SqliteOutbox(RelationalDatabaseConfiguration configuration, ISqliteConnectionProvider connectionProvider) : base( configuration.OutBoxTableName, new SqliteQueries(), ApplicationLogging.CreateLogger()) { _configuration = configuration; @@ -66,7 +66,7 @@ public SqliteOutbox(SqliteConfiguration configuration, ISqliteConnectionProvider /// Initializes a new instance of the class. /// /// The configuration to connect to this data store - public SqliteOutbox(SqliteConfiguration configuration) : this(configuration, + public SqliteOutbox(RelationalDatabaseConfiguration configuration) : this(configuration, new SqliteConnectionProvider(configuration)) { } diff --git a/src/Paramore.Brighter.PostgreSql/PostgreSqlNpgsqlConnectionProvider.cs b/src/Paramore.Brighter.PostgreSql/PostgreSqlNpgsqlConnectionProvider.cs index b12acd221e..04294d051f 100644 --- a/src/Paramore.Brighter.PostgreSql/PostgreSqlNpgsqlConnectionProvider.cs +++ b/src/Paramore.Brighter.PostgreSql/PostgreSqlNpgsqlConnectionProvider.cs @@ -9,7 +9,7 @@ public class PostgreSqlNpgsqlConnectionProvider : IPostgreSqlConnectionProvider { private readonly string _connectionString; - public PostgreSqlNpgsqlConnectionProvider(RelationalDatabaseOutboxConfiguration configuration) + public PostgreSqlNpgsqlConnectionProvider(RelationalDatabaseConfiguration configuration) { if (string.IsNullOrWhiteSpace(configuration?.ConnectionString)) throw new ArgumentNullException(nameof(configuration.ConnectionString)); diff --git a/src/Paramore.Brighter.Sqlite/SqliteConfiguration.cs b/src/Paramore.Brighter.Sqlite/SqliteConfiguration.cs deleted file mode 100644 index b80f07707b..0000000000 --- a/src/Paramore.Brighter.Sqlite/SqliteConfiguration.cs +++ /dev/null @@ -1,55 +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 : RelationalDatabaseOutboxConfiguration - { - /// - /// 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 SqliteConfiguration( - string connectionString, - string outBoxTableName = null, - string inboxTableName = null, - string queueStoreTable = null, - bool binaryMessagePayload = false) - : base(connectionString, outBoxTableName, queueStoreTable, binaryMessagePayload) - { - InBoxTableName = inboxTableName; - } - - - /// - /// Gets the name of the inbox table. - /// - /// The name of the inbox table. - public string InBoxTableName { get; private set; } - } -} diff --git a/src/Paramore.Brighter.Sqlite/SqliteConnectionProvider.cs b/src/Paramore.Brighter.Sqlite/SqliteConnectionProvider.cs index 618397b317..53fea4c994 100644 --- a/src/Paramore.Brighter.Sqlite/SqliteConnectionProvider.cs +++ b/src/Paramore.Brighter.Sqlite/SqliteConnectionProvider.cs @@ -39,7 +39,7 @@ public class SqliteConnectionProvider : ISqliteConnectionProvider /// Initialise a new instance of Sqlte Connection provider from a connection string /// /// Ms Sql Configuration - public SqliteConnectionProvider(SqliteConfiguration configuration) + public SqliteConnectionProvider(RelationalDatabaseConfiguration configuration) { _connectionString = configuration.ConnectionString; } diff --git a/src/Paramore.Brighter/RelationalDatabaseOutboxConfiguration.cs b/src/Paramore.Brighter/RelationalDatabaseConfiguration.cs similarity index 81% rename from src/Paramore.Brighter/RelationalDatabaseOutboxConfiguration.cs rename to src/Paramore.Brighter/RelationalDatabaseConfiguration.cs index 7d75b8d38d..b3f791dbb3 100644 --- a/src/Paramore.Brighter/RelationalDatabaseOutboxConfiguration.cs +++ b/src/Paramore.Brighter/RelationalDatabaseConfiguration.cs @@ -1,19 +1,20 @@ namespace Paramore.Brighter { - public class RelationalDatabaseOutboxConfiguration + public class RelationalDatabaseConfiguration { /// - /// Initializes a new instance of the class. + /// 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 - protected RelationalDatabaseOutboxConfiguration( + public RelationalDatabaseConfiguration( string connectionString, string outBoxTableName = null, + string inboxTableName = null, string queueStoreTable = null, bool binaryMessagePayload = false ) @@ -34,6 +35,12 @@ protected RelationalDatabaseOutboxConfiguration( /// /// 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. diff --git a/tests/Paramore.Brighter.MSSQL.Tests/MsSqlTestHelper.cs b/tests/Paramore.Brighter.MSSQL.Tests/MsSqlTestHelper.cs index 2d2206346c..e6d391e60d 100644 --- a/tests/Paramore.Brighter.MSSQL.Tests/MsSqlTestHelper.cs +++ b/tests/Paramore.Brighter.MSSQL.Tests/MsSqlTestHelper.cs @@ -49,9 +49,9 @@ public MsSqlTestHelper(bool binaryMessagePayload = false) _tableName = $"test_{Guid.NewGuid()}"; _connectionProvider = - new MsSqlSqlAuthConnectionProvider(new MsSqlConfiguration(_sqlSettings.TestsBrighterConnectionString)); + new MsSqlSqlAuthConnectionProvider(new RelationalDatabaseConfiguration(_sqlSettings.TestsBrighterConnectionString)); _masterConnectionProvider = - new MsSqlSqlAuthConnectionProvider(new MsSqlConfiguration(_sqlSettings.TestsMasterConnectionString)); + new MsSqlSqlAuthConnectionProvider(new RelationalDatabaseConfiguration(_sqlSettings.TestsMasterConnectionString)); } public void CreateDatabase() @@ -89,15 +89,15 @@ public void SetupQueueDb() CreateQueueTable(); } - public MsSqlConfiguration InboxConfiguration => - new MsSqlConfiguration(_sqlSettings.TestsBrighterConnectionString, inboxTableName: _tableName); + public RelationalDatabaseConfiguration InboxConfiguration => + new RelationalDatabaseConfiguration(_sqlSettings.TestsBrighterConnectionString, inboxTableName: _tableName); - public MsSqlConfiguration OutboxConfiguration => new MsSqlConfiguration( + public RelationalDatabaseConfiguration OutboxConfiguration => new RelationalDatabaseConfiguration( _sqlSettings.TestsBrighterConnectionString, outBoxTableName: _tableName, binaryMessagePayload: _binaryMessagePayload); - public MsSqlConfiguration QueueConfiguration => - new MsSqlConfiguration(_sqlSettings.TestsBrighterConnectionString, queueStoreTable: _tableName); + public RelationalDatabaseConfiguration QueueConfiguration => + new RelationalDatabaseConfiguration(_sqlSettings.TestsBrighterConnectionString, queueStoreTable: _tableName); private void CreateQueueTable() { diff --git a/tests/Paramore.Brighter.MySQL.Tests/MySqlTestHelper.cs b/tests/Paramore.Brighter.MySQL.Tests/MySqlTestHelper.cs index 512c458e2f..580f8de0f6 100644 --- a/tests/Paramore.Brighter.MySQL.Tests/MySqlTestHelper.cs +++ b/tests/Paramore.Brighter.MySQL.Tests/MySqlTestHelper.cs @@ -50,10 +50,10 @@ public void SetupCommandDb() CreateInboxTable(); } - public MySqlInboxConfiguration InboxConfiguration => + public RelationalDatabaseConfiguration InboxConfiguration => new(_mysqlSettings.TestsBrighterConnectionString, _tableName); - public MySqlConfiguration OutboxConfiguration => + public RelationalDatabaseConfiguration OutboxConfiguration => new(_mysqlSettings.TestsBrighterConnectionString, _tableName, binaryMessagePayload: _binaryMessagePayload); public void CleanUpDb() diff --git a/tests/Paramore.Brighter.PostgresSQL.Tests/PostgresSqlTestHelper.cs b/tests/Paramore.Brighter.PostgresSQL.Tests/PostgresSqlTestHelper.cs index ddea1d937f..dc68d58f6c 100644 --- a/tests/Paramore.Brighter.PostgresSQL.Tests/PostgresSqlTestHelper.cs +++ b/tests/Paramore.Brighter.PostgresSQL.Tests/PostgresSqlTestHelper.cs @@ -28,12 +28,12 @@ public PostgresSqlTestHelper(bool binaryMessagePayload = false) _tableName = $"test_{Guid.NewGuid():N}"; } - public PostgreSqlConfiguration Configuration - => new PostgreSqlConfiguration(_postgreSqlSettings.TestsBrighterConnectionString, + public RelationalDatabaseConfiguration Configuration + => new RelationalDatabaseConfiguration(_postgreSqlSettings.TestsBrighterConnectionString, outBoxTableName: _tableName, binaryMessagePayload: _binaryMessagePayload); - public PostgresSqlInboxConfiguration InboxConfiguration - => new PostgresSqlInboxConfiguration(_postgreSqlSettings.TestsBrighterConnectionString, _tableName); + public RelationalDatabaseConfiguration InboxConfiguration + => new RelationalDatabaseConfiguration(_postgreSqlSettings.TestsBrighterConnectionString, _tableName); public void SetupMessageDb() 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..fdc7468e99 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(new RelationalDatabaseConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableName)); _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..7faf63784a 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(new RelationalDatabaseConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableName)); _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..9f5bc91406 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(new RelationalDatabaseConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableName)); _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..872f1aea14 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(new RelationalDatabaseConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableName)); _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..1958085ff5 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(new RelationalDatabaseConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableName)); _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..af477b7017 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(new RelationalDatabaseConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableName)); _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 165d659d41..2a9c422f32 100644 --- a/tests/Paramore.Brighter.Sqlite.Tests/Outbox/SQlOutboxMigrationTests.cs +++ b/tests/Paramore.Brighter.Sqlite.Tests/Outbox/SQlOutboxMigrationTests.cs @@ -43,7 +43,7 @@ public SQlOutboxMigrationTests() { _sqliteTestHelper = new SqliteTestHelper(); _sqliteTestHelper.SetupMessageDb(); - _sqlOutbox = new SqliteOutbox(new SqliteConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableNameMessages)); + _sqlOutbox = new SqliteOutbox(new RelationalDatabaseConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableNameMessages)); _message = new Message(new MessageHeader(Guid.NewGuid(), "test_topic", MessageType.MT_DOCUMENT), new MessageBody("message body")); AddHistoricMessage(_message); 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 a3524147c6..107fba7120 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 @@ -46,7 +46,7 @@ public SqlOutboxDeletingMessagesTests() { _sqliteTestHelper = new SqliteTestHelper(); _sqliteTestHelper.SetupMessageDb(); - _sqlOutbox = new SqliteOutbox(new SqliteConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableNameMessages)); + _sqlOutbox = new SqliteOutbox(new RelationalDatabaseConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableNameMessages)); _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")); 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 d66229457d..856399c194 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 @@ -42,7 +42,7 @@ public SqliteOutboxMessageAlreadyExistsTests() { _sqliteTestHelper = new SqliteTestHelper(); _sqliteTestHelper.SetupMessageDb(); - _sqlOutbox = new SqliteOutbox(new SqliteConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableNameMessages)); + _sqlOutbox = new SqliteOutbox(new RelationalDatabaseConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableNameMessages)); _messageEarliest = new Message(new MessageHeader(Guid.NewGuid(), "test_topic", MessageType.MT_DOCUMENT), new MessageBody("message body")); _sqlOutbox.Add(_messageEarliest); 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 12e51f572e..7a334ec819 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 @@ -43,7 +43,7 @@ public SqliteOutboxMessageAlreadyExistsAsyncTests() { _sqliteTestHelper = new SqliteTestHelper(); _sqliteTestHelper.SetupMessageDb(); - _sqlOutbox = new SqliteOutbox(new SqliteConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableNameMessages)); + _sqlOutbox = new SqliteOutbox(new RelationalDatabaseConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableNameMessages)); _messageEarliest = new Message( new MessageHeader( Guid.NewGuid(), 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 2673d86692..147279e8e8 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 @@ -49,7 +49,7 @@ public SqliteOutboxRangeRequestTests() { _sqliteTestHelper = new SqliteTestHelper(); _sqliteTestHelper.SetupMessageDb(); - _sqlOutbox = new SqliteOutbox(new SqliteConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableNameMessages)); + _sqlOutbox = new SqliteOutbox(new RelationalDatabaseConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableNameMessages)); _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")); 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 d9d1dfc4d3..dedc47d62b 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 @@ -49,7 +49,7 @@ public SqliteOutboxRangeRequestAsyncTests() { _sqliteTestHelper = new SqliteTestHelper(); _sqliteTestHelper.SetupMessageDb(); - _sqlOutbox = new SqliteOutbox(new SqliteConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableNameMessages)); + _sqlOutbox = new SqliteOutbox(new RelationalDatabaseConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableNameMessages)); _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")); 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 abff2cbea3..069135403f 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 @@ -42,7 +42,7 @@ public SqliteOutboxEmptyStoreTests() { _sqliteTestHelper = new SqliteTestHelper(); _sqliteTestHelper.SetupMessageDb(); - _sqlOutbox = new SqliteOutbox(new SqliteConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableNameMessages)); + _sqlOutbox = new SqliteOutbox(new RelationalDatabaseConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableNameMessages)); _messageEarliest = new Message(new MessageHeader(Guid.NewGuid(), "test_topic", MessageType.MT_DOCUMENT), new MessageBody("message body")); } 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 08c20a73da..690bee8512 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 @@ -43,7 +43,7 @@ public SqliteOutboxEmptyStoreAsyncTests() { _sqliteTestHelper = new SqliteTestHelper(); _sqliteTestHelper.SetupMessageDb(); - _sqlOutbox = new SqliteOutbox(new SqliteConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableNameMessages)); + _sqlOutbox = new SqliteOutbox(new RelationalDatabaseConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableNameMessages)); _messageEarliest = new Message(new MessageHeader(Guid.NewGuid(), "test_topic", MessageType.MT_DOCUMENT), new MessageBody("message body")); } 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 index deda20eeba..3b2b378f25 100644 --- 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 @@ -53,7 +53,7 @@ public SqliteOutboxWritingBinaryMessageTests() { _sqliteTestHelper = new SqliteTestHelper(binaryMessagePayload: true); _sqliteTestHelper.SetupMessageDb(); - _sqlOutbox = new SqliteOutbox(new SqliteConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableNameMessages, binaryMessagePayload: true)); + _sqlOutbox = new SqliteOutbox(new RelationalDatabaseConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableNameMessages, binaryMessagePayload: true)); var messageHeader = new MessageHeader( messageId:Guid.NewGuid(), topic: "test_topic", 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 0f89eec3cd..9bc765c2a0 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 @@ -52,7 +52,7 @@ public SqliteOutboxWritingMessageTests() { _sqliteTestHelper = new SqliteTestHelper(); _sqliteTestHelper.SetupMessageDb(); - _sqlOutbox = new SqliteOutbox(new SqliteConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableNameMessages)); + _sqlOutbox = new SqliteOutbox(new RelationalDatabaseConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableNameMessages)); var messageHeader = new MessageHeader( messageId:Guid.NewGuid(), topic: "test_topic", 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 619b7ddd07..384f485652 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 @@ -52,7 +52,7 @@ public SqliteOutboxWritingMessageAsyncTests() { _sqliteTestHelper = new SqliteTestHelper(); _sqliteTestHelper.SetupMessageDb(); - _sqlOutbox = new SqliteOutbox(new SqliteConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableNameMessages)); + _sqlOutbox = new SqliteOutbox(new RelationalDatabaseConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableNameMessages)); var messageHeader = new MessageHeader( Guid.NewGuid(), 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 b977d55376..bdba229adf 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 @@ -46,7 +46,7 @@ public SqlOutboxWritngMessagesTests() { _sqliteTestHelper = new SqliteTestHelper(); _sqliteTestHelper.SetupMessageDb(); - _sqlOutbox = new SqliteOutbox(new SqliteConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableNameMessages)); + _sqlOutbox = new SqliteOutbox(new RelationalDatabaseConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableNameMessages)); _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")); 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 de79fa91fa..0b3ee0a434 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 @@ -47,7 +47,7 @@ public SqlOutboxWritngMessagesAsyncTests() { _sqliteTestHelper = new SqliteTestHelper(); _sqliteTestHelper.SetupMessageDb(); - _sSqlOutbox = new SqliteOutbox(new SqliteConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableNameMessages)); + _sSqlOutbox = new SqliteOutbox(new RelationalDatabaseConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableNameMessages)); } [Fact] 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 3a319c9686..29c173a837 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 @@ -24,7 +24,7 @@ public SqliteOutboxBulkGetAsyncTests() { _sqliteTestHelper = new SqliteTestHelper(); _sqliteTestHelper.SetupMessageDb(); - _sqlOutbox = new SqliteOutbox(new SqliteConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableNameMessages)); + _sqlOutbox = new SqliteOutbox(new RelationalDatabaseConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableNameMessages)); _message = new Message(new MessageHeader(Guid.NewGuid(), _Topic1, MessageType.MT_COMMAND), new MessageBody("message body")); From a2bc2ca709e08d19ce663cf336108cd2d76cfea6 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Sun, 7 May 2023 19:02:41 +0100 Subject: [PATCH 35/89] Alter the interfaces to outboxes, to prevent need to cast --- .../.idea/httpRequests/http-requests-log.http | 154 +++++++++-------- .../GreetingsSender.Web/Program.cs | 2 +- .../ASBTaskQueue/GreetingsWorker/Program.cs | 2 +- .../Handlers/AddGreetingHandlerAsync.cs | 7 +- .../Handlers/AddGreetingHandlerAsync.cs | 7 +- .../Handlers/AddGreetingHandlerAsync.cs | 2 +- .../Handlers/AddPersonHandlerAsync.cs | 2 +- .../Handlers/DeletePersonHandlerAsync.cs | 2 +- .../FIndGreetingsForPersonHandlerAsync.cs | 2 +- .../Handlers/FindPersonByNameHandlerAsync.cs | 2 +- .../Handlers/GreetingMadeHandler.cs | 2 +- .../Orders.Data/SqlConnectionProvider.cs | 19 +-- .../Extensions/BrighterExtensions.cs | 4 +- .../Extensions/HealthCheckExtensions.cs | 3 +- .../BrighterOutboxConnectionHealthCheck.cs | 5 +- .../Settings/BrighterBoxSettings.cs | 1 + src/Paramore.Brighter.Dapper/IUnitOfWork.cs | 2 +- .../IDynamoDbTransactionProvider.cs | 2 +- .../ServiceCollectionExtensions.cs | 6 +- .../MsSqlInbox.cs | 25 ++- .../PostgresSqlInbox.cs | 8 +- .../MsSqlMessageConsumer.cs | 5 +- .../MsSqlMessageProducer.cs | 4 +- .../SqlQueues/MsSqlMessageQueue.cs | 30 ++-- ....cs => MsSqlAzureConnectonProviderBase.cs} | 11 +- .../MsSqlChainedConnectionProvider.cs | 4 +- ... => MsSqlDefaultAzureConnectonProvider.cs} | 4 +- ... MsSqlManagedIdentityConnectonProvider.cs} | 4 +- ...MsSqlSharedTokenCacheConnectonProvider.cs} | 6 +- ... => MsSqlVisualStudioConnectonProvider.cs} | 4 +- .../MsSqlDapperConnectionProvider.cs | 14 +- ...qlEntityFrameworkCoreConnectonProvider.cs} | 13 +- .../IMsSqlConnectionProvider.cs | 15 -- .../IMsSqlTransactionConnectionProvider.cs | 9 - .../MsSqlSqlAuthConnectionProvider.cs | 25 +-- .../MySqlDapperConnectionProvider.cs | 13 +- .../MySqlEntityFrameworkConnectionProvider.cs | 17 +- .../IMySqlConnectionProvider.cs | 65 ------- .../IMySqlTransactionConnectionProvider.cs | 31 ---- .../MySqlConnectionProvider.cs | 25 +-- .../DynamoDbOutbox.cs | 27 ++- .../ServiceCollectionExtensions.cs | 2 +- .../EventStoreOutboxSync.cs | 42 +++-- .../MsSqlOutbox.cs | 100 ++++++----- .../ServiceCollectionExtensions.cs | 6 +- .../MySqlOutbox.cs | 76 +++++---- .../MySqlQueries.cs | 2 +- .../ServiceCollectionExtensions.cs | 6 +- .../PostgreSqlOutbox.cs | 93 +++++----- .../ServiceCollectionExtensions.cs | 14 +- .../ServiceCollectionExtensions.cs | 6 +- .../SqliteOutbox.cs | 83 ++++----- ...greSqlEntityFrameworkConnectonProvider.cs} | 22 +-- .../IPostgreSqlConnectionProvider.cs | 19 --- ...PostgreSqlTransactionConnectionProvider.cs | 4 - .../PostgreSqlNpgsqlConnectionProvider.cs | 25 +-- .../ControlBusReceiverBuilder.cs | 2 +- .../SqliteDapperConnectionProvider.cs | 20 +-- ...SqliteEntityFrameworkConnectionProvider.cs | 24 +-- .../ISqliteConnectionProvider.cs | 65 ------- .../ISqliteTransactionConnectionProvider.cs | 31 ---- .../SqliteConnectionProvider.cs | 24 +-- src/Paramore.Brighter/CommandProcessor.cs | 58 ++++--- .../CommandProcessorBuilder.cs | 16 +- src/Paramore.Brighter/ExternalBusServices.cs | 16 +- .../IAmABoxTransactionConnectionProvider.cs | 7 - .../IAmABoxTransactionProvider.cs | 7 + src/Paramore.Brighter/IAmABulkOutboxAsync.cs | 4 +- src/Paramore.Brighter/IAmABulkOutboxSync.cs | 4 +- .../IAmARelationalDbConnectionProvider.cs | 19 +++ .../IAmATransactionConnectonProvider.cs | 4 + src/Paramore.Brighter/IAmAnOutboxAsync.cs | 4 +- src/Paramore.Brighter/IAmAnOutboxSync.cs | 4 +- src/Paramore.Brighter/InMemoryOutbox.cs | 73 ++++++-- .../RelationDatabaseOutbox.cs | 160 +++++++++++------- .../RelationalDatabaseConfiguration.cs | 1 + .../RelationalDbConnectionProvider.cs | 28 +++ .../TestDoubles/FakeOutboxSync.cs | 12 +- .../MsSqlTestHelper.cs | 24 +-- .../MySqlTestHelper.cs | 14 +- .../PostgresSqlTestHelper.cs | 13 +- ...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 | 4 +- .../When_Removing_Messages_From_The_Outbox.cs | 2 +- ...en_The_Message_Is_Already_In_The_Outbox.cs | 2 +- ..._Message_Is_Already_In_The_Outbox_Async.cs | 2 +- ...es_In_The_Outbox_And_A_Range_Is_Fetched.cs | 2 +- ...The_Outbox_And_A_Range_Is_Fetched_Async.cs | 2 +- ...n_There_Is_No_Message_In_The_Sql_Outbox.cs | 2 +- ...e_Is_No_Message_In_The_Sql_Outbox_Async.cs | 2 +- ...iting_A_Message_To_A_Binary_Body_Outbox.cs | 2 +- .../When_Writing_A_Message_To_The_Outbox.cs | 2 +- ...n_Writing_A_Message_To_The_Outbox_Async.cs | 4 +- .../When_Writing_Messages_To_The_Outbox.cs | 4 +- ...en_Writing_Messages_To_The_Outbox_Async.cs | 16 +- ..._are_received_and_Dispatched_bulk_Async.cs | 4 +- .../SqliteTestHelper.cs | 13 +- 102 files changed, 857 insertions(+), 937 deletions(-) rename src/Paramore.Brighter.MsSql.Azure/{MsSqlAzureConnectionProviderBase.cs => MsSqlAzureConnectonProviderBase.cs} (90%) rename src/Paramore.Brighter.MsSql.Azure/{MsSqlDefaultAzureConnectionProvider.cs => MsSqlDefaultAzureConnectonProvider.cs} (81%) rename src/Paramore.Brighter.MsSql.Azure/{MsSqlManagedIdentityConnectionProvider.cs => MsSqlManagedIdentityConnectonProvider.cs} (81%) rename src/Paramore.Brighter.MsSql.Azure/{MsSqlSharedTokenCacheConnectionProvider.cs => MsSqlSharedTokenCacheConnectonProvider.cs} (84%) rename src/Paramore.Brighter.MsSql.Azure/{MsSqlVisualStudioConnectionProvider.cs => MsSqlVisualStudioConnectonProvider.cs} (81%) rename src/Paramore.Brighter.MsSql.EntityFrameworkCore/{MsSqlEntityFrameworkCoreConnectionProvider.cs => MsSqlEntityFrameworkCoreConnectonProvider.cs} (76%) delete mode 100644 src/Paramore.Brighter.MsSql/IMsSqlConnectionProvider.cs delete mode 100644 src/Paramore.Brighter.MsSql/IMsSqlTransactionConnectionProvider.cs delete mode 100644 src/Paramore.Brighter.MySql/IMySqlConnectionProvider.cs delete mode 100644 src/Paramore.Brighter.MySql/IMySqlTransactionConnectionProvider.cs rename src/Paramore.Brighter.PostgreSql.EntityFrameworkCore/{PostgreSqlEntityFrameworkConnectionProvider.cs => PostgreSqlEntityFrameworkConnectonProvider.cs} (65%) delete mode 100644 src/Paramore.Brighter.PostgreSql/IPostgreSqlConnectionProvider.cs delete mode 100644 src/Paramore.Brighter.PostgreSql/IPostgreSqlTransactionConnectionProvider.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/IAmABoxTransactionConnectionProvider.cs create mode 100644 src/Paramore.Brighter/IAmABoxTransactionProvider.cs create mode 100644 src/Paramore.Brighter/IAmARelationalDbConnectionProvider.cs create mode 100644 src/Paramore.Brighter/IAmATransactionConnectonProvider.cs create mode 100644 src/Paramore.Brighter/RelationalDbConnectionProvider.cs diff --git a/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http b/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http index 44a1221a77..060dde5811 100644 --- a/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http +++ b/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http @@ -1,3 +1,90 @@ +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-05-06T164909.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" +} + +<> 2023-05-06T164825.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" +} + +<> 2023-05-06T164716.500.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-05-06T164705.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" +} + +<> 2023-05-06T164701.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-05-06T164657.500.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-05-06T164601.200.json + +### + POST http://localhost:5000/People/new Content-Type: application/json Content-Length: 23 @@ -481,70 +568,3 @@ Content-Type: application/json ### -POST http://localhost:5000/People/new -Content-Type: application/json - -{ - "Name" : "Tyrion" -} - -<> 2022-10-19T183305.500.json - -### - -GET http://localhost:5000/People/Tyrion - -<> 2022-10-19T180348.500.html - -### - -POST http://localhost:5000/People/new -Content-Type: application/json - -{ - "Name" : "Tyrion" -} - -<> 2022-10-19T180248.500.html - -### - -DELETE http://localhost:5000/People/Tyrion - -<> 2022-10-19T180240.500.html - -### - -POST http://localhost:5000/Greetings/Tyrion/new -Content-Type: application/json - -{ - "Greeting" : "I drink, and I know things" -} - -<> 2022-06-25T125829.200.json - -### - -POST http://localhost:5000/People/new -Content-Type: application/json - -{ - "Name" : "Tyrion" -} - -<> 2022-06-25T125806.200.json - -### - -POST http://localhost:5000/Greetings/Tyrion/new -Content-Type: application/json - -{ - "Greeting" : "I drink, and I know things" -} - -<> 2022-06-24T234446.200.json - -### - diff --git a/samples/ASBTaskQueue/GreetingsSender.Web/Program.cs b/samples/ASBTaskQueue/GreetingsSender.Web/Program.cs index d626a014e2..f94faa0ca4 100644 --- a/samples/ASBTaskQueue/GreetingsSender.Web/Program.cs +++ b/samples/ASBTaskQueue/GreetingsSender.Web/Program.cs @@ -57,7 +57,7 @@ .Create() ) .UseMsSqlOutbox(outboxConfig, typeof(MsSqlSqlAuthConnectionProvider)) - .UseMsSqlTransactionConnectionProvider(typeof(MsSqlEntityFrameworkCoreConnectionProvider)) + .UseMsSqlTransactionConnectionProvider(typeof(MsSqlEntityFrameworkCoreConnectonProvider)) .MapperRegistry(r => { r.Add(typeof(GreetingEvent), typeof(GreetingEventMessageMapper)); diff --git a/samples/ASBTaskQueue/GreetingsWorker/Program.cs b/samples/ASBTaskQueue/GreetingsWorker/Program.cs index f93a1d9e14..0c58499134 100644 --- a/samples/ASBTaskQueue/GreetingsWorker/Program.cs +++ b/samples/ASBTaskQueue/GreetingsWorker/Program.cs @@ -82,7 +82,7 @@ options.UseScoped = true; }).UseMsSqlOutbox(outboxConfig, typeof(MsSqlSqlAuthConnectionProvider)) - .UseMsSqlTransactionConnectionProvider(typeof(MsSqlEntityFrameworkCoreConnectionProvider)) + .UseMsSqlTransactionConnectionProvider(typeof(MsSqlEntityFrameworkCoreConnectonProvider)) .AutoFromAssemblies(); builder.Services.AddHostedService(); diff --git a/samples/WebAPI_Dapper/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs b/samples/WebAPI_Dapper/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs index c4f768a767..5962ce037a 100644 --- a/samples/WebAPI_Dapper/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs +++ b/samples/WebAPI_Dapper/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs @@ -12,6 +12,7 @@ using Microsoft.Extensions.Logging; using Paramore.Brighter.Logging.Attributes; using Paramore.Brighter.Policies.Attributes; +using Paramore.Brighter.PostgreSql; using Paramore.Brighter.Sqlite.Dapper; namespace GreetingsPorts.Handlers @@ -20,12 +21,12 @@ public class AddGreetingHandlerAsync: RequestHandlerAsync { private readonly IAmACommandProcessor _postBox; private readonly ILogger _logger; - private readonly SqliteDapperConnectionProvider _uow; + private readonly RelationalDbConnectionProvider _uow; - public AddGreetingHandlerAsync(IAmABoxTransactionConnectionProvider uow, IAmACommandProcessor postBox, ILogger logger) + public AddGreetingHandlerAsync(RelationalDbConnectionProvider uow, 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 + _uow = uow; //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; } diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs index c4f768a767..5962ce037a 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs +++ b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs @@ -12,6 +12,7 @@ using Microsoft.Extensions.Logging; using Paramore.Brighter.Logging.Attributes; using Paramore.Brighter.Policies.Attributes; +using Paramore.Brighter.PostgreSql; using Paramore.Brighter.Sqlite.Dapper; namespace GreetingsPorts.Handlers @@ -20,12 +21,12 @@ public class AddGreetingHandlerAsync: RequestHandlerAsync { private readonly IAmACommandProcessor _postBox; private readonly ILogger _logger; - private readonly SqliteDapperConnectionProvider _uow; + private readonly RelationalDbConnectionProvider _uow; - public AddGreetingHandlerAsync(IAmABoxTransactionConnectionProvider uow, IAmACommandProcessor postBox, ILogger logger) + public AddGreetingHandlerAsync(RelationalDbConnectionProvider uow, 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 + _uow = uow; //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; } diff --git a/samples/WebAPI_Dynamo/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs b/samples/WebAPI_Dynamo/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs index 00b1511547..69b1f1b125 100644 --- a/samples/WebAPI_Dynamo/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs +++ b/samples/WebAPI_Dynamo/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs @@ -21,7 +21,7 @@ public class AddGreetingHandlerAsync: RequestHandlerAsync private readonly ILogger _logger; - public AddGreetingHandlerAsync(IAmABoxTransactionConnectionProvider uow, IAmACommandProcessor postBox, ILogger logger) + public AddGreetingHandlerAsync(IAmABoxTransactionProvider uow, IAmACommandProcessor postBox, ILogger logger) { _unitOfWork = (DynamoDbUnitOfWork)uow; _postBox = postBox; diff --git a/samples/WebAPI_Dynamo/GreetingsPorts/Handlers/AddPersonHandlerAsync.cs b/samples/WebAPI_Dynamo/GreetingsPorts/Handlers/AddPersonHandlerAsync.cs index fdced54ece..918c774ff2 100644 --- a/samples/WebAPI_Dynamo/GreetingsPorts/Handlers/AddPersonHandlerAsync.cs +++ b/samples/WebAPI_Dynamo/GreetingsPorts/Handlers/AddPersonHandlerAsync.cs @@ -14,7 +14,7 @@ public class AddPersonHandlerAsync : RequestHandlerAsync { private readonly DynamoDbUnitOfWork _dynamoDbUnitOfWork; - public AddPersonHandlerAsync(IAmABoxTransactionConnectionProvider dynamoDbUnitOfWork) + public AddPersonHandlerAsync(IAmABoxTransactionProvider dynamoDbUnitOfWork) { _dynamoDbUnitOfWork = (DynamoDbUnitOfWork )dynamoDbUnitOfWork; } diff --git a/samples/WebAPI_Dynamo/GreetingsPorts/Handlers/DeletePersonHandlerAsync.cs b/samples/WebAPI_Dynamo/GreetingsPorts/Handlers/DeletePersonHandlerAsync.cs index bc63f0be3e..747f46d269 100644 --- a/samples/WebAPI_Dynamo/GreetingsPorts/Handlers/DeletePersonHandlerAsync.cs +++ b/samples/WebAPI_Dynamo/GreetingsPorts/Handlers/DeletePersonHandlerAsync.cs @@ -13,7 +13,7 @@ public class DeletePersonHandlerAsync : RequestHandlerAsync { private readonly DynamoDbUnitOfWork _unitOfWork; - public DeletePersonHandlerAsync(IAmABoxTransactionConnectionProvider unitOfWork) + public DeletePersonHandlerAsync(IAmABoxTransactionProvider unitOfWork) { _unitOfWork = (DynamoDbUnitOfWork)unitOfWork; } diff --git a/samples/WebAPI_Dynamo/GreetingsPorts/Handlers/FIndGreetingsForPersonHandlerAsync.cs b/samples/WebAPI_Dynamo/GreetingsPorts/Handlers/FIndGreetingsForPersonHandlerAsync.cs index aecaae4237..06466e3bc2 100644 --- a/samples/WebAPI_Dynamo/GreetingsPorts/Handlers/FIndGreetingsForPersonHandlerAsync.cs +++ b/samples/WebAPI_Dynamo/GreetingsPorts/Handlers/FIndGreetingsForPersonHandlerAsync.cs @@ -18,7 +18,7 @@ public class FIndGreetingsForPersonHandlerAsync : QueryHandlerAsync private readonly IAmACommandProcessor _postBox; private readonly ILogger _logger; - public GreetingMadeHandlerAsync(IAmABoxTransactionConnectionProvider uow, IAmACommandProcessor postBox, ILogger logger) + public GreetingMadeHandlerAsync(IAmABoxTransactionProvider uow, IAmACommandProcessor postBox, ILogger logger) { _uow = (DynamoDbUnitOfWork)uow; _postBox = postBox; diff --git a/samples/WebApiWithWorkerAndSweeper/Orders.Data/SqlConnectionProvider.cs b/samples/WebApiWithWorkerAndSweeper/Orders.Data/SqlConnectionProvider.cs index eaead8234a..4c04ae3e59 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.PostgreSql; 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 SqlTransaction? 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 cd9de28917..703b317dc0 100644 --- a/samples/WebApiWithWorkerAndSweeper/Orders.Sweeper/Extensions/BrighterExtensions.cs +++ b/samples/WebApiWithWorkerAndSweeper/Orders.Sweeper/Extensions/BrighterExtensions.cs @@ -39,11 +39,11 @@ public static WebApplicationBuilder AddBrighter(this WebApplicationBuilder build { if (environmentName != null && environmentName.Equals(_developmentEnvironemntName, StringComparison.InvariantCultureIgnoreCase)) { - outboxType = typeof(MsSqlVisualStudioConnectionProvider); + outboxType = typeof(MsSqlVisualStudioConnectonProvider); } else { - outboxType = typeof(MsSqlDefaultAzureConnectionProvider); + outboxType = typeof(MsSqlDefaultAzureConnectonProvider); } } else 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..6c582a95f4 100644 --- a/samples/WebApiWithWorkerAndSweeper/Orders.Sweeper/HealthChecks/BrighterOutboxConnectionHealthCheck.cs +++ b/samples/WebApiWithWorkerAndSweeper/Orders.Sweeper/HealthChecks/BrighterOutboxConnectionHealthCheck.cs @@ -1,14 +1,15 @@ 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; } 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/src/Paramore.Brighter.Dapper/IUnitOfWork.cs b/src/Paramore.Brighter.Dapper/IUnitOfWork.cs index 642c18fc1f..9d3adc12d8 100644 --- a/src/Paramore.Brighter.Dapper/IUnitOfWork.cs +++ b/src/Paramore.Brighter.Dapper/IUnitOfWork.cs @@ -8,7 +8,7 @@ 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 + public interface IUnitOfWork : IAmABoxTransactionProvider, IDisposable { /// /// Begins a new transaction against the database. Will open the connection if it is not already open, diff --git a/src/Paramore.Brighter.DynamoDb/IDynamoDbTransactionProvider.cs b/src/Paramore.Brighter.DynamoDb/IDynamoDbTransactionProvider.cs index 90dad7735e..c837c6eede 100644 --- a/src/Paramore.Brighter.DynamoDb/IDynamoDbTransactionProvider.cs +++ b/src/Paramore.Brighter.DynamoDb/IDynamoDbTransactionProvider.cs @@ -1,6 +1,6 @@ namespace Paramore.Brighter.DynamoDb { - public interface IDynamoDbClientTransactionProvider : IDynamoDbClientProvider, IAmABoxTransactionConnectionProvider + public interface IDynamoDbClientTransactionProvider : IDynamoDbClientProvider, IAmABoxTransactionProvider { } diff --git a/src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionExtensions.cs b/src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionExtensions.cs index f9bd4a0a57..c57d9372ce 100644 --- a/src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionExtensions.cs @@ -293,7 +293,7 @@ private static CommandProcessor BuildCommandProcessor(IServiceProvider provider) var outbox = provider.GetService>(); var asyncOutbox = provider.GetService>(); - var overridingConnectionProvider = provider.GetService(); + var overridingConnectionProvider = provider.GetService(); if (outbox == null) outbox = new InMemoryOutbox(); if (asyncOutbox == null) asyncOutbox = new InMemoryOutbox(); @@ -347,7 +347,7 @@ private static INeedARequestContext AddExternalBusMaybe( MessageMapperRegistry messageMapperRegistry, InboxConfiguration inboxConfiguration, IAmAnOutboxSync outbox, - IAmABoxTransactionConnectionProvider overridingConnectionProvider, + IAmATransactionConnectonProvider overridingProvider, IUseRpc useRequestResponse, int outboxBulkChunkSize, IAmAMessageTransformerFactory transformerFactory) @@ -365,7 +365,7 @@ private static INeedARequestContext AddExternalBusMaybe( useInbox: inboxConfiguration, transformerFactory: transformerFactory), outbox, - overridingConnectionProvider); + overridingProvider); else if (externalBusType == ExternalBusType.RPC) { return messagingBuilder.ExternalRPC( diff --git a/src/Paramore.Brighter.Inbox.MsSql/MsSqlInbox.cs b/src/Paramore.Brighter.Inbox.MsSql/MsSqlInbox.cs index a27a9c6446..196eea60e5 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; @@ -32,6 +34,7 @@ THE SOFTWARE. */ using Paramore.Brighter.Inbox.Exceptions; using Paramore.Brighter.MsSql; using Paramore.Brighter.Logging; +using Paramore.Brighter.PostgreSql; namespace Paramore.Brighter.Inbox.MsSql { @@ -45,14 +48,14 @@ public class MsSqlInbox : IAmAnInboxSync, IAmAnInboxAsync private const int MsSqlDuplicateKeyError_UniqueIndexViolation = 2601; private const int MsSqlDuplicateKeyError_UniqueConstraintViolation = 2627; private readonly RelationalDatabaseConfiguration _configuration; - private readonly IMsSqlConnectionProvider _connectionProvider; + private readonly RelationalDbConnectionProvider _connectionProvider; /// /// Initializes a new instance of the class. /// /// The configuration. /// The Connection Provider. - public MsSqlInbox(RelationalDatabaseConfiguration configuration, IMsSqlConnectionProvider connectionProvider) + public MsSqlInbox(RelationalDatabaseConfiguration configuration, RelationalDbConnectionProvider connectionProvider) { _configuration = configuration; ContinueOnCapturedContext = false; @@ -255,8 +258,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()) @@ -272,11 +279,11 @@ private T ExecuteCommand(Func execute, string sql, int timeout } 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()) @@ -291,7 +298,7 @@ private async Task ExecuteCommandAsync( } } - 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 +311,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 +325,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.Postgres/PostgresSqlInbox.cs b/src/Paramore.Brighter.Inbox.Postgres/PostgresSqlInbox.cs index 85c07949ef..ac40fd8b50 100644 --- a/src/Paramore.Brighter.Inbox.Postgres/PostgresSqlInbox.cs +++ b/src/Paramore.Brighter.Inbox.Postgres/PostgresSqlInbox.cs @@ -41,7 +41,7 @@ namespace Paramore.Brighter.Inbox.Postgres public class PostgresSqlInbox : IAmAnInboxSync, IAmAnInboxAsync { private readonly RelationalDatabaseConfiguration _configuration; - private readonly IPostgreSqlConnectionProvider _connectionProvider; + 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 @@ -53,7 +53,7 @@ public class PostgresSqlInbox : IAmAnInboxSync, IAmAnInboxAsync /// public bool ContinueOnCapturedContext { get; set; } - public PostgresSqlInbox(RelationalDatabaseConfiguration configuration, IPostgreSqlConnectionProvider connectionProvider = null) + public PostgresSqlInbox(RelationalDatabaseConfiguration configuration, IAmARelationalDbConnectionProvider connectionProvider = null) { _configuration = configuration; _connectionProvider = connectionProvider; @@ -191,9 +191,9 @@ private DbConnection GetConnection() 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); diff --git a/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlMessageConsumer.cs b/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlMessageConsumer.cs index 570c8d73f8..45008874dc 100644 --- a/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlMessageConsumer.cs +++ b/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlMessageConsumer.cs @@ -4,6 +4,7 @@ using Paramore.Brighter.Logging; using Paramore.Brighter.MessagingGateway.MsSql.SqlQueues; using Paramore.Brighter.MsSql; +using Paramore.Brighter.PostgreSql; namespace Paramore.Brighter.MessagingGateway.MsSql { @@ -15,7 +16,9 @@ public class MsSqlMessageConsumer : IAmAMessageConsumer public MsSqlMessageConsumer( RelationalDatabaseConfiguration msSqlConfiguration, - string topic, IMsSqlConnectionProvider connectionProvider) + string topic, + RelationalDbConnectionProvider connectionProvider + ) { _topic = topic ?? throw new ArgumentNullException(nameof(topic)); _sqlQ = new MsSqlMessageQueue(msSqlConfiguration, connectionProvider); diff --git a/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlMessageProducer.cs b/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlMessageProducer.cs index 91eb5e70fc..7bc54c6088 100644 --- a/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlMessageProducer.cs +++ b/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlMessageProducer.cs @@ -59,10 +59,10 @@ public class MsSqlMessageProducer : IAmAMessageProducerSync, IAmAMessageProducer public MsSqlMessageProducer( RelationalDatabaseConfiguration msSqlConfiguration, - IMsSqlConnectionProvider connectionProvider, + 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; diff --git a/src/Paramore.Brighter.MessagingGateway.MsSql/SqlQueues/MsSqlMessageQueue.cs b/src/Paramore.Brighter.MessagingGateway.MsSql/SqlQueues/MsSqlMessageQueue.cs index 1739b4046e..6edd80c923 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 { @@ -18,14 +19,14 @@ public class MsSqlMessageQueue private const int RetryDelay = 100; private static readonly ILogger s_logger = ApplicationLogging.CreateLogger>(); private readonly RelationalDatabaseConfiguration _configuration; - private readonly IMsSqlConnectionProvider _connectionProvider; + private readonly IAmARelationalDbConnectionProvider _connectionProvider; /// /// Initializes a new instance of the class. /// /// /// - public MsSqlMessageQueue(RelationalDatabaseConfiguration configuration, IMsSqlConnectionProvider connectionProvider) + public MsSqlMessageQueue(RelationalDatabaseConfiguration configuration, IAmARelationalDbConnectionProvider connectionProvider) { _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); _connectionProvider = connectionProvider; @@ -72,8 +73,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); @@ -195,23 +195,23 @@ public void Purge() } } - 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 +223,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 +243,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.MsSql.Azure/MsSqlAzureConnectionProviderBase.cs b/src/Paramore.Brighter.MsSql.Azure/MsSqlAzureConnectonProviderBase.cs similarity index 90% rename from src/Paramore.Brighter.MsSql.Azure/MsSqlAzureConnectionProviderBase.cs rename to src/Paramore.Brighter.MsSql.Azure/MsSqlAzureConnectonProviderBase.cs index 887b2952be..71256efafb 100644 --- a/src/Paramore.Brighter.MsSql.Azure/MsSqlAzureConnectionProviderBase.cs +++ b/src/Paramore.Brighter.MsSql.Azure/MsSqlAzureConnectonProviderBase.cs @@ -1,4 +1,5 @@ using System; +using System.Data.Common; using System.Threading; using System.Threading.Tasks; using Azure.Core; @@ -7,7 +8,7 @@ namespace Paramore.Brighter.MsSql.Azure { - public abstract class MsSqlAzureConnectionProviderBase : IMsSqlConnectionProvider + public abstract class MsSqlAzureConnectonProviderBase : IAmATransactionConnectonProvider { private readonly bool _cacheTokens; private const string _azureScope = "https://database.windows.net/.default"; @@ -24,14 +25,14 @@ public abstract class MsSqlAzureConnectionProviderBase : IMsSqlConnectionProvide /// /// Ms Sql Configuration. /// Cache Access Tokens until they have less than 5 minutes of life left. - protected MsSqlAzureConnectionProviderBase(RelationalDatabaseConfiguration configuration, bool cacheTokens = true) + protected MsSqlAzureConnectonProviderBase(RelationalDatabaseConfiguration configuration, bool cacheTokens = true) { _cacheTokens = cacheTokens; _connectionString = configuration.ConnectionString; _authenticationTokenScopes = new string[1] {_azureScope}; } - public SqlConnection GetConnection() + public DbConnection GetConnection() { var sqlConnection = new SqlConnection(_connectionString); sqlConnection.AccessToken = GetAccessToken(); @@ -39,7 +40,7 @@ public SqlConnection GetConnection() return sqlConnection; } - public async Task GetConnectionAsync( + public async Task GetConnectionAsync( CancellationToken cancellationToken = default) { var sqlConnection = new SqlConnection(_connectionString); @@ -96,7 +97,7 @@ private async Task GetAccessTokenAsync(CancellationToken cancellationTok protected abstract Task GetAccessTokenFromProviderAsync(CancellationToken cancellationToken); - public SqlTransaction GetTransaction() + public DbTransaction GetTransaction() { //This Connection Factory does not support Transactions return null; diff --git a/src/Paramore.Brighter.MsSql.Azure/MsSqlChainedConnectionProvider.cs b/src/Paramore.Brighter.MsSql.Azure/MsSqlChainedConnectionProvider.cs index 265a65b213..6cc615db30 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 ServiceBusChainedClientConnectonProvider : MsSqlAzureConnectonProviderBase { 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(RelationalDatabaseConfiguration configuration, + public ServiceBusChainedClientConnectonProvider(RelationalDatabaseConfiguration configuration, params TokenCredential[] credentialSources) : base(configuration) { if (credentialSources == null || credentialSources.Length < 1) diff --git a/src/Paramore.Brighter.MsSql.Azure/MsSqlDefaultAzureConnectionProvider.cs b/src/Paramore.Brighter.MsSql.Azure/MsSqlDefaultAzureConnectonProvider.cs similarity index 81% rename from src/Paramore.Brighter.MsSql.Azure/MsSqlDefaultAzureConnectionProvider.cs rename to src/Paramore.Brighter.MsSql.Azure/MsSqlDefaultAzureConnectonProvider.cs index 07993e598c..e02773a2e6 100644 --- a/src/Paramore.Brighter.MsSql.Azure/MsSqlDefaultAzureConnectionProvider.cs +++ b/src/Paramore.Brighter.MsSql.Azure/MsSqlDefaultAzureConnectonProvider.cs @@ -5,13 +5,13 @@ namespace Paramore.Brighter.MsSql.Azure { - public class MsSqlDefaultAzureConnectionProvider : MsSqlAzureConnectionProviderBase + public class MsSqlDefaultAzureConnectonProvider : MsSqlAzureConnectonProviderBase { /// /// Initialise a new instance of Ms Sql Connection provider using Default Azure Credentials to acquire Access Tokens. /// /// Ms Sql Configuration - public MsSqlDefaultAzureConnectionProvider(RelationalDatabaseConfiguration configuration) : base(configuration) { } + public MsSqlDefaultAzureConnectonProvider(RelationalDatabaseConfiguration configuration) : base(configuration) { } protected override AccessToken GetAccessTokenFromProvider() { diff --git a/src/Paramore.Brighter.MsSql.Azure/MsSqlManagedIdentityConnectionProvider.cs b/src/Paramore.Brighter.MsSql.Azure/MsSqlManagedIdentityConnectonProvider.cs similarity index 81% rename from src/Paramore.Brighter.MsSql.Azure/MsSqlManagedIdentityConnectionProvider.cs rename to src/Paramore.Brighter.MsSql.Azure/MsSqlManagedIdentityConnectonProvider.cs index 85d589e6e3..f1308a7031 100644 --- a/src/Paramore.Brighter.MsSql.Azure/MsSqlManagedIdentityConnectionProvider.cs +++ b/src/Paramore.Brighter.MsSql.Azure/MsSqlManagedIdentityConnectonProvider.cs @@ -5,13 +5,13 @@ namespace Paramore.Brighter.MsSql.Azure { - public class MsSqlManagedIdentityConnectionProvider : MsSqlAzureConnectionProviderBase + public class MsSqlManagedIdentityConnectonProvider : MsSqlAzureConnectonProviderBase { /// /// Initialise a new instance of Ms Sql Connection provider using Managed Identity Credentials to acquire Access Tokens. /// /// Ms Sql Configuration - public MsSqlManagedIdentityConnectionProvider(RelationalDatabaseConfiguration configuration) : base(configuration) + public MsSqlManagedIdentityConnectonProvider(RelationalDatabaseConfiguration configuration) : base(configuration) { } diff --git a/src/Paramore.Brighter.MsSql.Azure/MsSqlSharedTokenCacheConnectionProvider.cs b/src/Paramore.Brighter.MsSql.Azure/MsSqlSharedTokenCacheConnectonProvider.cs similarity index 84% rename from src/Paramore.Brighter.MsSql.Azure/MsSqlSharedTokenCacheConnectionProvider.cs rename to src/Paramore.Brighter.MsSql.Azure/MsSqlSharedTokenCacheConnectonProvider.cs index b12c0344df..e5e170012a 100644 --- a/src/Paramore.Brighter.MsSql.Azure/MsSqlSharedTokenCacheConnectionProvider.cs +++ b/src/Paramore.Brighter.MsSql.Azure/MsSqlSharedTokenCacheConnectonProvider.cs @@ -6,7 +6,7 @@ namespace Paramore.Brighter.MsSql.Azure { - public class MsSqlSharedTokenCacheConnectionProvider : MsSqlAzureConnectionProviderBase + public class MsSqlSharedTokenCacheConnectonProvider : MsSqlAzureConnectonProviderBase { private const string _azureUserNameKey = "AZURE_USERNAME"; private const string _azureTenantIdKey = "AZURE_TENANT_ID"; @@ -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(RelationalDatabaseConfiguration configuration) : base(configuration) + public MsSqlSharedTokenCacheConnectonProvider(RelationalDatabaseConfiguration configuration) : base(configuration) { _azureUserName = Environment.GetEnvironmentVariable(_azureUserNameKey); _azureTenantId = Environment.GetEnvironmentVariable(_azureTenantIdKey); @@ -28,7 +28,7 @@ public MsSqlSharedTokenCacheConnectionProvider(RelationalDatabaseConfiguration c /// Initialise a new instance of Ms Sql Connection provider using Shared Token Cache Credentials to acquire Access Tokens. /// /// Ms Sql Configuration - public MsSqlSharedTokenCacheConnectionProvider(RelationalDatabaseConfiguration configuration, string userName, string tenantId) : base(configuration) + public MsSqlSharedTokenCacheConnectonProvider(RelationalDatabaseConfiguration configuration, string userName, string tenantId) : base(configuration) { _azureUserName = userName; _azureTenantId = tenantId; diff --git a/src/Paramore.Brighter.MsSql.Azure/MsSqlVisualStudioConnectionProvider.cs b/src/Paramore.Brighter.MsSql.Azure/MsSqlVisualStudioConnectonProvider.cs similarity index 81% rename from src/Paramore.Brighter.MsSql.Azure/MsSqlVisualStudioConnectionProvider.cs rename to src/Paramore.Brighter.MsSql.Azure/MsSqlVisualStudioConnectonProvider.cs index 135552487b..81944f8d9f 100644 --- a/src/Paramore.Brighter.MsSql.Azure/MsSqlVisualStudioConnectionProvider.cs +++ b/src/Paramore.Brighter.MsSql.Azure/MsSqlVisualStudioConnectonProvider.cs @@ -5,13 +5,13 @@ namespace Paramore.Brighter.MsSql.Azure { - public class MsSqlVisualStudioConnectionProvider : MsSqlAzureConnectionProviderBase + public class MsSqlVisualStudioConnectonProvider : MsSqlAzureConnectonProviderBase { /// /// Initialise a new instance of Ms Sql Connection provider using Visual Studio Credentials to acquire Access Tokens. /// /// Ms Sql Configuration - public MsSqlVisualStudioConnectionProvider(RelationalDatabaseConfiguration configuration) : base(configuration) + public MsSqlVisualStudioConnectonProvider(RelationalDatabaseConfiguration configuration) : base(configuration) { } diff --git a/src/Paramore.Brighter.MsSql.Dapper/MsSqlDapperConnectionProvider.cs b/src/Paramore.Brighter.MsSql.Dapper/MsSqlDapperConnectionProvider.cs index 2565a5413e..c657a3b74f 100644 --- a/src/Paramore.Brighter.MsSql.Dapper/MsSqlDapperConnectionProvider.cs +++ b/src/Paramore.Brighter.MsSql.Dapper/MsSqlDapperConnectionProvider.cs @@ -1,12 +1,12 @@ -using System.Threading; +using System.Data.Common; +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 + public class MsSqlDapperConnectionProvider : IAmATransactionConnectonProvider { private readonly IUnitOfWork _unitOfWork; @@ -15,19 +15,19 @@ public MsSqlDapperConnectionProvider(IUnitOfWork unitOfWork) _unitOfWork = unitOfWork; } - public SqlConnection GetConnection() + public DbConnection GetConnection() { return (SqlConnection)_unitOfWork.Database; } - public Task GetConnectionAsync(CancellationToken cancellationToken = default) + public Task GetConnectionAsync(CancellationToken cancellationToken = default) { - var tcs = new TaskCompletionSource(); + var tcs = new TaskCompletionSource(); tcs.SetResult(GetConnection()); return tcs.Task; } - public SqlTransaction GetTransaction() + public DbTransaction GetTransaction() { return (SqlTransaction)_unitOfWork.BeginOrGetTransaction(); } diff --git a/src/Paramore.Brighter.MsSql.EntityFrameworkCore/MsSqlEntityFrameworkCoreConnectionProvider.cs b/src/Paramore.Brighter.MsSql.EntityFrameworkCore/MsSqlEntityFrameworkCoreConnectonProvider.cs similarity index 76% rename from src/Paramore.Brighter.MsSql.EntityFrameworkCore/MsSqlEntityFrameworkCoreConnectionProvider.cs rename to src/Paramore.Brighter.MsSql.EntityFrameworkCore/MsSqlEntityFrameworkCoreConnectonProvider.cs index 36ded089ec..ad8987eee0 100644 --- a/src/Paramore.Brighter.MsSql.EntityFrameworkCore/MsSqlEntityFrameworkCoreConnectionProvider.cs +++ b/src/Paramore.Brighter.MsSql.EntityFrameworkCore/MsSqlEntityFrameworkCoreConnectonProvider.cs @@ -1,4 +1,5 @@ -using System.Threading; +using System.Data.Common; +using System.Threading; using System.Threading.Tasks; using Microsoft.Data.SqlClient; using Microsoft.EntityFrameworkCore; @@ -6,33 +7,33 @@ namespace Paramore.Brighter.MsSql.EntityFrameworkCore { - public class MsSqlEntityFrameworkCoreConnectionProvider : IMsSqlTransactionConnectionProvider where T : DbContext + public class MsSqlEntityFrameworkCoreConnectonProvider : IAmATransactionConnectonProvider where T : DbContext { private readonly T _context; /// /// Initialise a new instance of Ms Sql Connection provider using the Database Connection from an Entity Framework Core DbContext. /// - public MsSqlEntityFrameworkCoreConnectionProvider(T context) + public MsSqlEntityFrameworkCoreConnectonProvider(T context) { _context = context; } - public SqlConnection GetConnection() + public 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(); } - public async Task GetConnectionAsync(CancellationToken cancellationToken = default) + public 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(); } - public SqlTransaction GetTransaction() + public DbTransaction GetTransaction() { var trans = (SqlTransaction)_context.Database.CurrentTransaction?.GetDbTransaction(); return trans; 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/MsSqlSqlAuthConnectionProvider.cs b/src/Paramore.Brighter.MsSql/MsSqlSqlAuthConnectionProvider.cs index 55d880371d..341c287057 100644 --- a/src/Paramore.Brighter.MsSql/MsSqlSqlAuthConnectionProvider.cs +++ b/src/Paramore.Brighter.MsSql/MsSqlSqlAuthConnectionProvider.cs @@ -1,10 +1,10 @@ -using System.Threading; -using System.Threading.Tasks; +using System.Data.Common; using Microsoft.Data.SqlClient; +using Paramore.Brighter.PostgreSql; namespace Paramore.Brighter.MsSql { - public class MsSqlSqlAuthConnectionProvider : IMsSqlConnectionProvider + public class MsSqlSqlAuthConnectionProvider : RelationalDbConnectionProvider { private readonly string _connectionString; @@ -17,26 +17,9 @@ public MsSqlSqlAuthConnectionProvider(RelationalDatabaseConfiguration configurat _connectionString = configuration.ConnectionString; } - public SqlConnection GetConnection() + public override DbConnection 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.MySql.Dapper/MySqlDapperConnectionProvider.cs b/src/Paramore.Brighter.MySql.Dapper/MySqlDapperConnectionProvider.cs index 4643fc8391..c2f4b101d6 100644 --- a/src/Paramore.Brighter.MySql.Dapper/MySqlDapperConnectionProvider.cs +++ b/src/Paramore.Brighter.MySql.Dapper/MySqlDapperConnectionProvider.cs @@ -1,11 +1,12 @@ -using System.Threading; +using System.Data.Common; +using System.Threading; using System.Threading.Tasks; using MySqlConnector; using Paramore.Brighter.Dapper; namespace Paramore.Brighter.MySql.Dapper { - public class MySqlDapperConnectionProvider : IMySqlTransactionConnectionProvider + public class MySqlDapperConnectionProvider : IAmATransactionConnectonProvider { private readonly IUnitOfWork _unitOfWork; @@ -14,19 +15,19 @@ public MySqlDapperConnectionProvider(IUnitOfWork unitOfWork) _unitOfWork = unitOfWork; } - public MySqlConnection GetConnection() + public DbConnection GetConnection() { return (MySqlConnection)_unitOfWork.Database; } - public Task GetConnectionAsync(CancellationToken cancellationToken = default) + public Task GetConnectionAsync(CancellationToken cancellationToken = default) { - var tcs = new TaskCompletionSource(); + var tcs = new TaskCompletionSource(); tcs.SetResult(GetConnection()); return tcs.Task; } - public MySqlTransaction GetTransaction() + public DbTransaction GetTransaction() { return (MySqlTransaction)_unitOfWork.BeginOrGetTransaction(); } diff --git a/src/Paramore.Brighter.MySql.EntityFrameworkCore/MySqlEntityFrameworkConnectionProvider.cs b/src/Paramore.Brighter.MySql.EntityFrameworkCore/MySqlEntityFrameworkConnectionProvider.cs index e8e419d7f8..e4986a0ed2 100644 --- a/src/Paramore.Brighter.MySql.EntityFrameworkCore/MySqlEntityFrameworkConnectionProvider.cs +++ b/src/Paramore.Brighter.MySql.EntityFrameworkCore/MySqlEntityFrameworkConnectionProvider.cs @@ -1,4 +1,5 @@ -using System.Threading; +using System.Data.Common; +using System.Threading; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Storage; @@ -10,7 +11,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 : IAmATransactionConnectonProvider where T: DbContext { private readonly T _context; @@ -27,11 +28,11 @@ public MySqlEntityFrameworkConnectionProvider(T context) /// Get the current connection of the DB context /// /// The Sqlite Connection that is in use - public MySqlConnection GetConnection() + public 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,20 +40,20 @@ public MySqlConnection GetConnection() /// /// A cancellation token /// - public async Task GetConnectionAsync(CancellationToken cancellationToken = default) + public 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 DbTransaction GetTransaction() { - return (MySqlTransaction)_context.Database.CurrentTransaction?.GetDbTransaction(); + return _context.Database.CurrentTransaction?.GetDbTransaction(); } public bool HasOpenTransaction { get => _context.Database.CurrentTransaction != null; } 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/MySqlConnectionProvider.cs b/src/Paramore.Brighter.MySql/MySqlConnectionProvider.cs index 791d41f6a1..ea50fc44dd 100644 --- a/src/Paramore.Brighter.MySql/MySqlConnectionProvider.cs +++ b/src/Paramore.Brighter.MySql/MySqlConnectionProvider.cs @@ -22,16 +22,16 @@ THE SOFTWARE. */ #endregion -using System.Threading; -using System.Threading.Tasks; +using System.Data.Common; using MySqlConnector; +using Paramore.Brighter.PostgreSql; 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; @@ -44,26 +44,9 @@ public MySqlConnectionProvider(RelationalDatabaseConfiguration configuration) _connectionString = configuration.ConnectionString; } - public MySqlConnection GetConnection() + public override DbConnection GetConnection() { return new MySqlConnection(_connectionString); } - - public async Task GetConnectionAsync(CancellationToken cancellationToken = default) - { - var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - - tcs.SetResult(GetConnection()); - return await tcs.Task; - } - - public MySqlTransaction 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.Outbox.DynamoDB/DynamoDbOutbox.cs b/src/Paramore.Brighter.Outbox.DynamoDB/DynamoDbOutbox.cs index 9c4ee67421..805424a3a4 100644 --- a/src/Paramore.Brighter.Outbox.DynamoDB/DynamoDbOutbox.cs +++ b/src/Paramore.Brighter.Outbox.DynamoDB/DynamoDbOutbox.cs @@ -76,7 +76,7 @@ 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, IAmATransactionConnectonProvider transactionProvider = null) { AddAsync(message, outBoxTimeout).ConfigureAwait(ContinueOnCapturedContext).GetAwaiter().GetResult(); } @@ -88,13 +88,17 @@ 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) + public async Task AddAsync( + Message message, + int outBoxTimeout = -1, + CancellationToken cancellationToken = default, + IAmATransactionConnectonProvider transactionProvider = null) { var messageToStore = new MessageItem(message); - if (transactionConnectionProvider != null) + if (transactionProvider != null) { - await AddToTransactionWrite(messageToStore, (DynamoDbUnitOfWork)transactionConnectionProvider); + await AddToTransactionWrite(messageToStore, (DynamoDbUnitOfWork)transactionProvider); } else { @@ -219,7 +223,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); @@ -230,8 +239,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) { diff --git a/src/Paramore.Brighter.Outbox.DynamoDB/ServiceCollectionExtensions.cs b/src/Paramore.Brighter.Outbox.DynamoDB/ServiceCollectionExtensions.cs index 85e1765131..4f1edfa72b 100644 --- a/src/Paramore.Brighter.Outbox.DynamoDB/ServiceCollectionExtensions.cs +++ b/src/Paramore.Brighter.Outbox.DynamoDB/ServiceCollectionExtensions.cs @@ -43,7 +43,7 @@ public static IBrighterBuilder UseDynamoDbTransactionConnectionProvider( this IBrighterBuilder brighterBuilder, Type connectionProvider, ServiceLifetime serviceLifetime = ServiceLifetime.Scoped) { - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmABoxTransactionConnectionProvider), connectionProvider, serviceLifetime)); + brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmABoxTransactionProvider), connectionProvider, serviceLifetime)); return brighterBuilder; } diff --git a/src/Paramore.Brighter.Outbox.EventStore/EventStoreOutboxSync.cs b/src/Paramore.Brighter.Outbox.EventStore/EventStoreOutboxSync.cs index 67deaff90d..595eab2d89 100644 --- a/src/Paramore.Brighter.Outbox.EventStore/EventStoreOutboxSync.cs +++ b/src/Paramore.Brighter.Outbox.EventStore/EventStoreOutboxSync.cs @@ -41,9 +41,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(); @@ -75,7 +73,7 @@ public EventStoreOutboxSync(IEventStoreConnection eventStore) /// The message. /// The outBoxTimeout. /// Task. - public void Add(Message message, int outBoxTimeout = -1, IAmABoxTransactionConnectionProvider transactionConnectionProvider = null) + public void Add(Message message, int outBoxTimeout = -1, IAmATransactionConnectonProvider transactionProvider = null) { s_logger.LogDebug("Adding message to Event Store Outbox: {Request}", JsonSerializer.Serialize(message, JsonSerialisationOptions.Options)); @@ -96,8 +94,12 @@ public void Add(Message message, int outBoxTimeout = -1, IAmABoxTransactionConne /// The time allowed for the write in milliseconds; on a -1 default /// Allows the sender to cancel the request pipeline. Optional /// . - 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, + IAmATransactionConnectonProvider transactionProvider = null + ) { s_logger.LogDebug("Adding message to Event Store Outbox: {Request}", JsonSerializer.Serialize(message, JsonSerialisationOptions.Options)); @@ -199,7 +201,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 +231,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 +270,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.MsSql/MsSqlOutbox.cs b/src/Paramore.Brighter.Outbox.MsSql/MsSqlOutbox.cs index 75c81c68f0..43a7589830 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; @@ -33,26 +34,26 @@ THE SOFTWARE. */ using System.Threading.Tasks; using Paramore.Brighter.Logging; using Paramore.Brighter.MsSql; +using Paramore.Brighter.PostgreSql; namespace Paramore.Brighter.Outbox.MsSql { /// /// Class MsSqlOutbox. /// - public class MsSqlOutbox : - RelationDatabaseOutbox + public class MsSqlOutbox : RelationDatabaseOutbox { private const int MsSqlDuplicateKeyError_UniqueIndexViolation = 2601; private const int MsSqlDuplicateKeyError_UniqueConstraintViolation = 2627; private readonly RelationalDatabaseConfiguration _configuration; - private readonly IMsSqlConnectionProvider _connectionProvider; + private readonly IAmARelationalDbConnectionProvider _connectionProvider; /// /// Initializes a new instance of the class. /// /// The configuration. /// The connection factory. - public MsSqlOutbox(RelationalDatabaseConfiguration configuration, IMsSqlConnectionProvider connectionProvider) : base( + public MsSqlOutbox(RelationalDatabaseConfiguration configuration, IAmARelationalDbConnectionProvider connectionProvider) : base( configuration.OutBoxTableName, new MsSqlQueries(), ApplicationLogging.CreateLogger()) { _configuration = configuration; @@ -69,13 +70,14 @@ public MsSqlOutbox(RelationalDatabaseConfiguration configuration) : this(configu { } - protected override void WriteToStore(IAmABoxTransactionConnectionProvider transactionConnectionProvider, - Func commandFunc, Action loggingAction) + protected override void WriteToStore( + IAmATransactionConnectonProvider transactionProvider, + Func commandFunc, + Action loggingAction) { var connectionProvider = _connectionProvider; - if (transactionConnectionProvider != null && - transactionConnectionProvider is IMsSqlTransactionConnectionProvider provider) - connectionProvider = provider; + if (transactionProvider != null) + connectionProvider = transactionProvider; var connection = connectionProvider.GetConnection(); @@ -85,7 +87,7 @@ protected override void WriteToStore(IAmABoxTransactionConnectionProvider transa { try { - if (transactionConnectionProvider != null && connectionProvider.HasOpenTransaction) + if (transactionProvider != null && connectionProvider.HasOpenTransaction) command.Transaction = connectionProvider.GetTransaction(); command.ExecuteNonQuery(); } @@ -111,13 +113,14 @@ protected override void WriteToStore(IAmABoxTransactionConnectionProvider transa } protected override async Task WriteToStoreAsync( - IAmABoxTransactionConnectionProvider transactionConnectionProvider, - Func commandFunc, Action loggingAction, CancellationToken cancellationToken) + IAmATransactionConnectonProvider transactionProvider, + Func commandFunc, + Action loggingAction, + CancellationToken cancellationToken) { var connectionProvider = _connectionProvider; - if (transactionConnectionProvider != null && - transactionConnectionProvider is IMsSqlTransactionConnectionProvider provider) - connectionProvider = provider; + if (transactionProvider != null) + connectionProvider = transactionProvider; var connection = await connectionProvider.GetConnectionAsync(cancellationToken) .ConfigureAwait(ContinueOnCapturedContext); @@ -128,7 +131,7 @@ protected override async Task WriteToStoreAsync( { try { - if (transactionConnectionProvider != null && connectionProvider.HasOpenTransaction) + if (transactionProvider != null && connectionProvider.HasOpenTransaction) command.Transaction = connectionProvider.GetTransaction(); await command.ExecuteNonQueryAsync(cancellationToken); } @@ -153,8 +156,10 @@ protected override async Task WriteToStoreAsync( } } - protected override T ReadFromStore(Func commandFunc, - Func resultFunc) + protected override T ReadFromStore( + Func commandFunc, + Func resultFunc + ) { var connection = _connectionProvider.GetConnection(); @@ -176,8 +181,11 @@ protected override T ReadFromStore(Func commandFun } } - 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); @@ -199,8 +207,12 @@ 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) => + 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; @@ -325,7 +337,7 @@ private string GetContentType(SqlDataReader dr) return contentType; } - private string GetReplyTo(SqlDataReader dr) + private string GetReplyTo(DbDataReader dr) { var ordinal = dr.GetOrdinal("ReplyTo"); if (dr.IsDBNull(ordinal)) return null; @@ -334,7 +346,7 @@ private string GetReplyTo(SqlDataReader dr) 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); @@ -343,7 +355,7 @@ private static Dictionary GetContextBag(SqlDataReader dr) return dictionaryBag; } - private Guid? GetCorrelationId(SqlDataReader dr) + private Guid? GetCorrelationId(DbDataReader dr) { var ordinal = dr.GetOrdinal("CorrelationId"); if (dr.IsDBNull(ordinal)) return null; @@ -352,7 +364,7 @@ private static Dictionary GetContextBag(SqlDataReader dr) return correlationId; } - private static DateTime GetTimeStamp(SqlDataReader dr) + private static DateTime GetTimeStamp(DbDataReader dr) { var ordinal = dr.GetOrdinal("Timestamp"); var timeStamp = dr.IsDBNull(ordinal) @@ -361,7 +373,7 @@ private static DateTime GetTimeStamp(SqlDataReader dr) return timeStamp; } - private string GetPartitionKey(SqlDataReader dr) + private string GetPartitionKey(DbDataReader dr) { var ordinal = dr.GetOrdinal("PartitionKey"); if (dr.IsDBNull(ordinal)) return null; @@ -370,7 +382,7 @@ private string GetPartitionKey(SqlDataReader dr) return partitionKey; } - private byte[] GetBodyAsBytes(SqlDataReader dr) + private byte[] GetBodyAsBytes(DbDataReader dr) { var ordinal = dr.GetOrdinal("Body"); if (dr.IsDBNull(ordinal)) return null; @@ -382,7 +394,7 @@ private byte[] GetBodyAsBytes(SqlDataReader dr) return buffer; } - private static string GetBodyAsText(SqlDataReader dr) + private static string GetBodyAsText(DbDataReader dr) { var ordinal = dr.GetOrdinal("Body"); return dr.IsDBNull(ordinal) ? null : dr.GetString(ordinal); @@ -392,7 +404,7 @@ private static string GetBodyAsText(SqlDataReader dr) #region DataReader Operators - protected override Message MapFunction(SqlDataReader dr) + protected override Message MapFunction(DbDataReader dr) { Message message = null; if (dr.Read()) @@ -405,7 +417,7 @@ protected override Message MapFunction(SqlDataReader dr) 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)) @@ -418,7 +430,7 @@ protected override async Task MapFunctionAsync(SqlDataReader dr, Cancel return message ?? new Message(); } - protected override IEnumerable MapListFunction(SqlDataReader dr) + protected override IEnumerable MapListFunction(DbDataReader dr) { var messages = new List(); while (dr.Read()) @@ -431,8 +443,10 @@ protected override IEnumerable MapListFunction(SqlDataReader dr) 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)) @@ -447,7 +461,7 @@ protected override async Task> MapListFunctionAsync(SqlData #endregion - private Message MapAMessage(SqlDataReader dr) + private Message MapAMessage(DbDataReader dr) { var id = GetMessageId(dr); var messageType = GetMessageType(dr); diff --git a/src/Paramore.Brighter.Outbox.MsSql/ServiceCollectionExtensions.cs b/src/Paramore.Brighter.Outbox.MsSql/ServiceCollectionExtensions.cs index cf0da720d5..ff51489f6a 100644 --- a/src/Paramore.Brighter.Outbox.MsSql/ServiceCollectionExtensions.cs +++ b/src/Paramore.Brighter.Outbox.MsSql/ServiceCollectionExtensions.cs @@ -27,7 +27,7 @@ public static IBrighterBuilder UseMsSqlOutbox( this IBrighterBuilder brighterBuilder, RelationalDatabaseConfiguration 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(IAmARelationalDbConnectionProvider), connectionProvider, serviceLifetime)); brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmAnOutboxSync), BuildMsSqlOutbox, serviceLifetime)); brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmAnOutboxAsync), BuildMsSqlOutbox, serviceLifetime)); @@ -52,14 +52,14 @@ public static IBrighterBuilder UseMsSqlTransactionConnectionProvider( this IBrighterBuilder brighterBuilder, Type connectionProvider, ServiceLifetime serviceLifetime = ServiceLifetime.Scoped) { - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmABoxTransactionConnectionProvider), connectionProvider, serviceLifetime)); + brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmABoxTransactionProvider), connectionProvider, serviceLifetime)); return brighterBuilder; } private static MsSqlOutbox BuildMsSqlOutbox(IServiceProvider provider) { - var connectionProvider = provider.GetService(); + var connectionProvider = provider.GetService(); var config = provider.GetService(); return new MsSqlOutbox(config, connectionProvider); diff --git a/src/Paramore.Brighter.Outbox.MySql/MySqlOutbox.cs b/src/Paramore.Brighter.Outbox.MySql/MySqlOutbox.cs index ebf53573cc..07731b5376 100644 --- a/src/Paramore.Brighter.Outbox.MySql/MySqlOutbox.cs +++ b/src/Paramore.Brighter.Outbox.MySql/MySqlOutbox.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,35 +40,36 @@ namespace Paramore.Brighter.Outbox.MySql /// /// Class MySqlOutbox. /// - public class MySqlOutbox : RelationDatabaseOutbox + public class MySqlOutbox : RelationDatabaseOutbox { private static readonly ILogger s_logger = ApplicationLogging.CreateLogger(); private const int MySqlDuplicateKeyError = 1062; private readonly RelationalDatabaseConfiguration _configuration; - private readonly IMySqlConnectionProvider _connectionProvider; + private readonly IAmARelationalDbConnectionProvider _connectionProvider; - public MySqlOutbox(RelationalDatabaseConfiguration configuration, IMySqlConnectionProvider connectionProvider) : base( - configuration.OutBoxTableName, new MySqlQueries(), ApplicationLogging.CreateLogger()) + public MySqlOutbox(RelationalDatabaseConfiguration configuration, IAmARelationalDbConnectionProvider connectionProvider) + : base(configuration.OutBoxTableName, new MySqlQueries(), ApplicationLogging.CreateLogger()) { _configuration = configuration; _connectionProvider = connectionProvider; ContinueOnCapturedContext = false; } - public MySqlOutbox(RelationalDatabaseConfiguration configuration) : this(configuration, - new MySqlConnectionProvider(configuration)) + public MySqlOutbox(RelationalDatabaseConfiguration configuration) + : this(configuration, new MySqlConnectionProvider(configuration)) { } - protected override void WriteToStore(IAmABoxTransactionConnectionProvider transactionConnectionProvider, - Func commandFunc, - Action loggingAction) + protected override void WriteToStore( + IAmATransactionConnectonProvider transactionProvider, + Func commandFunc, + Action loggingAction + ) { var connectionProvider = _connectionProvider; - if (transactionConnectionProvider != null && - transactionConnectionProvider is IMySqlTransactionConnectionProvider provider) - connectionProvider = provider; + if (transactionProvider != null) + connectionProvider = transactionProvider; var connection = connectionProvider.GetConnection(); @@ -77,7 +79,7 @@ protected override void WriteToStore(IAmABoxTransactionConnectionProvider transa { try { - if (transactionConnectionProvider != null && connectionProvider.HasOpenTransaction) + if (transactionProvider != null && connectionProvider.HasOpenTransaction) command.Transaction = connectionProvider.GetTransaction(); command.ExecuteNonQuery(); } @@ -103,15 +105,15 @@ protected override void WriteToStore(IAmABoxTransactionConnectionProvider transa } protected override async Task WriteToStoreAsync( - IAmABoxTransactionConnectionProvider transactionConnectionProvider, - Func commandFunc, + IAmATransactionConnectonProvider transactionProvider, + Func commandFunc, Action loggingAction, - CancellationToken cancellationToken) + CancellationToken cancellationToken + ) { var connectionProvider = _connectionProvider; - if (transactionConnectionProvider != null && - transactionConnectionProvider is IMySqlTransactionConnectionProvider provider) - connectionProvider = provider; + if (transactionProvider != null) + connectionProvider = transactionProvider; var connection = await connectionProvider.GetConnectionAsync(cancellationToken) .ConfigureAwait(ContinueOnCapturedContext); @@ -122,7 +124,7 @@ protected override async Task WriteToStoreAsync( { try { - if (transactionConnectionProvider != null && connectionProvider.HasOpenTransaction) + if (transactionProvider != null && connectionProvider.HasOpenTransaction) command.Transaction = connectionProvider.GetTransaction(); await command.ExecuteNonQueryAsync(cancellationToken); } @@ -142,15 +144,15 @@ protected override async Task WriteToStoreAsync( if (!connectionProvider.IsSharedConnection) connection.Dispose(); else if (!connectionProvider.HasOpenTransaction) - await connection.CloseAsync(); + connection.Close(); } } } protected override T ReadFromStore( - Func commandFunc, - Func resultFunc) + Func commandFunc, + Func resultFunc + ) { var connection = _connectionProvider.GetConnection(); @@ -173,8 +175,8 @@ protected override T ReadFromStore( } protected override async Task ReadFromStoreAsync( - Func commandFunc, - Func> resultFunc, + Func commandFunc, + Func> resultFunc, CancellationToken cancellationToken) { var connection = await _connectionProvider.GetConnectionAsync(cancellationToken); @@ -197,11 +199,11 @@ protected override async Task ReadFromStoreAsync( } } - protected override MySqlCommand CreateCommand( - MySqlConnection connection, + protected override DbCommand CreateCommand( + DbConnection connection, string sqlText, int outBoxTimeout, - params MySqlParameter[] parameters) + params IDbDataParameter[] parameters) { var command = connection.CreateCommand(); @@ -212,13 +214,13 @@ protected override MySqlCommand CreateCommand( return command; } - protected override MySqlParameter CreateSqlParameter(string parameterName, object value) + protected override IDbDataParameter CreateSqlParameter(string parameterName, object value) { return new MySqlParameter { ParameterName = parameterName, Value = value }; } - protected override MySqlParameter[] InitAddDbParameters(Message message, int? position = null) + protected override IDbDataParameter[] InitAddDbParameters(Message message, int? position = null) { var prefix = position.HasValue ? $"p{position}_" : ""; var bagJson = JsonSerializer.Serialize(message.Header.Bag, JsonSerialisationOptions.Options); @@ -279,11 +281,11 @@ protected override MySqlParameter[] InitAddDbParameters(Message message, int? po }; } - protected override MySqlParameter[] CreatePagedOutstandingParameters(double milliSecondsSinceAdded, + protected override IDbDataParameter[] CreatePagedOutstandingParameters(double milliSecondsSinceAdded, int pageSize, int pageNumber) { var offset = (pageNumber - 1) * pageSize; - var parameters = new MySqlParameter[3]; + var parameters = new IDbDataParameter[3]; parameters[0] = CreateSqlParameter("OffsetValue", offset); parameters[1] = CreateSqlParameter("PageSize", pageSize); parameters[2] = CreateSqlParameter("OutstandingSince", milliSecondsSinceAdded); @@ -291,7 +293,7 @@ protected override MySqlParameter[] CreatePagedOutstandingParameters(double mill return parameters; } - protected override Message MapFunction(MySqlDataReader dr) + protected override Message MapFunction(DbDataReader dr) { if (dr.Read()) { @@ -301,7 +303,7 @@ protected override Message MapFunction(MySqlDataReader dr) return new Message(); } - protected override async Task MapFunctionAsync(MySqlDataReader dr, CancellationToken cancellationToken) + protected override async Task MapFunctionAsync(DbDataReader dr, CancellationToken cancellationToken) { if (await dr.ReadAsync(cancellationToken)) { @@ -311,7 +313,7 @@ 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()) @@ -325,7 +327,7 @@ protected override IEnumerable MapListFunction(MySqlDataReader dr) } protected override async Task> MapListFunctionAsync( - MySqlDataReader dr, + DbDataReader dr, CancellationToken cancellationToken) { var messages = new List(); diff --git a/src/Paramore.Brighter.Outbox.MySql/MySqlQueries.cs b/src/Paramore.Brighter.Outbox.MySql/MySqlQueries.cs index 99d96f8a4d..602819d6a9 100644 --- a/src/Paramore.Brighter.Outbox.MySql/MySqlQueries.cs +++ b/src/Paramore.Brighter.Outbox.MySql/MySqlQueries.cs @@ -10,7 +10,7 @@ public class MySqlQueries : IRelationDatabaseOutboxQueries 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 ASC Timestamp"; + 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} )"; } } diff --git a/src/Paramore.Brighter.Outbox.MySql/ServiceCollectionExtensions.cs b/src/Paramore.Brighter.Outbox.MySql/ServiceCollectionExtensions.cs index 30511651f7..12d0f39bdf 100644 --- a/src/Paramore.Brighter.Outbox.MySql/ServiceCollectionExtensions.cs +++ b/src/Paramore.Brighter.Outbox.MySql/ServiceCollectionExtensions.cs @@ -26,7 +26,7 @@ public static IBrighterBuilder UseMySqlOutbox( this IBrighterBuilder brighterBuilder, RelationalDatabaseConfiguration 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(IAmARelationalDbConnectionProvider), connectionProvider, serviceLifetime)); brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmAnOutboxSync), BuildMySqlOutboxOutbox, serviceLifetime)); brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmAnOutboxAsync), BuildMySqlOutboxOutbox, serviceLifetime)); @@ -48,7 +48,7 @@ public static IBrighterBuilder UseMySqTransactionConnectionProvider( this IBrighterBuilder brighterBuilder, Type connectionProvider, ServiceLifetime serviceLifetime = ServiceLifetime.Scoped) { - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmABoxTransactionConnectionProvider), connectionProvider, serviceLifetime)); + brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmABoxTransactionProvider), connectionProvider, serviceLifetime)); return brighterBuilder; } @@ -56,7 +56,7 @@ public static IBrighterBuilder UseMySqTransactionConnectionProvider( private static MySqlOutbox BuildMySqlOutboxOutbox(IServiceProvider provider) { var config = provider.GetService(); - var connectionProvider = provider.GetService(); + var connectionProvider = provider.GetService(); return new MySqlOutbox(config, connectionProvider); } diff --git a/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutbox.cs b/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutbox.cs index 6e83ab6fb0..60a075993c 100644 --- a/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutbox.cs +++ b/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutbox.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; @@ -37,18 +38,17 @@ THE SOFTWARE. */ namespace Paramore.Brighter.Outbox.PostgreSql { - public class - PostgreSqlOutbox : RelationDatabaseOutbox + public class PostgreSqlOutbox : RelationDatabaseOutbox { private static readonly ILogger s_logger = ApplicationLogging.CreateLogger(); private readonly RelationalDatabaseConfiguration _configuration; - private readonly IPostgreSqlConnectionProvider _connectionProvider; + private readonly IAmARelationalDbConnectionProvider _connectionProvider; public PostgreSqlOutbox( RelationalDatabaseConfiguration configuration, - IPostgreSqlConnectionProvider connectionProvider) : base( + IAmARelationalDbConnectionProvider connectionProvider) : base( configuration.OutBoxTableName, new PostgreSqlQueries(), ApplicationLogging.CreateLogger()) { _configuration = configuration; @@ -59,14 +59,14 @@ public PostgreSqlOutbox(RelationalDatabaseConfiguration configuration) : this(configuration, new PostgreSqlNpgsqlConnectionProvider(configuration)) { } - protected override void WriteToStore(IAmABoxTransactionConnectionProvider transactionConnectionProvider, - Func commandFunc, + protected override void WriteToStore( + IAmATransactionConnectonProvider transactionConnectonProvider, + Func commandFunc, Action loggingAction) { var connectionProvider = _connectionProvider; - if (transactionConnectionProvider != null && - transactionConnectionProvider is IPostgreSqlConnectionProvider provider) - connectionProvider = provider; + if (transactionConnectonProvider != null) + connectionProvider = transactionConnectonProvider; var connection = connectionProvider.GetConnection(); @@ -76,7 +76,7 @@ protected override void WriteToStore(IAmABoxTransactionConnectionProvider transa { try { - if (transactionConnectionProvider != null && connectionProvider.HasOpenTransaction) + if (transactionConnectonProvider != null && connectionProvider.HasOpenTransaction) command.Transaction = connectionProvider.GetTransaction(); command.ExecuteNonQuery(); } @@ -101,15 +101,14 @@ protected override void WriteToStore(IAmABoxTransactionConnectionProvider transa } protected override async Task WriteToStoreAsync( - IAmABoxTransactionConnectionProvider transactionConnectionProvider, - Func commandFunc, + IAmATransactionConnectonProvider transactionConnectionProvider, + Func commandFunc, Action loggingAction, CancellationToken cancellationToken) { var connectionProvider = _connectionProvider; - if (transactionConnectionProvider != null && - transactionConnectionProvider is IPostgreSqlConnectionProvider provider) - connectionProvider = provider; + if (transactionConnectionProvider != null) + connectionProvider = transactionConnectionProvider; var connection = await connectionProvider.GetConnectionAsync(cancellationToken) .ConfigureAwait(ContinueOnCapturedContext); @@ -140,13 +139,15 @@ protected override async Task WriteToStoreAsync( if (!connectionProvider.IsSharedConnection) connection.Dispose(); else if (!connectionProvider.HasOpenTransaction) - await connection.CloseAsync(); + connection.Close(); } } } - protected override T ReadFromStore(Func commandFunc, - Func resultFunc) + protected override T ReadFromStore( + Func commandFunc, + Func resultFunc + ) { var connection = _connectionProvider.GetConnection(); @@ -168,8 +169,11 @@ protected override T ReadFromStore(Func comm } } - 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); @@ -191,11 +195,11 @@ protected override async Task ReadFromStoreAsync(Func MapFunctionAsync(NpgsqlDataReader dr, - CancellationToken cancellationToken) + protected override async Task MapFunctionAsync(DbDataReader dr, CancellationToken cancellationToken) { if (await dr.ReadAsync(cancellationToken)) { @@ -315,7 +318,7 @@ protected override async Task MapFunctionAsync(NpgsqlDataReader dr, return new Message(); } - protected override IEnumerable MapListFunction(NpgsqlDataReader dr) + protected override IEnumerable MapListFunction(DbDataReader dr) { var messages = new List(); while (dr.Read()) @@ -329,8 +332,9 @@ protected override IEnumerable MapListFunction(NpgsqlDataReader dr) } protected override async Task> MapListFunctionAsync( - NpgsqlDataReader dr, - CancellationToken cancellationToken) + DbDataReader dr, + CancellationToken cancellationToken + ) { var messages = new List(); while (await dr.ReadAsync(cancellationToken)) @@ -338,12 +342,11 @@ protected override async Task> MapListFunctionAsync( messages.Add(MapAMessage(dr)); } - await dr.CloseAsync(); + dr.Close(); return messages; } - - public Message MapAMessage(IDataReader dr) + public Message MapAMessage(DbDataReader dr) { var id = GetMessageId(dr); var messageType = GetMessageType(dr); @@ -383,7 +386,7 @@ public Message MapAMessage(IDataReader dr) return new Message(header, body); } - private string GetContentType(IDataReader dr) + private string GetContentType(DbDataReader dr) { var ordinal = dr.GetOrdinal("ContentType"); if (dr.IsDBNull(ordinal)) @@ -393,7 +396,7 @@ private string GetContentType(IDataReader dr) return replyTo; } - private static Dictionary GetContextBag(IDataReader dr) + private static Dictionary GetContextBag(DbDataReader dr) { var i = dr.GetOrdinal("HeaderBag"); var headerBag = dr.IsDBNull(i) ? "" : dr.GetString(i); @@ -402,7 +405,7 @@ private static Dictionary GetContextBag(IDataReader dr) return dictionaryBag; } - private Guid? GetCorrelationId(IDataReader dr) + private Guid? GetCorrelationId(DbDataReader dr) { var ordinal = dr.GetOrdinal("CorrelationId"); if (dr.IsDBNull(ordinal)) @@ -412,22 +415,22 @@ private static Dictionary GetContextBag(IDataReader dr) return correlationId; } - private static string GetTopic(IDataReader dr) + private static string GetTopic(DbDataReader dr) { return dr.GetString(dr.GetOrdinal("Topic")); } - private static MessageType GetMessageType(IDataReader dr) + private static MessageType GetMessageType(DbDataReader dr) { return (MessageType)Enum.Parse(typeof(MessageType), dr.GetString(dr.GetOrdinal("MessageType"))); } - private static Guid GetMessageId(IDataReader dr) + private static Guid GetMessageId(DbDataReader dr) { return dr.GetGuid(dr.GetOrdinal("MessageId")); } - private string GetPartitionKey(IDataReader dr) + private string GetPartitionKey(DbDataReader dr) { var ordinal = dr.GetOrdinal("PartitionKey"); if (dr.IsDBNull(ordinal)) return null; @@ -436,7 +439,7 @@ private string GetPartitionKey(IDataReader dr) return partitionKey; } - private string GetReplyTo(IDataReader dr) + private string GetReplyTo(DbDataReader dr) { var ordinal = dr.GetOrdinal("ReplyTo"); if (dr.IsDBNull(ordinal)) @@ -446,7 +449,7 @@ private string GetReplyTo(IDataReader dr) return replyTo; } - private static DateTime GetTimeStamp(IDataReader dr) + private static DateTime GetTimeStamp(DbDataReader dr) { var ordinal = dr.GetOrdinal("Timestamp"); var timeStamp = dr.IsDBNull(ordinal) diff --git a/src/Paramore.Brighter.Outbox.PostgreSql/ServiceCollectionExtensions.cs b/src/Paramore.Brighter.Outbox.PostgreSql/ServiceCollectionExtensions.cs index 7916f08000..469895721a 100644 --- a/src/Paramore.Brighter.Outbox.PostgreSql/ServiceCollectionExtensions.cs +++ b/src/Paramore.Brighter.Outbox.PostgreSql/ServiceCollectionExtensions.cs @@ -20,10 +20,10 @@ public static IBrighterBuilder UsePostgreSqlOutbox( 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)}."); + if (!typeof(IAmARelationalDbConnectionProvider).IsAssignableFrom(connectionProvider)) + throw new Exception($"Unable to register provider of type {connectionProvider.GetType().Name}. Class does not implement interface {nameof(IAmARelationalDbConnectionProvider)}."); - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IPostgreSqlConnectionProvider), connectionProvider, serviceLifetime)); + brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmARelationalDbConnectionProvider), connectionProvider, serviceLifetime)); } brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmAnOutboxSync), BuildPostgreSqlOutboxSync, serviceLifetime)); @@ -51,10 +51,10 @@ public static IBrighterBuilder UsePostgreSqlTransactionConnectionProvider( 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)}."); + if (!typeof(IAmATransactionConnectonProvider).IsAssignableFrom(connectionProvider)) + throw new Exception($"Unable to register provider of type {connectionProvider.GetType().Name}. Class does not implement interface {nameof(IAmATransactionConnectonProvider)}."); - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmABoxTransactionConnectionProvider), connectionProvider, serviceLifetime)); + brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmABoxTransactionProvider), connectionProvider, serviceLifetime)); return brighterBuilder; } @@ -62,7 +62,7 @@ public static IBrighterBuilder UsePostgreSqlTransactionConnectionProvider( private static PostgreSqlOutbox BuildPostgreSqlOutboxSync(IServiceProvider provider) { var config = provider.GetService(); - var connectionProvider = provider.GetService(); + var connectionProvider = provider.GetService(); return new PostgreSqlOutbox(config, connectionProvider); } diff --git a/src/Paramore.Brighter.Outbox.Sqlite/ServiceCollectionExtensions.cs b/src/Paramore.Brighter.Outbox.Sqlite/ServiceCollectionExtensions.cs index b4a7e4a173..1380d6d3df 100644 --- a/src/Paramore.Brighter.Outbox.Sqlite/ServiceCollectionExtensions.cs +++ b/src/Paramore.Brighter.Outbox.Sqlite/ServiceCollectionExtensions.cs @@ -26,7 +26,7 @@ public static IBrighterBuilder UseSqliteOutbox( this IBrighterBuilder brighterBuilder, RelationalDatabaseConfiguration 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(IAmARelationalDbConnectionProvider), connectionProvider, serviceLifetime)); brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmAnOutboxSync), BuildSqliteOutbox, serviceLifetime)); brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmAnOutboxAsync), BuildSqliteOutbox, serviceLifetime)); @@ -48,7 +48,7 @@ public static IBrighterBuilder UseSqliteTransactionConnectionProvider( this IBrighterBuilder brighterBuilder, Type connectionProvider, ServiceLifetime serviceLifetime = ServiceLifetime.Scoped) { - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmABoxTransactionConnectionProvider), connectionProvider, serviceLifetime)); + brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmABoxTransactionProvider), connectionProvider, serviceLifetime)); return brighterBuilder; } @@ -56,7 +56,7 @@ public static IBrighterBuilder UseSqliteTransactionConnectionProvider( private static SqliteOutbox BuildSqliteOutbox(IServiceProvider provider) { var config = provider.GetService(); - var connectionProvider = provider.GetService(); + var connectionProvider = provider.GetService(); return new SqliteOutbox(config, connectionProvider); } diff --git a/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutbox.cs b/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutbox.cs index ebf9c9249a..1e4e7a47ba 100644 --- a/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutbox.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,23 +40,22 @@ namespace Paramore.Brighter.Outbox.Sqlite /// /// Implements an outbox using Sqlite as a backing store /// - public class SqliteOutbox : RelationDatabaseOutbox + public class SqliteOutbox : RelationDatabaseOutbox { private static readonly ILogger s_logger = ApplicationLogging.CreateLogger(); private const int SqliteDuplicateKeyError = 1555; private const int SqliteUniqueKeyError = 19; private readonly RelationalDatabaseConfiguration _configuration; - private readonly ISqliteConnectionProvider _connectionProvider; + 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 SqliteOutbox(RelationalDatabaseConfiguration configuration, ISqliteConnectionProvider connectionProvider) : base( - configuration.OutBoxTableName, new SqliteQueries(), ApplicationLogging.CreateLogger()) + public SqliteOutbox(RelationalDatabaseConfiguration configuration, IAmARelationalDbConnectionProvider connectionProvider) + : base(configuration.OutBoxTableName, new SqliteQueries(), ApplicationLogging.CreateLogger()) { _configuration = configuration; ContinueOnCapturedContext = false; @@ -66,19 +66,20 @@ public SqliteOutbox(RelationalDatabaseConfiguration configuration, ISqliteConnec /// Initializes a new instance of the class. /// /// The configuration to connect to this data store - public SqliteOutbox(RelationalDatabaseConfiguration configuration) : this(configuration, - new SqliteConnectionProvider(configuration)) + public SqliteOutbox(RelationalDatabaseConfiguration configuration) + : this(configuration, new SqliteConnectionProvider(configuration)) { } - protected override void WriteToStore(IAmABoxTransactionConnectionProvider transactionConnectionProvider, - Func commandFunc, - Action loggingAction) + protected override void WriteToStore( + IAmATransactionConnectonProvider transactionProvider, + Func commandFunc, + Action loggingAction + ) { var connectionProvider = _connectionProvider; - if (transactionConnectionProvider != null && - transactionConnectionProvider is ISqliteTransactionConnectionProvider provider) - connectionProvider = provider; + if (transactionProvider != null) + connectionProvider = transactionProvider; var connection = connectionProvider.GetConnection(); @@ -88,7 +89,7 @@ protected override void WriteToStore(IAmABoxTransactionConnectionProvider transa { try { - if (transactionConnectionProvider != null && connectionProvider.HasOpenTransaction) + if (transactionProvider != null && connectionProvider.HasOpenTransaction) command.Transaction = connectionProvider.GetTransaction(); command.ExecuteNonQuery(); } @@ -113,14 +114,14 @@ protected override void WriteToStore(IAmABoxTransactionConnectionProvider transa } protected override async Task WriteToStoreAsync( - IAmABoxTransactionConnectionProvider transactionConnectionProvider, - Func commandFunc, - Action loggingAction, CancellationToken cancellationToken) + IAmATransactionConnectonProvider transactionConnectionProvider, + Func commandFunc, + Action loggingAction, + CancellationToken cancellationToken) { var connectionProvider = _connectionProvider; - if (transactionConnectionProvider != null && - transactionConnectionProvider is ISqliteTransactionConnectionProvider provider) - connectionProvider = provider; + if (transactionConnectionProvider != null) + connectionProvider = transactionConnectionProvider; var connection = await connectionProvider.GetConnectionAsync(cancellationToken); @@ -154,8 +155,10 @@ protected override async Task WriteToStoreAsync( } } - protected override T ReadFromStore(Func commandFunc, - Func resultFunc) + protected override T ReadFromStore( + Func commandFunc, + Func resultFunc + ) { var connection = _connectionProvider.GetConnection(); @@ -177,8 +180,9 @@ protected override T ReadFromStore(Func comm } } - 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); @@ -200,8 +204,11 @@ protected override async Task ReadFromStoreAsync(Func MapFunctionAsync(SqliteDataReader dr, - CancellationToken cancellationToken) + protected override async Task MapFunctionAsync(DbDataReader dr, CancellationToken cancellationToken) { using (dr) { @@ -323,7 +331,7 @@ protected override async Task MapFunctionAsync(SqliteDataReader dr, } } - protected override IEnumerable MapListFunction(SqliteDataReader dr) + protected override IEnumerable MapListFunction(DbDataReader dr) { var messages = new List(); while (dr.Read()) @@ -336,8 +344,7 @@ protected override IEnumerable MapListFunction(SqliteDataReader dr) 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)) @@ -404,7 +411,7 @@ private Message MapAMessage(IDataReader dr) } - private byte[] GetBodyAsBytes(SqliteDataReader dr) + private byte[] GetBodyAsBytes(DbDataReader dr) { var i = dr.GetOrdinal("Body"); var body = dr.GetStream(i); diff --git a/src/Paramore.Brighter.PostgreSql.EntityFrameworkCore/PostgreSqlEntityFrameworkConnectionProvider.cs b/src/Paramore.Brighter.PostgreSql.EntityFrameworkCore/PostgreSqlEntityFrameworkConnectonProvider.cs similarity index 65% rename from src/Paramore.Brighter.PostgreSql.EntityFrameworkCore/PostgreSqlEntityFrameworkConnectionProvider.cs rename to src/Paramore.Brighter.PostgreSql.EntityFrameworkCore/PostgreSqlEntityFrameworkConnectonProvider.cs index 9f14c6d1e8..366580518f 100644 --- a/src/Paramore.Brighter.PostgreSql.EntityFrameworkCore/PostgreSqlEntityFrameworkConnectionProvider.cs +++ b/src/Paramore.Brighter.PostgreSql.EntityFrameworkCore/PostgreSqlEntityFrameworkConnectonProvider.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 PostgreSqlEntityFrameworkConnectonProvider : IAmATransactionConnectonProvider where T : DbContext { private readonly T _context; @@ -18,7 +20,7 @@ public class PostgreSqlEntityFrameworkConnectionProvider : IPostgreSqlTransac /// Constructs and instance from a database context /// /// The database context to use - public PostgreSqlEntityFrameworkConnectionProvider(T context) + public PostgreSqlEntityFrameworkConnectonProvider(T context) { _context = context; } @@ -27,9 +29,9 @@ public PostgreSqlEntityFrameworkConnectionProvider(T context) /// Get the current connection of the database context /// /// The NpgsqlConnection that is in use - public NpgsqlConnection GetConnection() + public DbConnection GetConnection() { - return (NpgsqlConnection)_context.Database.GetDbConnection(); + return _context.Database.GetDbConnection(); } /// @@ -37,10 +39,10 @@ public NpgsqlConnection GetConnection() /// /// A cancellation token /// - public Task GetConnectionAsync(CancellationToken cancellationToken = default) + public Task GetConnectionAsync(CancellationToken cancellationToken = default) { - var tcs = new TaskCompletionSource(); - tcs.SetResult((NpgsqlConnection)_context.Database.GetDbConnection()); + var tcs = new TaskCompletionSource(); + tcs.SetResult((DbConnection)_context.Database.GetDbConnection()); return tcs.Task; } @@ -48,9 +50,9 @@ public Task GetConnectionAsync(CancellationToken cancellationT /// Get the ambient Transaction /// /// The NpgsqlTransaction - public NpgsqlTransaction GetTransaction() + public DbTransaction GetTransaction() { - return (NpgsqlTransaction)_context.Database.CurrentTransaction?.GetDbTransaction(); + return _context.Database.CurrentTransaction?.GetDbTransaction(); } public bool HasOpenTransaction { get => _context.Database.CurrentTransaction != null; } 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/PostgreSqlNpgsqlConnectionProvider.cs b/src/Paramore.Brighter.PostgreSql/PostgreSqlNpgsqlConnectionProvider.cs index 04294d051f..31d2a3c4b0 100644 --- a/src/Paramore.Brighter.PostgreSql/PostgreSqlNpgsqlConnectionProvider.cs +++ b/src/Paramore.Brighter.PostgreSql/PostgreSqlNpgsqlConnectionProvider.cs @@ -1,11 +1,10 @@ using System; -using System.Threading; -using System.Threading.Tasks; +using System.Data.Common; using Npgsql; namespace Paramore.Brighter.PostgreSql { - public class PostgreSqlNpgsqlConnectionProvider : IPostgreSqlConnectionProvider + public class PostgreSqlNpgsqlConnectionProvider : RelationalDbConnectionProvider { private readonly string _connectionString; @@ -17,26 +16,10 @@ public PostgreSqlNpgsqlConnectionProvider(RelationalDatabaseConfiguration config _connectionString = configuration.ConnectionString; } - public NpgsqlConnection GetConnection() + public override DbConnection 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.ServiceActivator/ControlBusReceiverBuilder.cs b/src/Paramore.Brighter.ServiceActivator/ControlBusReceiverBuilder.cs index d3894e618f..2016334183 100644 --- a/src/Paramore.Brighter.ServiceActivator/ControlBusReceiverBuilder.cs +++ b/src/Paramore.Brighter.ServiceActivator/ControlBusReceiverBuilder.cs @@ -185,7 +185,7 @@ public Dispatcher Build(string hostName) /// private class SinkOutboxSync : IAmAnOutboxSync { - public void Add(Message message, int outBoxTimeout = -1, IAmABoxTransactionConnectionProvider transactionConnectionProvider = null) + public void Add(Message message, int outBoxTimeout = -1, IAmATransactionConnectonProvider transactionProvider = null) { //discard message } diff --git a/src/Paramore.Brighter.Sqlite.Dapper/SqliteDapperConnectionProvider.cs b/src/Paramore.Brighter.Sqlite.Dapper/SqliteDapperConnectionProvider.cs index bf4be813aa..a6447e013d 100644 --- a/src/Paramore.Brighter.Sqlite.Dapper/SqliteDapperConnectionProvider.cs +++ b/src/Paramore.Brighter.Sqlite.Dapper/SqliteDapperConnectionProvider.cs @@ -1,11 +1,14 @@ -using System.Threading; +using System.Data; +using System.Data.Common; +using System.Threading; using System.Threading.Tasks; using Microsoft.Data.Sqlite; using Paramore.Brighter.Dapper; +using Paramore.Brighter.PostgreSql; namespace Paramore.Brighter.Sqlite.Dapper { - public class SqliteDapperConnectionProvider : ISqliteTransactionConnectionProvider + public class SqliteDapperConnectionProvider : RelationalDbConnectionProvider { private readonly IUnitOfWork _unitOfWork; @@ -14,21 +17,14 @@ public SqliteDapperConnectionProvider(IUnitOfWork unitOfWork) _unitOfWork = unitOfWork; } - public SqliteConnection GetConnection() + public override DbConnection GetConnection() { return (SqliteConnection)_unitOfWork.Database; } - public Task GetConnectionAsync(CancellationToken cancellationToken = default) + public IDbTransaction GetTransaction() { - var tcs = new TaskCompletionSource(); - tcs.SetResult(GetConnection()); - return tcs.Task; - } - - public SqliteTransaction GetTransaction() - { - return (SqliteTransaction)_unitOfWork.BeginOrGetTransaction(); + return (DbTransaction)_unitOfWork.BeginOrGetTransaction(); } public bool HasOpenTransaction diff --git a/src/Paramore.Brighter.Sqlite.EntityFrameworkCore/SqliteEntityFrameworkConnectionProvider.cs b/src/Paramore.Brighter.Sqlite.EntityFrameworkCore/SqliteEntityFrameworkConnectionProvider.cs index c024d4839a..3e5103bc62 100644 --- a/src/Paramore.Brighter.Sqlite.EntityFrameworkCore/SqliteEntityFrameworkConnectionProvider.cs +++ b/src/Paramore.Brighter.Sqlite.EntityFrameworkCore/SqliteEntityFrameworkConnectionProvider.cs @@ -1,8 +1,10 @@ -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; +using Paramore.Brighter.PostgreSql; namespace Paramore.Brighter.Sqlite.EntityFrameworkCore { @@ -10,7 +12,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 : RelationalDbConnectionProvider where T: DbContext { private readonly T _context; @@ -27,11 +29,11 @@ public SqliteEntityFrameworkConnectionProvider(T context) /// 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 +41,23 @@ 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; } + public override bool HasOpenTransaction { get => _context.Database.CurrentTransaction != null; } + 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/SqliteConnectionProvider.cs b/src/Paramore.Brighter.Sqlite/SqliteConnectionProvider.cs index 53fea4c994..48cf3c01b2 100644 --- a/src/Paramore.Brighter.Sqlite/SqliteConnectionProvider.cs +++ b/src/Paramore.Brighter.Sqlite/SqliteConnectionProvider.cs @@ -22,16 +22,19 @@ THE SOFTWARE. */ #endregion +using System.Data; +using System.Data.Common; using System.Threading; using System.Threading.Tasks; using Microsoft.Data.Sqlite; +using Paramore.Brighter.PostgreSql; namespace Paramore.Brighter.Sqlite { /// /// A connection provider that uses the connection string to create a connection /// - public class SqliteConnectionProvider : ISqliteConnectionProvider + public class SqliteConnectionProvider : RelationalDbConnectionProvider { private readonly string _connectionString; @@ -44,26 +47,9 @@ public SqliteConnectionProvider(RelationalDatabaseConfiguration configuration) _connectionString = configuration.ConnectionString; } - public SqliteConnection GetConnection() + public override DbConnection GetConnection() { return new SqliteConnection(_connectionString); } - - public async Task GetConnectionAsync(CancellationToken cancellationToken = default) - { - var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - - tcs.SetResult(GetConnection()); - return await tcs.Task; - } - - public SqliteTransaction 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/CommandProcessor.cs b/src/Paramore.Brighter/CommandProcessor.cs index 38be239e67..a242748537 100644 --- a/src/Paramore.Brighter/CommandProcessor.cs +++ b/src/Paramore.Brighter/CommandProcessor.cs @@ -55,7 +55,7 @@ public class CommandProcessor : IAmACommandProcessor private readonly IAmARequestContextFactory _requestContextFactory; private readonly IPolicyRegistry _policyRegistry; private readonly InboxConfiguration _inboxConfiguration; - private readonly IAmABoxTransactionConnectionProvider _boxTransactionConnectionProvider; + private readonly IAmATransactionConnectonProvider _transactionConnectonProvider; private readonly IAmAFeatureSwitchRegistry _featureSwitchRegistry; private readonly IEnumerable _replySubscriptions; private readonly TransformPipelineBuilder _transformPipelineBuilder; @@ -149,7 +149,7 @@ public CommandProcessor( /// 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 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, @@ -160,7 +160,7 @@ public CommandProcessor(IAmARequestContextFactory requestContextFactory, int outboxTimeout = 300, IAmAFeatureSwitchRegistry featureSwitchRegistry = null, InboxConfiguration inboxConfiguration = null, - IAmABoxTransactionConnectionProvider boxTransactionConnectionProvider = null, + IAmATransactionConnectonProvider transactionConnectonProvider = null, int outboxBulkChunkSize = 100, IAmAMessageTransformerFactory messageTransformerFactory = null) { @@ -168,7 +168,7 @@ public CommandProcessor(IAmARequestContextFactory requestContextFactory, _policyRegistry = policyRegistry; _featureSwitchRegistry = featureSwitchRegistry; _inboxConfiguration = inboxConfiguration; - _boxTransactionConnectionProvider = boxTransactionConnectionProvider; + _transactionConnectonProvider = transactionConnectonProvider; _transformPipelineBuilder = new TransformPipelineBuilder(mapperRegistry, messageTransformerFactory); InitExtServiceBus(policyRegistry, outBox, outboxTimeout, producerRegistry, outboxBulkChunkSize); @@ -192,7 +192,7 @@ public CommandProcessor(IAmARequestContextFactory requestContextFactory, /// 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 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, @@ -207,7 +207,7 @@ public CommandProcessor(IAmASubscriberRegistry subscriberRegistry, IAmAFeatureSwitchRegistry featureSwitchRegistry = null, IAmAChannelFactory responseChannelFactory = null, InboxConfiguration inboxConfiguration = null, - IAmABoxTransactionConnectionProvider boxTransactionConnectionProvider = null, + IAmATransactionConnectonProvider transactionConnectonProvider = null, int outboxBulkChunkSize = 100, IAmAMessageTransformerFactory messageTransformerFactory = null) : this(subscriberRegistry, handlerFactory, requestContextFactory, policyRegistry) @@ -215,7 +215,7 @@ public CommandProcessor(IAmASubscriberRegistry subscriberRegistry, _featureSwitchRegistry = featureSwitchRegistry; _responseChannelFactory = responseChannelFactory; _inboxConfiguration = inboxConfiguration; - _boxTransactionConnectionProvider = boxTransactionConnectionProvider; + _transactionConnectonProvider = transactionConnectonProvider; _replySubscriptions = replySubscriptions; _transformPipelineBuilder = new TransformPipelineBuilder(mapperRegistry, messageTransformerFactory); @@ -238,7 +238,7 @@ public CommandProcessor(IAmASubscriberRegistry subscriberRegistry, /// 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 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, @@ -251,13 +251,13 @@ public CommandProcessor(IAmASubscriberRegistry subscriberRegistry, int outboxTimeout = 300, IAmAFeatureSwitchRegistry featureSwitchRegistry = null, InboxConfiguration inboxConfiguration = null, - IAmABoxTransactionConnectionProvider boxTransactionConnectionProvider = null, + IAmATransactionConnectonProvider transactionConnectonProvider = null, int outboxBulkChunkSize = 100, IAmAMessageTransformerFactory messageTransformerFactory = null) : this(subscriberRegistry, handlerFactory, requestContextFactory, policyRegistry, featureSwitchRegistry) { _inboxConfiguration = inboxConfiguration; - _boxTransactionConnectionProvider = boxTransactionConnectionProvider; + _transactionConnectonProvider = transactionConnectonProvider; _transformPipelineBuilder = new TransformPipelineBuilder(mapperRegistry, messageTransformerFactory); InitExtServiceBus(policyRegistry, outBox, outboxTimeout, producerRegistry, outboxBulkChunkSize); @@ -537,7 +537,7 @@ public async Task PostAsync(T request, bool continueOnCapturedContext = false /// The Id of the Message that has been deposited. public Guid DepositPost(T request) where T : class, IRequest { - return DepositPost(request, _boxTransactionConnectionProvider); + return DepositPost(request, _transactionConnectonProvider); } /// @@ -552,10 +552,10 @@ public Guid DepositPost(T request) where T : class, IRequest /// The Id of the Message that has been deposited. public Guid[] DepositPost(IEnumerable requests) where T : class, IRequest { - return DepositPost(requests, _boxTransactionConnectionProvider); + return DepositPost(requests, _transactionConnectonProvider); } - private Guid DepositPost(T request, IAmABoxTransactionConnectionProvider connectionProvider) + private Guid DepositPost(T request, IAmATransactionConnectonProvider provider) where T : class, IRequest { s_logger.LogInformation("Save request: {RequestType} {Id}", request.GetType(), request.Id); @@ -567,12 +567,12 @@ private Guid DepositPost(T request, IAmABoxTransactionConnectionProvider conn AddTelemetryToMessage(message); - _bus.AddToOutbox(request, message, connectionProvider); + _bus.AddToOutbox(request, message, provider); return message.Id; } - private Guid[] DepositPost(IEnumerable requests, IAmABoxTransactionConnectionProvider connectionProvider) + private Guid[] DepositPost(IEnumerable requests, IAmATransactionConnectonProvider provider) where T : class, IRequest { if (!_bus.HasBulkOutbox()) @@ -586,7 +586,7 @@ private Guid[] DepositPost(IEnumerable requests, IAmABoxTransactionConnect s_logger.LogInformation("Save requests: {RequestType} {AmountOfMessages}", batch.Key, messages.Count()); - _bus.AddToOutbox(messages, connectionProvider); + _bus.AddToOutbox(messages, provider); successfullySentMessage.AddRange(messages.Select(m => m.Id)); } @@ -609,7 +609,7 @@ private Guid[] DepositPost(IEnumerable requests, IAmABoxTransactionConnect public async Task DepositPostAsync(T request, bool continueOnCapturedContext = false, CancellationToken cancellationToken = default) where T : class, IRequest { - return await DepositPostAsync(request, _boxTransactionConnectionProvider, continueOnCapturedContext, + return await DepositPostAsync(request, _transactionConnectonProvider, continueOnCapturedContext, cancellationToken); } @@ -625,14 +625,18 @@ public async Task DepositPostAsync(T request, bool continueOnCapturedCo /// The Cancellation Token. /// The type of the request /// - public Task DepositPostAsync(IEnumerable requests, bool continueOnCapturedContext = false, - CancellationToken cancellationToken = default) where T : class, IRequest + public Task DepositPostAsync( + IEnumerable requests, + bool continueOnCapturedContext = false, + CancellationToken cancellationToken = default + ) where T : class, IRequest { - return DepositPostAsync(requests, _boxTransactionConnectionProvider, continueOnCapturedContext, - cancellationToken); + return DepositPostAsync(requests, _transactionConnectonProvider, continueOnCapturedContext, cancellationToken); } - private async Task DepositPostAsync(T request, IAmABoxTransactionConnectionProvider connectionProvider, + private async Task DepositPostAsync( + T request, + IAmATransactionConnectonProvider provider, bool continueOnCapturedContext = false, CancellationToken cancellationToken = default) where T : class, IRequest { @@ -646,7 +650,7 @@ private async Task DepositPostAsync(T request, IAmABoxTransactionConnec AddTelemetryToMessage(message); await _bus.AddToOutboxAsync(request, continueOnCapturedContext, cancellationToken, message, - connectionProvider); + provider); return message.Id; } @@ -875,8 +879,10 @@ private static void InitExtServiceBus( } private async Task DepositPostAsync(IEnumerable requests, - IAmABoxTransactionConnectionProvider connectionProvider, bool continueOnCapturedContext = false, - CancellationToken cancellationToken = default) where T : class, IRequest + IAmATransactionConnectonProvider provider, + bool continueOnCapturedContext = false, + CancellationToken cancellationToken = default + ) where T : class, IRequest { if (!_bus.HasAsyncBulkOutbox()) throw new InvalidOperationException("No bulk async outbox defined."); @@ -889,7 +895,7 @@ private async Task DepositPostAsync(IEnumerable requests, s_logger.LogInformation("Save requests: {RequestType} {AmountOfMessages}", batch.Key, messages.Count()); - await _bus.AddToOutboxAsync(messages, continueOnCapturedContext, cancellationToken, connectionProvider); + await _bus.AddToOutboxAsync(messages, continueOnCapturedContext, cancellationToken, provider); successfullySentMessage.AddRange(messages.Select(m => m.Id)); } diff --git a/src/Paramore.Brighter/CommandProcessorBuilder.cs b/src/Paramore.Brighter/CommandProcessorBuilder.cs index 228fcdba20..dbd4cf8876 100644 --- a/src/Paramore.Brighter/CommandProcessorBuilder.cs +++ b/src/Paramore.Brighter/CommandProcessorBuilder.cs @@ -84,7 +84,7 @@ public class CommandProcessorBuilder : INeedAHandlers, INeedPolicy, INeedMessagi private bool _useExternalBus = false; private bool _useRequestReplyQueues = false; private IEnumerable _replySubscriptions; - private IAmABoxTransactionConnectionProvider _overridingBoxTransactionConnectionProvider = null; + private IAmATransactionConnectonProvider _overridingBoxTransactionProvider = null; private int _outboxBulkChunkSize; private CommandProcessorBuilder() @@ -162,14 +162,14 @@ public INeedMessaging DefaultPolicy() /// /// The Task Queues configuration. /// The Outbox. - /// + /// /// INeedARequestContext. - public INeedARequestContext ExternalBus(ExternalBusConfiguration configuration, IAmAnOutbox outbox, IAmABoxTransactionConnectionProvider boxTransactionConnectionProvider = null) + public INeedARequestContext ExternalBus(ExternalBusConfiguration configuration, IAmAnOutbox outbox, IAmATransactionConnectonProvider transactionConnectonProvider = null) { _useExternalBus = true; _producers = configuration.ProducerRegistry; _outbox = outbox; - _overridingBoxTransactionConnectionProvider = boxTransactionConnectionProvider; + _overridingBoxTransactionProvider = transactionConnectonProvider; _messageMapperRegistry = configuration.MessageMapperRegistry; _outboxWriteTimeout = configuration.OutboxWriteTimeout; _outboxBulkChunkSize = configuration.OutboxBulkChunkSize; @@ -247,7 +247,7 @@ public CommandProcessor Build() producerRegistry: _producers, outboxTimeout: _outboxWriteTimeout, featureSwitchRegistry: _featureSwitchRegistry, - boxTransactionConnectionProvider: _overridingBoxTransactionConnectionProvider, + transactionConnectonProvider: _overridingBoxTransactionProvider, outboxBulkChunkSize: _outboxBulkChunkSize, messageTransformerFactory: _transformerFactory ); @@ -263,7 +263,7 @@ public CommandProcessor Build() outBox: _outbox, producerRegistry: _producers, replySubscriptions: _replySubscriptions, - responseChannelFactory: _responseChannelFactory, boxTransactionConnectionProvider: _overridingBoxTransactionConnectionProvider); + responseChannelFactory: _responseChannelFactory, transactionConnectonProvider: _overridingBoxTransactionProvider); } else { @@ -323,9 +323,9 @@ public interface INeedMessaging /// /// The configuration. /// The outbox. - /// The connection provider to use when adding messages to the bus + /// The connection provider to use when adding messages to the bus /// INeedARequestContext. - INeedARequestContext ExternalBus(ExternalBusConfiguration configuration, IAmAnOutbox outbox, IAmABoxTransactionConnectionProvider boxTransactionConnectionProvider = null); + INeedARequestContext ExternalBus(ExternalBusConfiguration configuration, IAmAnOutbox outbox, IAmATransactionConnectonProvider transactionConnectonProvider = null); /// /// We don't send messages out of process /// diff --git a/src/Paramore.Brighter/ExternalBusServices.cs b/src/Paramore.Brighter/ExternalBusServices.cs index d6e7325edd..f3ef9866c1 100644 --- a/src/Paramore.Brighter/ExternalBusServices.cs +++ b/src/Paramore.Brighter/ExternalBusServices.cs @@ -65,12 +65,12 @@ protected virtual void Dispose(bool disposing) } - internal async Task AddToOutboxAsync(T request, bool continueOnCapturedContext, CancellationToken cancellationToken, Message message, IAmABoxTransactionConnectionProvider overridingTransactionConnectionProvider = null) + internal async Task AddToOutboxAsync(T request, bool continueOnCapturedContext, CancellationToken cancellationToken, Message message, IAmATransactionConnectonProvider overridingAmATransactionProvider = null) where T : class, IRequest { CheckOutboxOutstandingLimit(); - var written = await RetryAsync(async ct => { await AsyncOutbox.AddAsync(message, OutboxTimeout, ct, overridingTransactionConnectionProvider).ConfigureAwait(continueOnCapturedContext); }, + var written = await RetryAsync(async ct => { await AsyncOutbox.AddAsync(message, OutboxTimeout, ct, overridingAmATransactionProvider).ConfigureAwait(continueOnCapturedContext); }, continueOnCapturedContext, cancellationToken).ConfigureAwait(continueOnCapturedContext); if (!written) @@ -79,7 +79,7 @@ internal async Task AddToOutboxAsync(T request, bool continueOnCapturedContex tags: new ActivityTagsCollection {{"MessageId", message.Id}})); } - internal async Task AddToOutboxAsync(IEnumerable messages, bool continueOnCapturedContext, CancellationToken cancellationToken, IAmABoxTransactionConnectionProvider overridingTransactionConnectionProvider = null) + internal async Task AddToOutboxAsync(IEnumerable messages, bool continueOnCapturedContext, CancellationToken cancellationToken, IAmATransactionConnectonProvider overridingAmATransactionProvider = null) { CheckOutboxOutstandingLimit(); @@ -92,7 +92,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, overridingAmATransactionProvider) .ConfigureAwait(continueOnCapturedContext); }, continueOnCapturedContext, cancellationToken).ConfigureAwait(continueOnCapturedContext); @@ -107,11 +107,11 @@ await box.AddAsync(chunk, OutboxTimeout, ct, overridingTransactionConnectionProv } } - internal void AddToOutbox(T request, Message message, IAmABoxTransactionConnectionProvider overridingTransactionConnectionProvider = null) where T : class, IRequest + internal void AddToOutbox(T request, Message message, IAmATransactionConnectonProvider overridingAmATransactionProvider = null) where T : class, IRequest { CheckOutboxOutstandingLimit(); - var written = Retry(() => { OutBox.Add(message, OutboxTimeout, overridingTransactionConnectionProvider); }); + var written = Retry(() => { OutBox.Add(message, OutboxTimeout, overridingAmATransactionProvider); }); if (!written) throw new ChannelFailureException($"Could not write request {request.Id} to the outbox"); @@ -119,7 +119,7 @@ internal void AddToOutbox(T request, Message message, IAmABoxTransactionConne tags: new ActivityTagsCollection {{"MessageId", message.Id}})); } - internal void AddToOutbox(IEnumerable messages, IAmABoxTransactionConnectionProvider overridingTransactionConnectionProvider = null) + internal void AddToOutbox(IEnumerable messages, IAmATransactionConnectonProvider overridingAmATransactionProvider = null) { CheckOutboxOutstandingLimit(); @@ -130,7 +130,7 @@ internal void AddToOutbox(IEnumerable messages, IAmABoxTransactionConne foreach (var chunk in ChunkMessages(messages)) { var written = - Retry(() => { box.Add(chunk, OutboxTimeout, overridingTransactionConnectionProvider); }); + Retry(() => { box.Add(chunk, OutboxTimeout, overridingAmATransactionProvider); }); if (!written) throw new ChannelFailureException($"Could not write {chunk.Count()} messages to the outbox"); 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..c0de5a5c95 --- /dev/null +++ b/src/Paramore.Brighter/IAmABoxTransactionProvider.cs @@ -0,0 +1,7 @@ +namespace Paramore.Brighter +{ + public interface IAmABoxTransactionProvider + { + + } +} diff --git a/src/Paramore.Brighter/IAmABulkOutboxAsync.cs b/src/Paramore.Brighter/IAmABulkOutboxAsync.cs index c6dbc4b993..f61ac20576 100644 --- a/src/Paramore.Brighter/IAmABulkOutboxAsync.cs +++ b/src/Paramore.Brighter/IAmABulkOutboxAsync.cs @@ -46,8 +46,8 @@ 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, IAmATransactionConnectonProvider amATransactionProvider = null); } } diff --git a/src/Paramore.Brighter/IAmABulkOutboxSync.cs b/src/Paramore.Brighter/IAmABulkOutboxSync.cs index 3d3c2c2c46..346a888e6e 100644 --- a/src/Paramore.Brighter/IAmABulkOutboxSync.cs +++ b/src/Paramore.Brighter/IAmABulkOutboxSync.cs @@ -44,7 +44,7 @@ public interface IAmABulkOutboxSync : IAmAnOutboxSync where T : 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, IAmATransactionConnectonProvider amATransactionProvider = null); } } diff --git a/src/Paramore.Brighter/IAmARelationalDbConnectionProvider.cs b/src/Paramore.Brighter/IAmARelationalDbConnectionProvider.cs new file mode 100644 index 0000000000..a3cd01f9b2 --- /dev/null +++ b/src/Paramore.Brighter/IAmARelationalDbConnectionProvider.cs @@ -0,0 +1,19 @@ +using System.Data.Common; +using System.Threading; +using System.Threading.Tasks; + +namespace Paramore.Brighter +{ + public interface IAmARelationalDbConnectionProvider + { + DbConnection GetConnection(); + + Task GetConnectionAsync(CancellationToken cancellationToken = default); + + DbTransaction GetTransaction(); + + bool HasOpenTransaction { get; } + + bool IsSharedConnection { get; } + } +} diff --git a/src/Paramore.Brighter/IAmATransactionConnectonProvider.cs b/src/Paramore.Brighter/IAmATransactionConnectonProvider.cs new file mode 100644 index 0000000000..5024f23ffd --- /dev/null +++ b/src/Paramore.Brighter/IAmATransactionConnectonProvider.cs @@ -0,0 +1,4 @@ +namespace Paramore.Brighter +{ + public interface IAmATransactionConnectonProvider : IAmARelationalDbConnectionProvider, IAmABoxTransactionProvider { } +} diff --git a/src/Paramore.Brighter/IAmAnOutboxAsync.cs b/src/Paramore.Brighter/IAmAnOutboxAsync.cs index 84bfe6ec6d..f98f97d16e 100644 --- a/src/Paramore.Brighter/IAmAnOutboxAsync.cs +++ b/src/Paramore.Brighter/IAmAnOutboxAsync.cs @@ -53,9 +53,9 @@ 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, IAmATransactionConnectonProvider amATransactionProvider = null); /// /// Awaitable Get the specified message identifier. diff --git a/src/Paramore.Brighter/IAmAnOutboxSync.cs b/src/Paramore.Brighter/IAmAnOutboxSync.cs index 1407618b84..02d339e4e9 100644 --- a/src/Paramore.Brighter/IAmAnOutboxSync.cs +++ b/src/Paramore.Brighter/IAmAnOutboxSync.cs @@ -41,8 +41,8 @@ public interface IAmAnOutboxSync : IAmAnOutbox where T : 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, IAmATransactionConnectonProvider amATransactionProvider = null); /// /// Gets the specified message identifier. diff --git a/src/Paramore.Brighter/InMemoryOutbox.cs b/src/Paramore.Brighter/InMemoryOutbox.cs index 7057606934..5eb87ad84d 100644 --- a/src/Paramore.Brighter/InMemoryOutbox.cs +++ b/src/Paramore.Brighter/InMemoryOutbox.cs @@ -98,8 +98,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, IAmATransactionConnectonProvider amATransactionProvider = null) { ClearExpiredMessages(); EnforceCapacityLimit(); @@ -119,15 +119,15 @@ 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, IAmATransactionConnectonProvider amATransactionProvider = null) { ClearExpiredMessages(); EnforceCapacityLimit(); foreach (Message message in messages) { - Add(message, outBoxTimeout, transactionConnectionProvider); + Add(message, outBoxTimeout, amATransactionProvider); } } @@ -137,9 +137,14 @@ 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, + IAmATransactionConnectonProvider amATransactionProvider = null + ) { var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); @@ -161,9 +166,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, + IAmATransactionConnectonProvider amATransactionProvider = null + ) { var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); @@ -264,8 +274,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(); @@ -281,7 +294,12 @@ 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) + public Task MarkDispatchedAsync( + Guid id, + DateTime? dispatchedAt = null, + Dictionary args = null, + CancellationToken cancellationToken = default + ) { var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); @@ -292,14 +310,24 @@ 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(); } - 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)); @@ -345,7 +373,11 @@ public void Delete(params Guid[] messageIds) } } - public Task> GetAsync(int pageSize = 100, int pageNumber = 1, Dictionary args = null, CancellationToken cancellationToken = default) + public Task> GetAsync( + int pageSize = 100, + int pageNumber = 1, + Dictionary args = null, + CancellationToken cancellationToken = default) { var tcs = new TaskCompletionSource>(TaskCreationOptions.RunContinuationsAsynchronously); @@ -354,7 +386,12 @@ 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) + public Task> OutstandingMessagesAsync( + double millSecondsSinceSent, + int pageSize = 100, + int pageNumber = 1, + Dictionary args = null, + CancellationToken cancellationToken = default) { var tcs = new TaskCompletionSource>(TaskCreationOptions.RunContinuationsAsynchronously); diff --git a/src/Paramore.Brighter/RelationDatabaseOutbox.cs b/src/Paramore.Brighter/RelationDatabaseOutbox.cs index ff887787d9..4f5b0c71a4 100644 --- a/src/Paramore.Brighter/RelationDatabaseOutbox.cs +++ b/src/Paramore.Brighter/RelationDatabaseOutbox.cs @@ -1,6 +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; @@ -9,13 +10,7 @@ namespace Paramore.Brighter { - public abstract class - RelationDatabaseOutbox : IAmAnOutboxSync, - IAmAnOutboxAsync - where TConnection : IDbConnection, new() - where TCommand : IDbCommand, new() - where TDataReader: IDataReader - where TParameter : IDbDataParameter, new() + public abstract class RelationDatabaseOutbox : IAmAnOutboxSync, IAmAnOutboxAsync { private readonly IRelationDatabaseOutboxQueries _queries; private readonly ILogger _logger; @@ -44,13 +39,15 @@ protected RelationDatabaseOutbox(string outboxTableName, IRelationDatabaseOutbox /// /// 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, + IAmATransactionConnectonProvider amATransactionProvider = null) { var parameters = InitAddDbParameters(message); - WriteToStore(transactionConnectionProvider, connection => InitAddDbCommand(connection, parameters), () => + WriteToStore(amATransactionProvider, connection => InitAddDbCommand(connection, parameters), () => { _logger.LogWarning( "MsSqlOutbox: A duplicate Message with the MessageId {Id} was inserted into the Outbox, ignoring and continuing", @@ -63,12 +60,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, + IAmATransactionConnectonProvider amATransactionProvider = null + ) { - WriteToStore(transactionConnectionProvider, + WriteToStore(amATransactionProvider, connection => InitBulkAddDbCommand(messages.ToList(), connection), () => _logger.LogWarning("MsSqlOutbox: At least one message already exists in the outbox")); } @@ -89,14 +89,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) + IAmATransactionConnectonProvider amATransactionProvider = null + ) { var parameters = InitAddDbParameters(message); - return WriteToStoreAsync(transactionConnectionProvider, + return WriteToStoreAsync(amATransactionProvider, connection => InitAddDbCommand(connection, parameters), () => { _logger.LogWarning( @@ -112,13 +115,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) + IAmATransactionConnectonProvider amATransactionProvider = null + ) { - return WriteToStoreAsync(transactionConnectionProvider, + return WriteToStoreAsync(amATransactionProvider, connection => InitBulkAddDbCommand(messages.ToList(), connection), () => _logger.LogWarning("MsSqlOutbox: At least one message already exists in the outbox"), cancellationToken); @@ -165,7 +171,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), @@ -179,8 +187,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), @@ -332,51 +343,76 @@ public Task DeleteAsync(CancellationToken cancellationToken, params Guid[] messa #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( + IAmATransactionConnectonProvider provider, + Func commandFunc, + Action loggingAction + ); + + protected abstract Task WriteToStoreAsync( + IAmATransactionConnectonProvider 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 + ); #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 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()); @@ -385,11 +421,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, @@ -399,44 +435,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]); @@ -445,10 +481,10 @@ 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++) { diff --git a/src/Paramore.Brighter/RelationalDatabaseConfiguration.cs b/src/Paramore.Brighter/RelationalDatabaseConfiguration.cs index b3f791dbb3..ccb4eb2ae4 100644 --- a/src/Paramore.Brighter/RelationalDatabaseConfiguration.cs +++ b/src/Paramore.Brighter/RelationalDatabaseConfiguration.cs @@ -20,6 +20,7 @@ public RelationalDatabaseConfiguration( ) { OutBoxTableName = outBoxTableName; + InBoxTableName = inboxTableName; ConnectionString = connectionString; QueueStoreTable = queueStoreTable; BinaryMessagePayload = binaryMessagePayload; diff --git a/src/Paramore.Brighter/RelationalDbConnectionProvider.cs b/src/Paramore.Brighter/RelationalDbConnectionProvider.cs new file mode 100644 index 0000000000..a5cd70d871 --- /dev/null +++ b/src/Paramore.Brighter/RelationalDbConnectionProvider.cs @@ -0,0 +1,28 @@ +using System.Data.Common; +using System.Threading; +using System.Threading.Tasks; + +namespace Paramore.Brighter.PostgreSql +{ + public abstract class RelationalDbConnectionProvider : IAmARelationalDbConnectionProvider + { + public abstract DbConnection GetConnection(); + + public virtual async Task GetConnectionAsync(CancellationToken cancellationToken = default) + { + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + tcs.SetResult(GetConnection()); + return await tcs.Task; + } + + public virtual DbTransaction GetTransaction() + { + //This connection factory does not support transactions + return null; + } + + public virtual bool HasOpenTransaction { get => false; } + + public virtual bool IsSharedConnection { get => false; } + } +} diff --git a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/TestDoubles/FakeOutboxSync.cs b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/TestDoubles/FakeOutboxSync.cs index 971c5403d5..cb2522f83b 100644 --- a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/TestDoubles/FakeOutboxSync.cs +++ b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/TestDoubles/FakeOutboxSync.cs @@ -37,12 +37,12 @@ public class FakeOutboxSync : IAmABulkOutboxSync, IAmABulkOutboxAsync messages, int outBoxTimeout = -1, - IAmABoxTransactionConnectionProvider transactionConnectionProvider = null) + IAmATransactionConnectonProvider transactionProvider = null) { foreach (Message message in messages) { - Add(message,outBoxTimeout, transactionConnectionProvider); + Add(message,outBoxTimeout, transactionProvider); } } public async Task AddAsync(IEnumerable messages, int outBoxTimeout = -1, CancellationToken cancellationToken = default, - IAmABoxTransactionConnectionProvider transactionConnectionProvider = null) + IAmATransactionConnectonProvider 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.MSSQL.Tests/MsSqlTestHelper.cs b/tests/Paramore.Brighter.MSSQL.Tests/MsSqlTestHelper.cs index e6d391e60d..11f6171a3e 100644 --- a/tests/Paramore.Brighter.MSSQL.Tests/MsSqlTestHelper.cs +++ b/tests/Paramore.Brighter.MSSQL.Tests/MsSqlTestHelper.cs @@ -12,8 +12,8 @@ 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 _textQueueDDL = @"CREATE TABLE [dbo].[{0}]( [Id][bigint] IDENTITY(1, 1) NOT NULL, @@ -36,6 +36,16 @@ [Payload] [varbinary](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]"; + + public RelationalDatabaseConfiguration InboxConfiguration => + new(_sqlSettings.TestsBrighterConnectionString, inboxTableName: _tableName); + + public RelationalDatabaseConfiguration OutboxConfiguration => + new(_sqlSettings.TestsBrighterConnectionString, outBoxTableName: _tableName, binaryMessagePayload: _binaryMessagePayload); + + public RelationalDatabaseConfiguration QueueConfiguration => + new(_sqlSettings.TestsBrighterConnectionString, queueStoreTable: _tableName); + public MsSqlTestHelper(bool binaryMessagePayload = false) { @@ -89,16 +99,6 @@ public void SetupQueueDb() CreateQueueTable(); } - public RelationalDatabaseConfiguration InboxConfiguration => - new RelationalDatabaseConfiguration(_sqlSettings.TestsBrighterConnectionString, inboxTableName: _tableName); - - public RelationalDatabaseConfiguration OutboxConfiguration => new RelationalDatabaseConfiguration( - _sqlSettings.TestsBrighterConnectionString, outBoxTableName: _tableName, - binaryMessagePayload: _binaryMessagePayload); - - public RelationalDatabaseConfiguration QueueConfiguration => - new RelationalDatabaseConfiguration(_sqlSettings.TestsBrighterConnectionString, queueStoreTable: _tableName); - private void CreateQueueTable() { _tableName = $"queue_{_tableName}"; diff --git a/tests/Paramore.Brighter.MySQL.Tests/MySqlTestHelper.cs b/tests/Paramore.Brighter.MySQL.Tests/MySqlTestHelper.cs index 580f8de0f6..4eddaeedf4 100644 --- a/tests/Paramore.Brighter.MySQL.Tests/MySqlTestHelper.cs +++ b/tests/Paramore.Brighter.MySQL.Tests/MySqlTestHelper.cs @@ -12,6 +12,12 @@ public class MySqlTestHelper private readonly bool _binaryMessagePayload; private string _tableName; private MySqlSettings _mysqlSettings; + + public RelationalDatabaseConfiguration InboxConfiguration => + new(_mysqlSettings.TestsBrighterConnectionString, inboxTableName: _tableName); + + public RelationalDatabaseConfiguration OutboxConfiguration => + new(_mysqlSettings.TestsBrighterConnectionString, outBoxTableName: _tableName, binaryMessagePayload: _binaryMessagePayload); public MySqlTestHelper(bool binaryMessagePayload = false) { @@ -50,13 +56,7 @@ public void SetupCommandDb() CreateInboxTable(); } - public RelationalDatabaseConfiguration InboxConfiguration => - new(_mysqlSettings.TestsBrighterConnectionString, _tableName); - - public RelationalDatabaseConfiguration OutboxConfiguration => - new(_mysqlSettings.TestsBrighterConnectionString, _tableName, binaryMessagePayload: _binaryMessagePayload); - - public void CleanUpDb() + public void CleanUpDb() { using (var connection = new MySqlConnection(_mysqlSettings.TestsBrighterConnectionString)) { diff --git a/tests/Paramore.Brighter.PostgresSQL.Tests/PostgresSqlTestHelper.cs b/tests/Paramore.Brighter.PostgresSQL.Tests/PostgresSqlTestHelper.cs index dc68d58f6c..0d3c5dc18c 100644 --- a/tests/Paramore.Brighter.PostgresSQL.Tests/PostgresSqlTestHelper.cs +++ b/tests/Paramore.Brighter.PostgresSQL.Tests/PostgresSqlTestHelper.cs @@ -16,6 +16,12 @@ internal class PostgresSqlTestHelper private string _tableName; private readonly object syncObject = new object(); + 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; @@ -28,13 +34,6 @@ public PostgresSqlTestHelper(bool binaryMessagePayload = false) _tableName = $"test_{Guid.NewGuid():N}"; } - public RelationalDatabaseConfiguration Configuration - => new RelationalDatabaseConfiguration(_postgreSqlSettings.TestsBrighterConnectionString, - outBoxTableName: _tableName, binaryMessagePayload: _binaryMessagePayload); - - public RelationalDatabaseConfiguration InboxConfiguration - => new RelationalDatabaseConfiguration(_postgreSqlSettings.TestsBrighterConnectionString, _tableName); - public void SetupMessageDb() { 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 fdc7468e99..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 RelationalDatabaseConfiguration(_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 7faf63784a..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 RelationalDatabaseConfiguration(_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 9f5bc91406..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 RelationalDatabaseConfiguration(_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 872f1aea14..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 RelationalDatabaseConfiguration(_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 1958085ff5..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 RelationalDatabaseConfiguration(_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 af477b7017..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 RelationalDatabaseConfiguration(_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 2a9c422f32..82ff79bef8 100644 --- a/tests/Paramore.Brighter.Sqlite.Tests/Outbox/SQlOutboxMigrationTests.cs +++ b/tests/Paramore.Brighter.Sqlite.Tests/Outbox/SQlOutboxMigrationTests.cs @@ -43,7 +43,7 @@ public SQlOutboxMigrationTests() { _sqliteTestHelper = new SqliteTestHelper(); _sqliteTestHelper.SetupMessageDb(); - _sqlOutbox = new SqliteOutbox(new RelationalDatabaseConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableNameMessages)); + _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.TableNameMessages); + 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()), 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 107fba7120..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 @@ -46,7 +46,7 @@ public SqlOutboxDeletingMessagesTests() { _sqliteTestHelper = new SqliteTestHelper(); _sqliteTestHelper.SetupMessageDb(); - _sqlOutbox = new SqliteOutbox(new RelationalDatabaseConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableNameMessages)); + _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")); 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 856399c194..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 @@ -42,7 +42,7 @@ public SqliteOutboxMessageAlreadyExistsTests() { _sqliteTestHelper = new SqliteTestHelper(); _sqliteTestHelper.SetupMessageDb(); - _sqlOutbox = new SqliteOutbox(new RelationalDatabaseConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableNameMessages)); + _sqlOutbox = new SqliteOutbox(_sqliteTestHelper.OutboxConfiguration); _messageEarliest = new Message(new MessageHeader(Guid.NewGuid(), "test_topic", MessageType.MT_DOCUMENT), new MessageBody("message body")); _sqlOutbox.Add(_messageEarliest); 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 7a334ec819..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 @@ -43,7 +43,7 @@ public SqliteOutboxMessageAlreadyExistsAsyncTests() { _sqliteTestHelper = new SqliteTestHelper(); _sqliteTestHelper.SetupMessageDb(); - _sqlOutbox = new SqliteOutbox(new RelationalDatabaseConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableNameMessages)); + _sqlOutbox = new SqliteOutbox(_sqliteTestHelper.OutboxConfiguration); _messageEarliest = new Message( new MessageHeader( Guid.NewGuid(), 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 147279e8e8..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 @@ -49,7 +49,7 @@ public SqliteOutboxRangeRequestTests() { _sqliteTestHelper = new SqliteTestHelper(); _sqliteTestHelper.SetupMessageDb(); - _sqlOutbox = new SqliteOutbox(new RelationalDatabaseConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableNameMessages)); + _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")); 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 dedc47d62b..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 @@ -49,7 +49,7 @@ public SqliteOutboxRangeRequestAsyncTests() { _sqliteTestHelper = new SqliteTestHelper(); _sqliteTestHelper.SetupMessageDb(); - _sqlOutbox = new SqliteOutbox(new RelationalDatabaseConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableNameMessages)); + _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")); 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 069135403f..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 @@ -42,7 +42,7 @@ public SqliteOutboxEmptyStoreTests() { _sqliteTestHelper = new SqliteTestHelper(); _sqliteTestHelper.SetupMessageDb(); - _sqlOutbox = new SqliteOutbox(new RelationalDatabaseConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableNameMessages)); + _sqlOutbox = new SqliteOutbox(_sqliteTestHelper.OutboxConfiguration); _messageEarliest = new Message(new MessageHeader(Guid.NewGuid(), "test_topic", MessageType.MT_DOCUMENT), new MessageBody("message body")); } 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 690bee8512..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 @@ -43,7 +43,7 @@ public SqliteOutboxEmptyStoreAsyncTests() { _sqliteTestHelper = new SqliteTestHelper(); _sqliteTestHelper.SetupMessageDb(); - _sqlOutbox = new SqliteOutbox(new RelationalDatabaseConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableNameMessages)); + _sqlOutbox = new SqliteOutbox(_sqliteTestHelper.OutboxConfiguration); _messageEarliest = new Message(new MessageHeader(Guid.NewGuid(), "test_topic", MessageType.MT_DOCUMENT), new MessageBody("message body")); } 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 index 3b2b378f25..48526f23c1 100644 --- 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 @@ -53,7 +53,7 @@ public SqliteOutboxWritingBinaryMessageTests() { _sqliteTestHelper = new SqliteTestHelper(binaryMessagePayload: true); _sqliteTestHelper.SetupMessageDb(); - _sqlOutbox = new SqliteOutbox(new RelationalDatabaseConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableNameMessages, binaryMessagePayload: true)); + _sqlOutbox = new SqliteOutbox(_sqliteTestHelper.OutboxConfiguration); var messageHeader = new MessageHeader( messageId:Guid.NewGuid(), topic: "test_topic", 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 9bc765c2a0..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 @@ -52,7 +52,7 @@ public SqliteOutboxWritingMessageTests() { _sqliteTestHelper = new SqliteTestHelper(); _sqliteTestHelper.SetupMessageDb(); - _sqlOutbox = new SqliteOutbox(new RelationalDatabaseConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableNameMessages)); + _sqlOutbox = new SqliteOutbox(_sqliteTestHelper.OutboxConfiguration); var messageHeader = new MessageHeader( messageId:Guid.NewGuid(), topic: "test_topic", 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 384f485652..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 @@ -52,8 +52,8 @@ public SqliteOutboxWritingMessageAsyncTests() { _sqliteTestHelper = new SqliteTestHelper(); _sqliteTestHelper.SetupMessageDb(); - _sqlOutbox = new SqliteOutbox(new RelationalDatabaseConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableNameMessages)); - + _sqlOutbox = new SqliteOutbox(_sqliteTestHelper.OutboxConfiguration); + var messageHeader = new MessageHeader( Guid.NewGuid(), "test_topic", 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 bdba229adf..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 @@ -46,8 +46,8 @@ public SqlOutboxWritngMessagesTests() { _sqliteTestHelper = new SqliteTestHelper(); _sqliteTestHelper.SetupMessageDb(); - _sqlOutbox = new SqliteOutbox(new RelationalDatabaseConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableNameMessages)); - + _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")); 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 0b3ee0a434..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,7 +37,7 @@ namespace Paramore.Brighter.Sqlite.Tests.Outbox public class SqlOutboxWritngMessagesAsyncTests : IDisposable { private readonly SqliteTestHelper _sqliteTestHelper; - private readonly SqliteOutbox _sSqlOutbox; + private readonly SqliteOutbox _sqlOutbox; private Message _messageTwo; private Message _messageOne; private Message _messageThree; @@ -47,7 +47,7 @@ public SqlOutboxWritngMessagesAsyncTests() { _sqliteTestHelper = new SqliteTestHelper(); _sqliteTestHelper.SetupMessageDb(); - _sSqlOutbox = new SqliteOutbox(new RelationalDatabaseConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableNameMessages)); + _sqlOutbox = new SqliteOutbox(_sqliteTestHelper.OutboxConfiguration); } [Fact] @@ -55,7 +55,7 @@ public async Task When_Writing_Messages_To_The_Outbox_Async() { await SetUpMessagesAsync(); - _retrievedMessages = await _sSqlOutbox.GetAsync(); + _retrievedMessages = await _sqlOutbox.GetAsync(); //should read last message last from the outbox _retrievedMessages.Last().Id.Should().Be(_messageThree.Id); @@ -69,9 +69,9 @@ 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 _sSqlOutbox.AddAsync(messages); + await _sqlOutbox.AddAsync(messages); - _retrievedMessages = await _sSqlOutbox.GetAsync(); + _retrievedMessages = await _sqlOutbox.GetAsync(); //should read last message last from the outbox _retrievedMessages.Last().Id.Should().Be(_messageThree.Id); @@ -84,13 +84,13 @@ public async Task When_Writing_Messages_To_The_Outbox_Async_Bulk() private async Task> SetUpMessagesAsync(bool addMessagesToOutbox = true) { _messageOne = new Message(new MessageHeader(Guid.NewGuid(), "Test", MessageType.MT_COMMAND, DateTime.UtcNow.AddHours(-3)), new MessageBody("Body")); - if(addMessagesToOutbox) await _sSqlOutbox.AddAsync(_messageOne); + if(addMessagesToOutbox) await _sqlOutbox.AddAsync(_messageOne); _messageTwo = new Message(new MessageHeader(Guid.NewGuid(), "Test2", MessageType.MT_COMMAND, DateTime.UtcNow.AddHours(-2)), new MessageBody("Body2")); - if(addMessagesToOutbox) await _sSqlOutbox.AddAsync(_messageTwo); + if(addMessagesToOutbox) await _sqlOutbox.AddAsync(_messageTwo); _messageThree = new Message(new MessageHeader(Guid.NewGuid(), "Test3", MessageType.MT_COMMAND, DateTime.UtcNow.AddHours(-1)), new MessageBody("Body3")); - if(addMessagesToOutbox) await _sSqlOutbox.AddAsync(_messageThree); + if(addMessagesToOutbox) await _sqlOutbox.AddAsync(_messageThree); return new List { _messageOne, _messageTwo, _messageThree }; } 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 29c173a837..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 @@ -24,8 +24,8 @@ public SqliteOutboxBulkGetAsyncTests() { _sqliteTestHelper = new SqliteTestHelper(); _sqliteTestHelper.SetupMessageDb(); - _sqlOutbox = new SqliteOutbox(new RelationalDatabaseConfiguration(_sqliteTestHelper.ConnectionString, _sqliteTestHelper.TableNameMessages)); - + _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 0f72a9022e..56a89e8a0e 100644 --- a/tests/Paramore.Brighter.Sqlite.Tests/SqliteTestHelper.cs +++ b/tests/Paramore.Brighter.Sqlite.Tests/SqliteTestHelper.cs @@ -11,10 +11,15 @@ public class SqliteTestHelper private readonly bool _binaryMessagePayload; private const string TestDbPath = "test.db"; public string ConnectionString = $"DataSource=\"{TestDbPath}\""; - public readonly string TableName = "test_commands"; - public readonly string TableNameMessages = "test_messages"; + 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) { @@ -25,14 +30,14 @@ public void SetupCommandDb() { _connectionStringPath = GetUniqueTestDbPathAndCreateDir(); ConnectionString = $"DataSource=\"{_connectionStringPath}\""; - CreateDatabaseWithTable(ConnectionString, SqliteInboxBuilder.GetDDL(TableName)); + CreateDatabaseWithTable(ConnectionString, SqliteInboxBuilder.GetDDL(InboxTableName)); } public void SetupMessageDb() { _connectionStringPath = GetUniqueTestDbPathAndCreateDir(); ConnectionString = $"DataSource=\"{_connectionStringPath}\""; - CreateDatabaseWithTable(ConnectionString, SqliteOutboxBuilder.GetDDL(TableNameMessages, hasBinaryMessagePayload: _binaryMessagePayload)); + CreateDatabaseWithTable(ConnectionString, SqliteOutboxBuilder.GetDDL(OutboxTableName, hasBinaryMessagePayload: _binaryMessagePayload)); } private string GetUniqueTestDbPathAndCreateDir() From dae3153138492d34f6df09b98f56674ee092369f Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Sun, 7 May 2023 19:18:32 +0100 Subject: [PATCH 36/89] Fix base type --- .../Handlers/AddGreetingHandlerAsync.cs | 1 - .../Handlers/AddGreetingHandlerAsync.cs | 1 - .../Orders.Data/SqlConnectionProvider.cs | 2 +- src/Paramore.Brighter.Inbox.MsSql/MsSqlInbox.cs | 1 - .../MsSqlMessageConsumer.cs | 1 - .../MsSqlEntityFrameworkCoreConnectonProvider.cs | 16 ++++++++-------- .../MsSqlSqlAuthConnectionProvider.cs | 1 - .../MySqlEntityFrameworkConnectionProvider.cs | 12 ++++++------ .../MySqlConnectionProvider.cs | 1 - .../MsSqlOutbox.cs | 1 - ...PostgreSqlEntityFrameworkConnectonProvider.cs | 14 +++++++------- .../SqliteDapperConnectionProvider.cs | 1 - .../SqliteEntityFrameworkConnectionProvider.cs | 3 +-- .../SqliteConnectionProvider.cs | 1 - .../RelationalDbConnectionProvider.cs | 2 +- 15 files changed, 24 insertions(+), 34 deletions(-) diff --git a/samples/WebAPI_Dapper/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs b/samples/WebAPI_Dapper/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs index 5962ce037a..d43ae4f612 100644 --- a/samples/WebAPI_Dapper/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs +++ b/samples/WebAPI_Dapper/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs @@ -12,7 +12,6 @@ using Microsoft.Extensions.Logging; using Paramore.Brighter.Logging.Attributes; using Paramore.Brighter.Policies.Attributes; -using Paramore.Brighter.PostgreSql; using Paramore.Brighter.Sqlite.Dapper; namespace GreetingsPorts.Handlers diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs index 5962ce037a..d43ae4f612 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs +++ b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs @@ -12,7 +12,6 @@ using Microsoft.Extensions.Logging; using Paramore.Brighter.Logging.Attributes; using Paramore.Brighter.Policies.Attributes; -using Paramore.Brighter.PostgreSql; using Paramore.Brighter.Sqlite.Dapper; namespace GreetingsPorts.Handlers diff --git a/samples/WebApiWithWorkerAndSweeper/Orders.Data/SqlConnectionProvider.cs b/samples/WebApiWithWorkerAndSweeper/Orders.Data/SqlConnectionProvider.cs index 4c04ae3e59..172f41c85e 100644 --- a/samples/WebApiWithWorkerAndSweeper/Orders.Data/SqlConnectionProvider.cs +++ b/samples/WebApiWithWorkerAndSweeper/Orders.Data/SqlConnectionProvider.cs @@ -1,6 +1,6 @@ using System.Data.Common; using Microsoft.Data.SqlClient; -using Paramore.Brighter.PostgreSql; +using Paramore.Brighter; namespace Orders.Data; diff --git a/src/Paramore.Brighter.Inbox.MsSql/MsSqlInbox.cs b/src/Paramore.Brighter.Inbox.MsSql/MsSqlInbox.cs index 196eea60e5..e93ba8d897 100644 --- a/src/Paramore.Brighter.Inbox.MsSql/MsSqlInbox.cs +++ b/src/Paramore.Brighter.Inbox.MsSql/MsSqlInbox.cs @@ -34,7 +34,6 @@ THE SOFTWARE. */ using Paramore.Brighter.Inbox.Exceptions; using Paramore.Brighter.MsSql; using Paramore.Brighter.Logging; -using Paramore.Brighter.PostgreSql; namespace Paramore.Brighter.Inbox.MsSql { diff --git a/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlMessageConsumer.cs b/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlMessageConsumer.cs index 45008874dc..5a52c4302f 100644 --- a/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlMessageConsumer.cs +++ b/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlMessageConsumer.cs @@ -4,7 +4,6 @@ using Paramore.Brighter.Logging; using Paramore.Brighter.MessagingGateway.MsSql.SqlQueues; using Paramore.Brighter.MsSql; -using Paramore.Brighter.PostgreSql; namespace Paramore.Brighter.MessagingGateway.MsSql { diff --git a/src/Paramore.Brighter.MsSql.EntityFrameworkCore/MsSqlEntityFrameworkCoreConnectonProvider.cs b/src/Paramore.Brighter.MsSql.EntityFrameworkCore/MsSqlEntityFrameworkCoreConnectonProvider.cs index ad8987eee0..b9e9c69a75 100644 --- a/src/Paramore.Brighter.MsSql.EntityFrameworkCore/MsSqlEntityFrameworkCoreConnectonProvider.cs +++ b/src/Paramore.Brighter.MsSql.EntityFrameworkCore/MsSqlEntityFrameworkCoreConnectonProvider.cs @@ -7,7 +7,7 @@ namespace Paramore.Brighter.MsSql.EntityFrameworkCore { - public class MsSqlEntityFrameworkCoreConnectonProvider : IAmATransactionConnectonProvider where T : DbContext + public class MsSqlEntityFrameworkCoreConnectonProvider : RelationalDbConnectionProvider, IAmATransactionConnectonProvider where T : DbContext { private readonly T _context; @@ -19,27 +19,27 @@ public MsSqlEntityFrameworkCoreConnectonProvider(T context) _context = context; } - public DbConnection 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 (SqlConnection)_context.Database.GetDbConnection(); + return _context.Database.GetDbConnection(); } - 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 (SqlConnection)_context.Database.GetDbConnection(); + return _context.Database.GetDbConnection(); } - public DbTransaction GetTransaction() + 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; } + public override bool HasOpenTransaction { get => _context.Database.CurrentTransaction != null; } + public override bool IsSharedConnection { get => true; } } } diff --git a/src/Paramore.Brighter.MsSql/MsSqlSqlAuthConnectionProvider.cs b/src/Paramore.Brighter.MsSql/MsSqlSqlAuthConnectionProvider.cs index 341c287057..c927ccff1b 100644 --- a/src/Paramore.Brighter.MsSql/MsSqlSqlAuthConnectionProvider.cs +++ b/src/Paramore.Brighter.MsSql/MsSqlSqlAuthConnectionProvider.cs @@ -1,6 +1,5 @@ using System.Data.Common; using Microsoft.Data.SqlClient; -using Paramore.Brighter.PostgreSql; namespace Paramore.Brighter.MsSql { diff --git a/src/Paramore.Brighter.MySql.EntityFrameworkCore/MySqlEntityFrameworkConnectionProvider.cs b/src/Paramore.Brighter.MySql.EntityFrameworkCore/MySqlEntityFrameworkConnectionProvider.cs index e4986a0ed2..266abb691a 100644 --- a/src/Paramore.Brighter.MySql.EntityFrameworkCore/MySqlEntityFrameworkConnectionProvider.cs +++ b/src/Paramore.Brighter.MySql.EntityFrameworkCore/MySqlEntityFrameworkConnectionProvider.cs @@ -11,7 +11,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 : IAmATransactionConnectonProvider where T: DbContext + public class MySqlEntityFrameworkConnectionProvider : RelationalDbConnectionProvider, IAmATransactionConnectonProvider where T: DbContext { private readonly T _context; @@ -28,7 +28,7 @@ public MySqlEntityFrameworkConnectionProvider(T context) /// Get the current connection of the DB context /// /// The Sqlite Connection that is in use - public DbConnection 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(); @@ -40,7 +40,7 @@ public DbConnection 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); @@ -51,12 +51,12 @@ public async Task GetConnectionAsync(CancellationToken cancellatio /// Get the ambient EF Core Transaction /// /// The Sqlite Transaction - public DbTransaction GetTransaction() + public override DbTransaction GetTransaction() { return _context.Database.CurrentTransaction?.GetDbTransaction(); } - public bool HasOpenTransaction { get => _context.Database.CurrentTransaction != null; } - public bool IsSharedConnection { get => true; } + public override bool HasOpenTransaction { get => _context.Database.CurrentTransaction != null; } + public override bool IsSharedConnection { get => true; } } } diff --git a/src/Paramore.Brighter.MySql/MySqlConnectionProvider.cs b/src/Paramore.Brighter.MySql/MySqlConnectionProvider.cs index ea50fc44dd..010ec94bab 100644 --- a/src/Paramore.Brighter.MySql/MySqlConnectionProvider.cs +++ b/src/Paramore.Brighter.MySql/MySqlConnectionProvider.cs @@ -24,7 +24,6 @@ THE SOFTWARE. */ using System.Data.Common; using MySqlConnector; -using Paramore.Brighter.PostgreSql; namespace Paramore.Brighter.MySql { diff --git a/src/Paramore.Brighter.Outbox.MsSql/MsSqlOutbox.cs b/src/Paramore.Brighter.Outbox.MsSql/MsSqlOutbox.cs index 43a7589830..f3737b7911 100644 --- a/src/Paramore.Brighter.Outbox.MsSql/MsSqlOutbox.cs +++ b/src/Paramore.Brighter.Outbox.MsSql/MsSqlOutbox.cs @@ -34,7 +34,6 @@ THE SOFTWARE. */ using System.Threading.Tasks; using Paramore.Brighter.Logging; using Paramore.Brighter.MsSql; -using Paramore.Brighter.PostgreSql; namespace Paramore.Brighter.Outbox.MsSql { diff --git a/src/Paramore.Brighter.PostgreSql.EntityFrameworkCore/PostgreSqlEntityFrameworkConnectonProvider.cs b/src/Paramore.Brighter.PostgreSql.EntityFrameworkCore/PostgreSqlEntityFrameworkConnectonProvider.cs index 366580518f..dad29315a2 100644 --- a/src/Paramore.Brighter.PostgreSql.EntityFrameworkCore/PostgreSqlEntityFrameworkConnectonProvider.cs +++ b/src/Paramore.Brighter.PostgreSql.EntityFrameworkCore/PostgreSqlEntityFrameworkConnectonProvider.cs @@ -12,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 PostgreSqlEntityFrameworkConnectonProvider : IAmATransactionConnectonProvider where T : DbContext + public class PostgreSqlEntityFrameworkConnectonProvider : RelationalDbConnectionProvider, IAmATransactionConnectonProvider where T : DbContext { private readonly T _context; @@ -29,7 +29,7 @@ public PostgreSqlEntityFrameworkConnectonProvider(T context) /// Get the current connection of the database context /// /// The NpgsqlConnection that is in use - public DbConnection GetConnection() + public override DbConnection GetConnection() { return _context.Database.GetDbConnection(); } @@ -39,10 +39,10 @@ public DbConnection GetConnection() /// /// A cancellation token /// - public Task GetConnectionAsync(CancellationToken cancellationToken = default) + public override Task GetConnectionAsync(CancellationToken cancellationToken = default) { var tcs = new TaskCompletionSource(); - tcs.SetResult((DbConnection)_context.Database.GetDbConnection()); + tcs.SetResult(_context.Database.GetDbConnection()); return tcs.Task; } @@ -50,13 +50,13 @@ public Task GetConnectionAsync(CancellationToken cancellationToken /// Get the ambient Transaction /// /// The NpgsqlTransaction - public DbTransaction GetTransaction() + public override DbTransaction GetTransaction() { 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.Sqlite.Dapper/SqliteDapperConnectionProvider.cs b/src/Paramore.Brighter.Sqlite.Dapper/SqliteDapperConnectionProvider.cs index a6447e013d..3c0e2bd51c 100644 --- a/src/Paramore.Brighter.Sqlite.Dapper/SqliteDapperConnectionProvider.cs +++ b/src/Paramore.Brighter.Sqlite.Dapper/SqliteDapperConnectionProvider.cs @@ -4,7 +4,6 @@ using System.Threading.Tasks; using Microsoft.Data.Sqlite; using Paramore.Brighter.Dapper; -using Paramore.Brighter.PostgreSql; namespace Paramore.Brighter.Sqlite.Dapper { diff --git a/src/Paramore.Brighter.Sqlite.EntityFrameworkCore/SqliteEntityFrameworkConnectionProvider.cs b/src/Paramore.Brighter.Sqlite.EntityFrameworkCore/SqliteEntityFrameworkConnectionProvider.cs index 3e5103bc62..11a3246e6a 100644 --- a/src/Paramore.Brighter.Sqlite.EntityFrameworkCore/SqliteEntityFrameworkConnectionProvider.cs +++ b/src/Paramore.Brighter.Sqlite.EntityFrameworkCore/SqliteEntityFrameworkConnectionProvider.cs @@ -4,7 +4,6 @@ using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Storage; -using Paramore.Brighter.PostgreSql; namespace Paramore.Brighter.Sqlite.EntityFrameworkCore { @@ -12,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 : RelationalDbConnectionProvider where T: DbContext + public class SqliteEntityFrameworkConnectionProvider : RelationalDbConnectionProvider, IAmATransactionConnectonProvider where T: DbContext { private readonly T _context; diff --git a/src/Paramore.Brighter.Sqlite/SqliteConnectionProvider.cs b/src/Paramore.Brighter.Sqlite/SqliteConnectionProvider.cs index 48cf3c01b2..a1354c3136 100644 --- a/src/Paramore.Brighter.Sqlite/SqliteConnectionProvider.cs +++ b/src/Paramore.Brighter.Sqlite/SqliteConnectionProvider.cs @@ -27,7 +27,6 @@ THE SOFTWARE. */ using System.Threading; using System.Threading.Tasks; using Microsoft.Data.Sqlite; -using Paramore.Brighter.PostgreSql; namespace Paramore.Brighter.Sqlite { diff --git a/src/Paramore.Brighter/RelationalDbConnectionProvider.cs b/src/Paramore.Brighter/RelationalDbConnectionProvider.cs index a5cd70d871..ca541c11e4 100644 --- a/src/Paramore.Brighter/RelationalDbConnectionProvider.cs +++ b/src/Paramore.Brighter/RelationalDbConnectionProvider.cs @@ -2,7 +2,7 @@ using System.Threading; using System.Threading.Tasks; -namespace Paramore.Brighter.PostgreSql +namespace Paramore.Brighter { public abstract class RelationalDbConnectionProvider : IAmARelationalDbConnectionProvider { From d57bdd5ca45fe4c57b9127b665914a273b7dae7b Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Mon, 8 May 2023 12:50:58 +0100 Subject: [PATCH 37/89] Move to new model for Dbconnection provision --- Brighter.sln | 42 -------- .../GreetingsSender.Web/Program.cs | 2 +- .../ASBTaskQueue/GreetingsWorker/Program.cs | 2 +- .../GreetingsPorts/GreetingsPorts.csproj | 2 - .../Handlers/AddGreetingHandlerAsync.cs | 2 - .../Handlers/AddPersonHandlerAsync.cs | 9 +- .../Handlers/DeletePersonHandlerAsync.cs | 13 ++- .../FIndGreetingsForPersonHandlerAsync.cs | 10 +- .../Handlers/FindPersonByNameHandlerAsync.cs | 10 +- .../GreetingsWeb/Database/OutboxExtensions.cs | 7 +- .../GreetingsWeb/GreetingsWeb.csproj | 2 - samples/WebAPI_Dapper/GreetingsWeb/Startup.cs | 13 +-- .../SalutationAnalytics/Program.cs | 24 ++--- .../SalutationAnalytics.csproj | 2 - .../Handlers/GreetingMadeHandler.cs | 11 +-- .../SalutationPorts/SalutationPorts.csproj | 1 - .../GreetingsPorts/GreetingsPorts.csproj | 2 - .../Handlers/AddGreetingHandlerAsync.cs | 2 - .../Handlers/AddPersonHandlerAsync.cs | 9 +- .../Handlers/DeletePersonHandlerAsync.cs | 13 ++- .../FIndGreetingsForPersonHandlerAsync.cs | 10 +- .../Handlers/FindPersonByNameHandlerAsync.cs | 10 +- .../GreetingsWeb/Database/OutboxExtensions.cs | 96 +++++++++--------- .../GreetingsWeb/GreetingsWeb.csproj | 2 - .../GreetingsWeb/Startup.cs | 39 +++----- .../SalutationAnalytics/Program.cs | 6 +- .../SalutationAnalytics.csproj | 2 - .../Handlers/GreetingMadeHandler.cs | 15 ++- .../SalutationPorts/SalutationPorts.csproj | 1 - .../Extensions/BrighterExtensions.cs | 4 +- src/Paramore.Brighter.Dapper/IUnitOfWork.cs | 42 -------- .../ServiceCollectionExtensions.cs | 4 +- .../MsSqlInbox.cs | 8 +- .../MySqlInbox.cs | 4 +- .../PostgresSqlInbox.cs | 4 +- .../SqliteInbox.cs | 4 +- ...cs => MsSqlAzureConnectionProviderBase.cs} | 9 +- .../MsSqlChainedConnectionProvider.cs | 4 +- ...=> MsSqlDefaultAzureConnectionProvider.cs} | 4 +- ...MsSqlManagedIdentityConnectionProvider.cs} | 4 +- ...sSqlSharedTokenCacheConnectionProvider.cs} | 6 +- ...=> MsSqlVisualStudioConnectionProvider.cs} | 4 +- .../MsSqlDapperConnectionProvider.cs | 49 --------- .../Paramore.Brighter.MsSql.Dapper.csproj | 22 ----- .../UnitOfWork.cs | 82 --------------- ...lEntityFrameworkCoreConnectionProvider.cs} | 4 +- .../MsSqlSqlAuthConnectionProvider.cs | 23 +++-- .../MySqlDapperConnectionProvider.cs | 49 --------- .../Paramore.Brighter.MySql.Dapper.csproj | 21 ---- .../UnitOfWork.cs | 99 ------------------- .../MySqlEntityFrameworkConnectionProvider.cs | 2 +- .../MySqlConnectionProvider.cs | 59 +++++------ .../DynamoDbOutbox.cs | 4 +- .../EventStoreOutboxSync.cs | 4 +- .../MsSqlOutbox.cs | 10 +- .../MySqlOutbox.cs | 10 +- .../ServiceCollectionExtensions.cs | 12 ++- .../PostgreSqlOutbox.cs | 16 +-- .../ServiceCollectionExtensions.cs | 4 +- .../SqliteOutbox.cs | 10 +- ...reSqlEntityFrameworkConnectionProvider.cs} | 4 +- .../PostgreSqlNpgsqlConnectionProvider.cs | 15 +-- .../ControlBusReceiverBuilder.cs | 2 +- .../Paramore.Brighter.Sqlite.Dapper.csproj | 20 ---- .../SqliteDapperConnectionProvider.cs | 43 -------- .../UnitOfWork.cs | 87 ---------------- ...SqliteEntityFrameworkConnectionProvider.cs | 2 +- .../SqliteConnectionProvider.cs | 23 ++--- src/Paramore.Brighter/CommandProcessor.cs | 36 +++---- .../CommandProcessorBuilder.cs | 16 +-- src/Paramore.Brighter/ExternalBusServices.cs | 8 +- .../IAmABoxTransactionProvider.cs | 8 +- src/Paramore.Brighter/IAmABulkOutboxAsync.cs | 2 +- src/Paramore.Brighter/IAmABulkOutboxSync.cs | 2 +- .../IAmARelationalDatabaseConfiguration.cs | 33 +++++++ .../IAmARelationalDbConnectionProvider.cs | 2 + .../IAmATransactionConnectionProvider.cs | 4 + .../IAmATransactionConnectonProvider.cs | 4 - src/Paramore.Brighter/IAmAnOutboxAsync.cs | 2 +- src/Paramore.Brighter/IAmAnOutboxSync.cs | 2 +- src/Paramore.Brighter/InMemoryOutbox.cs | 8 +- .../RelationDatabaseOutbox.cs | 12 +-- .../RelationalDatabaseConfiguration.cs | 2 +- .../RelationalDbConnectionProvider.cs | 74 +++++++++++++- .../TestDoubles/FakeOutboxSync.cs | 8 +- 85 files changed, 440 insertions(+), 919 deletions(-) delete mode 100644 src/Paramore.Brighter.Dapper/IUnitOfWork.cs rename src/Paramore.Brighter.MsSql.Azure/{MsSqlAzureConnectonProviderBase.cs => MsSqlAzureConnectionProviderBase.cs} (90%) rename src/Paramore.Brighter.MsSql.Azure/{MsSqlDefaultAzureConnectonProvider.cs => MsSqlDefaultAzureConnectionProvider.cs} (81%) rename src/Paramore.Brighter.MsSql.Azure/{MsSqlManagedIdentityConnectonProvider.cs => MsSqlManagedIdentityConnectionProvider.cs} (81%) rename src/Paramore.Brighter.MsSql.Azure/{MsSqlSharedTokenCacheConnectonProvider.cs => MsSqlSharedTokenCacheConnectionProvider.cs} (84%) rename src/Paramore.Brighter.MsSql.Azure/{MsSqlVisualStudioConnectonProvider.cs => MsSqlVisualStudioConnectionProvider.cs} (81%) delete mode 100644 src/Paramore.Brighter.MsSql.Dapper/MsSqlDapperConnectionProvider.cs delete mode 100644 src/Paramore.Brighter.MsSql.Dapper/Paramore.Brighter.MsSql.Dapper.csproj delete mode 100644 src/Paramore.Brighter.MsSql.Dapper/UnitOfWork.cs rename src/Paramore.Brighter.MsSql.EntityFrameworkCore/{MsSqlEntityFrameworkCoreConnectonProvider.cs => MsSqlEntityFrameworkCoreConnectionProvider.cs} (88%) delete mode 100644 src/Paramore.Brighter.MySql.Dapper/MySqlDapperConnectionProvider.cs delete mode 100644 src/Paramore.Brighter.MySql.Dapper/Paramore.Brighter.MySql.Dapper.csproj delete mode 100644 src/Paramore.Brighter.MySql.Dapper/UnitOfWork.cs rename src/Paramore.Brighter.PostgreSql.EntityFrameworkCore/{PostgreSqlEntityFrameworkConnectonProvider.cs => PostgreSqlEntityFrameworkConnectionProvider.cs} (90%) delete mode 100644 src/Paramore.Brighter.Sqlite.Dapper/Paramore.Brighter.Sqlite.Dapper.csproj delete mode 100644 src/Paramore.Brighter.Sqlite.Dapper/SqliteDapperConnectionProvider.cs delete mode 100644 src/Paramore.Brighter.Sqlite.Dapper/UnitOfWork.cs create mode 100644 src/Paramore.Brighter/IAmARelationalDatabaseConfiguration.cs create mode 100644 src/Paramore.Brighter/IAmATransactionConnectionProvider.cs delete mode 100644 src/Paramore.Brighter/IAmATransactionConnectonProvider.cs diff --git a/Brighter.sln b/Brighter.sln index c3cb5a2860..cb97462ee8 100644 --- a/Brighter.sln +++ b/Brighter.sln @@ -251,10 +251,6 @@ 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}" @@ -265,8 +261,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Greetings_MySqlMigrations", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Salutations_mySqlMigrations", "samples\WebAPI_Dapper\Salutations_mySqlMigrations\Salutations_mySqlMigrations.csproj", "{F72FF4C5-4CD8-4EFE-B468-5FAE45D20931}" 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}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "WebAPI_Dynamo", "WebAPI_Dynamo", "{11935469-A062-4CFF-9F72-F4F41E14C2B4}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GreetingsEntities", "samples\WebAPI_Dynamo\GreetingsEntities\GreetingsEntities.csproj", "{8C5F9810-2158-4479-9C0B-E139F2BC8125}" @@ -1524,30 +1518,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 @@ -1608,18 +1578,6 @@ Global {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 diff --git a/samples/ASBTaskQueue/GreetingsSender.Web/Program.cs b/samples/ASBTaskQueue/GreetingsSender.Web/Program.cs index f94faa0ca4..d626a014e2 100644 --- a/samples/ASBTaskQueue/GreetingsSender.Web/Program.cs +++ b/samples/ASBTaskQueue/GreetingsSender.Web/Program.cs @@ -57,7 +57,7 @@ .Create() ) .UseMsSqlOutbox(outboxConfig, typeof(MsSqlSqlAuthConnectionProvider)) - .UseMsSqlTransactionConnectionProvider(typeof(MsSqlEntityFrameworkCoreConnectonProvider)) + .UseMsSqlTransactionConnectionProvider(typeof(MsSqlEntityFrameworkCoreConnectionProvider)) .MapperRegistry(r => { r.Add(typeof(GreetingEvent), typeof(GreetingEventMessageMapper)); diff --git a/samples/ASBTaskQueue/GreetingsWorker/Program.cs b/samples/ASBTaskQueue/GreetingsWorker/Program.cs index 0c58499134..f93a1d9e14 100644 --- a/samples/ASBTaskQueue/GreetingsWorker/Program.cs +++ b/samples/ASBTaskQueue/GreetingsWorker/Program.cs @@ -82,7 +82,7 @@ options.UseScoped = true; }).UseMsSqlOutbox(outboxConfig, typeof(MsSqlSqlAuthConnectionProvider)) - .UseMsSqlTransactionConnectionProvider(typeof(MsSqlEntityFrameworkCoreConnectonProvider)) + .UseMsSqlTransactionConnectionProvider(typeof(MsSqlEntityFrameworkCoreConnectionProvider)) .AutoFromAssemblies(); builder.Services.AddHostedService(); diff --git a/samples/WebAPI_Dapper/GreetingsPorts/GreetingsPorts.csproj b/samples/WebAPI_Dapper/GreetingsPorts/GreetingsPorts.csproj index 61b428817b..564807e0b5 100644 --- a/samples/WebAPI_Dapper/GreetingsPorts/GreetingsPorts.csproj +++ b/samples/WebAPI_Dapper/GreetingsPorts/GreetingsPorts.csproj @@ -5,8 +5,6 @@ - - diff --git a/samples/WebAPI_Dapper/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs b/samples/WebAPI_Dapper/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs index d43ae4f612..a0fb7c7e83 100644 --- a/samples/WebAPI_Dapper/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs +++ b/samples/WebAPI_Dapper/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs @@ -6,13 +6,11 @@ using DapperExtensions; using DapperExtensions.Predicate; 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 { diff --git a/samples/WebAPI_Dapper/GreetingsPorts/Handlers/AddPersonHandlerAsync.cs b/samples/WebAPI_Dapper/GreetingsPorts/Handlers/AddPersonHandlerAsync.cs index 21baf781e5..27af764f2e 100644 --- a/samples/WebAPI_Dapper/GreetingsPorts/Handlers/AddPersonHandlerAsync.cs +++ b/samples/WebAPI_Dapper/GreetingsPorts/Handlers/AddPersonHandlerAsync.cs @@ -4,7 +4,6 @@ using GreetingsEntities; using GreetingsPorts.Requests; using Paramore.Brighter; -using Paramore.Brighter.Dapper; using Paramore.Brighter.Logging.Attributes; using Paramore.Brighter.Policies.Attributes; @@ -12,18 +11,18 @@ namespace GreetingsPorts.Handlers { public class AddPersonHandlerAsync : RequestHandlerAsync { - private readonly IUnitOfWork _uow; + private readonly IAmATransactionConnectionProvider _transactionConnectionProvider; - public AddPersonHandlerAsync(IUnitOfWork uow) + public AddPersonHandlerAsync(IAmATransactionConnectionProvider transactionConnectionProvider) { - _uow = uow; + _transactionConnectionProvider = transactionConnectionProvider; } [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 _transactionConnectionProvider.GetConnection().InsertAsync(new Person(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..b7e9956210 100644 --- a/samples/WebAPI_Dapper/GreetingsPorts/Handlers/DeletePersonHandlerAsync.cs +++ b/samples/WebAPI_Dapper/GreetingsPorts/Handlers/DeletePersonHandlerAsync.cs @@ -7,7 +7,6 @@ using GreetingsEntities; using GreetingsPorts.Requests; using Paramore.Brighter; -using Paramore.Brighter.Dapper; using Paramore.Brighter.Logging.Attributes; using Paramore.Brighter.Policies.Attributes; @@ -15,27 +14,27 @@ namespace GreetingsPorts.Handlers { public class DeletePersonHandlerAsync : RequestHandlerAsync { - private readonly IUnitOfWork _uow; + private readonly IAmATransactionConnectionProvider _transactionConnectionProvider; - public DeletePersonHandlerAsync(IUnitOfWork uow) + public DeletePersonHandlerAsync(IAmATransactionConnectionProvider transactionConnectionProvider) { - _uow = uow; + _transactionConnectionProvider = transactionConnectionProvider; } [RequestLoggingAsync(0, HandlerTiming.Before)] [UsePolicyAsync(step:1, policy: Policies.Retry.EXPONENTIAL_RETRYPOLICYASYNC)] public async override Task HandleAsync(DeletePerson deletePerson, CancellationToken cancellationToken = default) { - var tx = await _uow.BeginOrGetTransactionAsync(cancellationToken); + var tx = await _transactionConnectionProvider.GetTransactionAsync(cancellationToken); try { var searchbyName = Predicates.Field(p => p.Name, Operator.Eq, deletePerson.Name); - var people = await _uow.Database.GetListAsync(searchbyName, transaction: tx); + var people = await _transactionConnectionProvider.GetConnection().GetListAsync(searchbyName, transaction: tx); var person = people.Single(); var deleteById = Predicates.Field(g => g.RecipientId, Operator.Eq, person.Id); - await _uow.Database.DeleteAsync(deleteById, tx); + await _transactionConnectionProvider.GetConnection().DeleteAsync(deleteById, tx); await tx.CommitAsync(cancellationToken); } diff --git a/samples/WebAPI_Dapper/GreetingsPorts/Handlers/FIndGreetingsForPersonHandlerAsync.cs b/samples/WebAPI_Dapper/GreetingsPorts/Handlers/FIndGreetingsForPersonHandlerAsync.cs index d908df5c2c..355ecd5a5e 100644 --- a/samples/WebAPI_Dapper/GreetingsPorts/Handlers/FIndGreetingsForPersonHandlerAsync.cs +++ b/samples/WebAPI_Dapper/GreetingsPorts/Handlers/FIndGreetingsForPersonHandlerAsync.cs @@ -6,7 +6,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 +15,11 @@ namespace GreetingsPorts.Handlers { public class FIndGreetingsForPersonHandlerAsync : QueryHandlerAsync { - private readonly IUnitOfWork _uow; + private readonly IAmATransactionConnectionProvider _transactionConnectionProvider; - public FIndGreetingsForPersonHandlerAsync(IUnitOfWork uow) + public FIndGreetingsForPersonHandlerAsync(IAmATransactionConnectionProvider transactionConnectionProvider) { - _uow = uow; + _transactionConnectionProvider = transactionConnectionProvider; } [QueryLogging(0)] @@ -33,7 +33,7 @@ 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) => + var people = await _transactionConnectionProvider.GetConnection().QueryAsync(sql, (person, greeting) => { person.Greetings.Add(greeting); return person; diff --git a/samples/WebAPI_Dapper/GreetingsPorts/Handlers/FindPersonByNameHandlerAsync.cs b/samples/WebAPI_Dapper/GreetingsPorts/Handlers/FindPersonByNameHandlerAsync.cs index 1ab5898541..9682055c2d 100644 --- a/samples/WebAPI_Dapper/GreetingsPorts/Handlers/FindPersonByNameHandlerAsync.cs +++ b/samples/WebAPI_Dapper/GreetingsPorts/Handlers/FindPersonByNameHandlerAsync.cs @@ -7,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; @@ -16,11 +16,11 @@ namespace GreetingsPorts.Handlers { public class FindPersonByNameHandlerAsync : QueryHandlerAsync { - private readonly IUnitOfWork _uow; + private readonly IAmATransactionConnectionProvider _transactionConnectionProvider; - public FindPersonByNameHandlerAsync(IUnitOfWork uow) + public FindPersonByNameHandlerAsync(IAmATransactionConnectionProvider transactionConnectionProvider) { - _uow = uow; + _transactionConnectionProvider = transactionConnectionProvider; } [QueryLogging(0)] @@ -28,7 +28,7 @@ public FindPersonByNameHandlerAsync(IUnitOfWork uow) 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 people = await _transactionConnectionProvider.GetConnection().GetListAsync(searchbyName); var person = people.Single(); return new FindPersonResult(person); diff --git a/samples/WebAPI_Dapper/GreetingsWeb/Database/OutboxExtensions.cs b/samples/WebAPI_Dapper/GreetingsWeb/Database/OutboxExtensions.cs index 0249b41b48..b87334a6ab 100644 --- a/samples/WebAPI_Dapper/GreetingsWeb/Database/OutboxExtensions.cs +++ b/samples/WebAPI_Dapper/GreetingsWeb/Database/OutboxExtensions.cs @@ -6,12 +6,9 @@ using Paramore.Brighter.Extensions.DependencyInjection; using Paramore.Brighter.Extensions.Hosting; using Paramore.Brighter.MySql; -using Paramore.Brighter.MySql.Dapper; using Paramore.Brighter.Outbox.MySql; using Paramore.Brighter.Outbox.Sqlite; using Paramore.Brighter.Sqlite; -using Paramore.Brighter.Sqlite.Dapper; -using UnitOfWork = Paramore.Brighter.MySql.Dapper.UnitOfWork; namespace GreetingsWeb.Database; @@ -44,7 +41,7 @@ private static void AddMySqlOutbox(IBrighterBuilder brighterBuilder, string dbCo new RelationalDatabaseConfiguration(dbConnectionString, outBoxTableName), typeof(MySqlConnectionProvider), ServiceLifetime.Singleton) - .UseMySqTransactionConnectionProvider(typeof(Paramore.Brighter.MySql.Dapper.MySqlDapperConnectionProvider), ServiceLifetime.Scoped) + .UseMySqTransactionConnectionProvider(typeof(Paramore.Brighter.MySql.MySqlConnectionProvider), ServiceLifetime.Scoped) .UseOutboxSweeper(); } @@ -54,7 +51,7 @@ private static void AddSqliteOutBox(IBrighterBuilder brighterBuilder, string dbC new RelationalDatabaseConfiguration(dbConnectionString, outBoxTableName), typeof(SqliteConnectionProvider), ServiceLifetime.Singleton) - .UseSqliteTransactionConnectionProvider(typeof(Paramore.Brighter.Sqlite.Dapper.SqliteDapperConnectionProvider), ServiceLifetime.Scoped) + .UseSqliteTransactionConnectionProvider(typeof(Paramore.Brighter.Sqlite.SqliteConnectionProvider), ServiceLifetime.Scoped) .UseOutboxSweeper(options => { options.TimerInterval = 5; diff --git a/samples/WebAPI_Dapper/GreetingsWeb/GreetingsWeb.csproj b/samples/WebAPI_Dapper/GreetingsWeb/GreetingsWeb.csproj index 3d30f65e5d..fb7ffc1ade 100644 --- a/samples/WebAPI_Dapper/GreetingsWeb/GreetingsWeb.csproj +++ b/samples/WebAPI_Dapper/GreetingsWeb/GreetingsWeb.csproj @@ -17,10 +17,8 @@ - - diff --git a/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs b/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs index 5a16c8a4a3..d4e7d14a20 100644 --- a/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs +++ b/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs @@ -16,7 +16,6 @@ using Microsoft.Extensions.Hosting; using Microsoft.OpenApi.Models; using Paramore.Brighter; -using Paramore.Brighter.Dapper; using Paramore.Brighter.Extensions.DependencyInjection; using Paramore.Brighter.MessagingGateway.RMQ; using Paramore.Darker.AspNetCore; @@ -120,8 +119,6 @@ private void ConfigureMySql(IServiceCollection services) private void ConfigureDapper(IServiceCollection services) { - services.AddSingleton(new DbConnectionStringProvider(DbConnectionString())); - ConfigureDapperByHost(GetDatabaseType(), services); DapperExtensions.DapperExtensions.SetMappingAssemblies(new[] { typeof(PersonMapper).Assembly }); @@ -147,19 +144,23 @@ private static void ConfigureDapperSqlite(IServiceCollection services) { DapperExtensions.DapperExtensions.SqlDialect = new SqliteDialect(); DapperAsyncExtensions.SqlDialect = new SqliteDialect(); - services.AddScoped(); + services.AddScoped(); } private static void ConfigureDapperMySql(IServiceCollection services) { DapperExtensions.DapperExtensions.SqlDialect = new MySqlDialect(); DapperAsyncExtensions.SqlDialect = new MySqlDialect(); - services.AddScoped(); + services.AddScoped(); } private void ConfigureBrighter(IServiceCollection services) { - services.AddSingleton(new DbConnectionStringProvider(DbConnectionString())); + var configuration = new RelationalDatabaseConfiguration( + DbConnectionString(), + outBoxTableName:_outBoxTableName + ); + services.AddSingleton(configuration); services.AddBrighter(options => { diff --git a/samples/WebAPI_Dapper/SalutationAnalytics/Program.cs b/samples/WebAPI_Dapper/SalutationAnalytics/Program.cs index 2e14e8976d..be479e73e4 100644 --- a/samples/WebAPI_Dapper/SalutationAnalytics/Program.cs +++ b/samples/WebAPI_Dapper/SalutationAnalytics/Program.cs @@ -9,7 +9,6 @@ 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.MySql; @@ -21,7 +20,6 @@ using SalutationPorts.EntityMappers; using SalutationPorts.Policies; using SalutationPorts.Requests; -using Salutations_SqliteMigrations.Migrations; namespace SalutationAnalytics { @@ -84,6 +82,12 @@ private static void ConfigureBrighter(HostBuilderContext hostContext, IServiceCo var rmqMessageConsumerFactory = new RmqMessageConsumerFactory(rmqConnection); + var dbConfiguration = new RelationalDatabaseConfiguration( + DbConnectionString(hostContext), + inboxTableName:SchemaCreation.INBOX_TABLE_NAME); + + services.AddSingleton(dbConfiguration); + services.AddServiceActivator(options => { options.Subscriptions = subscriptions; @@ -116,7 +120,7 @@ private static void ConfigureBrighter(HostBuilderContext hostContext, IServiceCo ) .AutoFromAssemblies() .UseExternalInbox( - ConfigureInbox(hostContext), + CreateInbox(hostContext, dbConfiguration), new InboxConfiguration( scope: InboxScope.Commands, onceOnly: true, @@ -153,7 +157,6 @@ private static void ConfigureMigration(HostBuilderContext hostBuilderContext, IS 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 }); @@ -178,25 +181,25 @@ private static void ConfigureDapperSqlite(IServiceCollection services) { DapperExtensions.DapperExtensions.SqlDialect = new SqliteDialect(); DapperAsyncExtensions.SqlDialect = new SqliteDialect(); - services.AddScoped(); + services.AddScoped(); } private static void ConfigureDapperMySql(IServiceCollection services) { DapperExtensions.DapperExtensions.SqlDialect = new MySqlDialect(); DapperAsyncExtensions.SqlDialect = new MySqlDialect(); - services.AddScoped(); + services.AddScoped(); } - private static IAmAnInbox ConfigureInbox(HostBuilderContext hostContext) + private static IAmAnInbox CreateInbox(HostBuilderContext hostContext, IAmARelationalDatabaseConfiguration configuration) { if (hostContext.HostingEnvironment.IsDevelopment()) { - return new SqliteInbox(new RelationalDatabaseConfiguration(DbConnectionString(hostContext), SchemaCreation.INBOX_TABLE_NAME)); + return new SqliteInbox(configuration); } - return new MySqlInbox(new RelationalDatabaseConfiguration(DbConnectionString(hostContext), SchemaCreation.INBOX_TABLE_NAME)); + return new MySqlInbox(configuration); } private static string DbConnectionString(HostBuilderContext hostContext) @@ -219,8 +222,7 @@ private static DatabaseType GetDatabaseType(HostBuilderContext hostContext) _ => throw new InvalidOperationException("Could not determine the database type") }; } - - + private static string GetEnvironment() { //NOTE: Hosting Context will always return Production outside of ASPNET_CORE at this point, so grab it directly diff --git a/samples/WebAPI_Dapper/SalutationAnalytics/SalutationAnalytics.csproj b/samples/WebAPI_Dapper/SalutationAnalytics/SalutationAnalytics.csproj index a59fc78023..a3ad0e6e99 100644 --- a/samples/WebAPI_Dapper/SalutationAnalytics/SalutationAnalytics.csproj +++ b/samples/WebAPI_Dapper/SalutationAnalytics/SalutationAnalytics.csproj @@ -9,13 +9,11 @@ - - diff --git a/samples/WebAPI_Dapper/SalutationPorts/Handlers/GreetingMadeHandler.cs b/samples/WebAPI_Dapper/SalutationPorts/Handlers/GreetingMadeHandler.cs index 4d9894b23d..18f3bda611 100644 --- a/samples/WebAPI_Dapper/SalutationPorts/Handlers/GreetingMadeHandler.cs +++ b/samples/WebAPI_Dapper/SalutationPorts/Handlers/GreetingMadeHandler.cs @@ -5,7 +5,6 @@ using DapperExtensions; using Microsoft.Extensions.Logging; using Paramore.Brighter; -using Paramore.Brighter.Dapper; using Paramore.Brighter.Logging.Attributes; using Paramore.Brighter.Policies.Attributes; using SalutationEntities; @@ -15,13 +14,13 @@ namespace SalutationPorts.Handlers { public class GreetingMadeHandlerAsync : RequestHandlerAsync { - private readonly IUnitOfWork _uow; + private readonly IAmATransactionConnectionProvider _transactionConnectionProvider; private readonly IAmACommandProcessor _postBox; private readonly ILogger _logger; - public GreetingMadeHandlerAsync(IUnitOfWork uow, IAmACommandProcessor postBox, ILogger logger) + public GreetingMadeHandlerAsync(IAmATransactionConnectionProvider transactionConnectionProvider, IAmACommandProcessor postBox, ILogger logger) { - _uow = uow; + _transactionConnectionProvider = transactionConnectionProvider; _postBox = postBox; _logger = logger; } @@ -33,12 +32,12 @@ public override async Task HandleAsync(GreetingMade @event, Cancel { var posts = new List(); - var tx = await _uow.BeginOrGetTransactionAsync(cancellationToken); + var tx = await _transactionConnectionProvider.GetTransactionAsync(cancellationToken); try { var salutation = new Salutation(@event.Greeting); - await _uow.Database.InsertAsync(salutation, tx); + await _transactionConnectionProvider.GetConnection().InsertAsync(salutation, tx); posts.Add(await _postBox.DepositPostAsync(new SalutationReceived(DateTimeOffset.Now), cancellationToken: cancellationToken)); diff --git a/samples/WebAPI_Dapper/SalutationPorts/SalutationPorts.csproj b/samples/WebAPI_Dapper/SalutationPorts/SalutationPorts.csproj index e54eee1aa4..4658d4de3b 100644 --- a/samples/WebAPI_Dapper/SalutationPorts/SalutationPorts.csproj +++ b/samples/WebAPI_Dapper/SalutationPorts/SalutationPorts.csproj @@ -5,7 +5,6 @@ - diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/GreetingsPorts.csproj b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/GreetingsPorts.csproj index 61b428817b..564807e0b5 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/GreetingsPorts.csproj +++ b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/GreetingsPorts.csproj @@ -5,8 +5,6 @@ - - diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs index d43ae4f612..a0fb7c7e83 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs +++ b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs @@ -6,13 +6,11 @@ using DapperExtensions; using DapperExtensions.Predicate; 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 { diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/AddPersonHandlerAsync.cs b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/AddPersonHandlerAsync.cs index 21baf781e5..7934753bd9 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/AddPersonHandlerAsync.cs +++ b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/AddPersonHandlerAsync.cs @@ -4,7 +4,6 @@ using GreetingsEntities; using GreetingsPorts.Requests; using Paramore.Brighter; -using Paramore.Brighter.Dapper; using Paramore.Brighter.Logging.Attributes; using Paramore.Brighter.Policies.Attributes; @@ -12,18 +11,18 @@ namespace GreetingsPorts.Handlers { public class AddPersonHandlerAsync : RequestHandlerAsync { - private readonly IUnitOfWork _uow; + private readonly IAmATransactionConnectionProvider _transactionConnectionProvider; - public AddPersonHandlerAsync(IUnitOfWork uow) + public AddPersonHandlerAsync(IAmATransactionConnectionProvider transactionConnectionProvider) { - _uow = uow; + _transactionConnectionProvider = transactionConnectionProvider; } [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 _transactionConnectionProvider.GetConnection().InsertAsync(new Person(addPerson.Name)); return await base.HandleAsync(addPerson, cancellationToken); } diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/DeletePersonHandlerAsync.cs b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/DeletePersonHandlerAsync.cs index f3b300dcd7..3964d9f195 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/DeletePersonHandlerAsync.cs +++ b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/DeletePersonHandlerAsync.cs @@ -7,7 +7,6 @@ using GreetingsEntities; using GreetingsPorts.Requests; using Paramore.Brighter; -using Paramore.Brighter.Dapper; using Paramore.Brighter.Logging.Attributes; using Paramore.Brighter.Policies.Attributes; @@ -15,27 +14,27 @@ namespace GreetingsPorts.Handlers { public class DeletePersonHandlerAsync : RequestHandlerAsync { - private readonly IUnitOfWork _uow; + private readonly IAmATransactionConnectionProvider _transactionConnectionProvider; - public DeletePersonHandlerAsync(IUnitOfWork uow) + public DeletePersonHandlerAsync(IAmATransactionConnectionProvider transactionConnectionProvider) { - _uow = uow; + _transactionConnectionProvider = transactionConnectionProvider; } [RequestLoggingAsync(0, HandlerTiming.Before)] [UsePolicyAsync(step:1, policy: Policies.Retry.EXPONENTIAL_RETRYPOLICYASYNC)] public async override Task HandleAsync(DeletePerson deletePerson, CancellationToken cancellationToken = default) { - var tx = await _uow.BeginOrGetTransactionAsync(cancellationToken); + var tx = await _transactionConnectionProvider.GetTransactionAsync(cancellationToken); try { var searchbyName = Predicates.Field(p => p.Name, Operator.Eq, deletePerson.Name); - var people = await _uow.Database.GetListAsync(searchbyName, transaction: tx); + var people = await _transactionConnectionProvider.GetConnection().GetListAsync(searchbyName, transaction: tx); var person = people.Single(); var deleteById = Predicates.Field(g => g.RecipientId, Operator.Eq, person.Id); - await _uow.Database.DeleteAsync(deleteById, tx); + await _transactionConnectionProvider.GetConnection().DeleteAsync(deleteById, tx); await tx.CommitAsync(cancellationToken); } diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/FIndGreetingsForPersonHandlerAsync.cs b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/FIndGreetingsForPersonHandlerAsync.cs index d908df5c2c..9fd4385c36 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/FIndGreetingsForPersonHandlerAsync.cs +++ b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/FIndGreetingsForPersonHandlerAsync.cs @@ -6,7 +6,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 +15,11 @@ namespace GreetingsPorts.Handlers { public class FIndGreetingsForPersonHandlerAsync : QueryHandlerAsync { - private readonly IUnitOfWork _uow; + private readonly IAmATransactionConnectionProvider _transactionConnectionProvider; - public FIndGreetingsForPersonHandlerAsync(IUnitOfWork uow) + public FIndGreetingsForPersonHandlerAsync(IAmATransactionConnectionProvider transactionConnectionProvider) { - _uow = uow; + _transactionConnectionProvider = transactionConnectionProvider; } [QueryLogging(0)] @@ -33,7 +33,7 @@ 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) => + var people = await _transactionConnectionProvider.GetConnection().QueryAsync(sql, (person, greeting) => { person.Greetings.Add(greeting); return person; diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/FindPersonByNameHandlerAsync.cs b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/FindPersonByNameHandlerAsync.cs index 1ab5898541..aace04b21c 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/FindPersonByNameHandlerAsync.cs +++ b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/FindPersonByNameHandlerAsync.cs @@ -7,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; @@ -16,11 +16,11 @@ namespace GreetingsPorts.Handlers { public class FindPersonByNameHandlerAsync : QueryHandlerAsync { - private readonly IUnitOfWork _uow; + private readonly IAmATransactionConnectionProvider _transactionConnectionProvider; - public FindPersonByNameHandlerAsync(IUnitOfWork uow) + public FindPersonByNameHandlerAsync(IAmATransactionConnectionProvider transactionConnectionProvider) { - _uow = uow; + _transactionConnectionProvider = transactionConnectionProvider; } [QueryLogging(0)] @@ -28,7 +28,7 @@ public FindPersonByNameHandlerAsync(IUnitOfWork uow) 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 people = await _transactionConnectionProvider.GetConnection().GetListAsync(searchbyName); var person = people.Single(); return new FindPersonResult(person); diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/OutboxExtensions.cs b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/OutboxExtensions.cs index 62e7710a79..cbb62c424f 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/OutboxExtensions.cs +++ b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/OutboxExtensions.cs @@ -6,63 +6,67 @@ using Paramore.Brighter.Extensions.DependencyInjection; using Paramore.Brighter.Extensions.Hosting; using Paramore.Brighter.MySql; -using Paramore.Brighter.MySql.Dapper; using Paramore.Brighter.Outbox.MySql; using Paramore.Brighter.Outbox.Sqlite; 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, - RelationalDatabaseConfiguration configuration) + + public static class OutboxExtensions { - if (env.IsDevelopment()) - { - AddSqliteOutBox(brighterBuilder, configuration); - } - else + public static IBrighterBuilder AddOutbox( + this IBrighterBuilder brighterBuilder, + IWebHostEnvironment env, + DatabaseType databaseType, + RelationalDatabaseConfiguration configuration) { - switch (databaseType) + if (env.IsDevelopment()) { - case DatabaseType.MySql: - AddMySqlOutbox(brighterBuilder, configuration); - break; - default: - throw new InvalidOperationException("Unknown Db type for Outbox configuration"); + AddSqliteOutBox(brighterBuilder, configuration); } + else + { + switch (databaseType) + { + case DatabaseType.MySql: + AddMySqlOutbox(brighterBuilder, configuration); + break; + default: + throw new InvalidOperationException("Unknown Db type for Outbox configuration"); + } + } + + return brighterBuilder; } - return brighterBuilder; - } - private static void AddMySqlOutbox(IBrighterBuilder brighterBuilder, RelationalDatabaseConfiguration configuration) - { - brighterBuilder.UseMySqlOutbox( - //new MySqlConfiguration(dbConnectionString, outBoxTableName, binaryMessagePayload: binaryMessagePayload), - configuration, - typeof(MySqlConnectionProvider), - ServiceLifetime.Singleton) - .UseMySqTransactionConnectionProvider(typeof(Paramore.Brighter.MySql.Dapper.MySqlDapperConnectionProvider), ServiceLifetime.Scoped) - .UseOutboxSweeper(); - } + private static void AddMySqlOutbox(IBrighterBuilder brighterBuilder, + RelationalDatabaseConfiguration configuration) + { + brighterBuilder.UseMySqlOutbox( + //new MySqlConfiguration(dbConnectionString, outBoxTableName, binaryMessagePayload: binaryMessagePayload), + configuration, + typeof(MySqlConnectionProvider), + ServiceLifetime.Singleton) + .UseMySqTransactionConnectionProvider( + typeof(Paramore.Brighter.MySql.MySqlConnectionProvider), ServiceLifetime.Scoped) + .UseOutboxSweeper(); + } - private static void AddSqliteOutBox(IBrighterBuilder brighterBuilder, RelationalDatabaseConfiguration configuration) - { - brighterBuilder.UseSqliteOutbox( - configuration, - typeof(SqliteConnectionProvider), - ServiceLifetime.Singleton) - .UseSqliteTransactionConnectionProvider(typeof(Paramore.Brighter.Sqlite.Dapper.SqliteDapperConnectionProvider), ServiceLifetime.Scoped) - .UseOutboxSweeper(options => - { - options.TimerInterval = 5; - options.MinimumMessageAge = 5000; - }); + private static void AddSqliteOutBox(IBrighterBuilder brighterBuilder, + RelationalDatabaseConfiguration configuration) + { + brighterBuilder.UseSqliteOutbox( + configuration, + typeof(SqliteConnectionProvider), + ServiceLifetime.Singleton) + .UseSqliteTransactionConnectionProvider( + typeof(Paramore.Brighter.Sqlite.SqliteConnectionProvider), ServiceLifetime.Scoped) + .UseOutboxSweeper(options => + { + options.TimerInterval = 5; + options.MinimumMessageAge = 5000; + }); + } } } diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/GreetingsWeb.csproj b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/GreetingsWeb.csproj index e58ab21e39..e98e6f3cb1 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/GreetingsWeb.csproj +++ b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/GreetingsWeb.csproj @@ -17,10 +17,8 @@ - - diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs index aaf7574b9b..bedc412c60 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs +++ b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs @@ -17,11 +17,8 @@ using Microsoft.Extensions.Hosting; using Microsoft.OpenApi.Models; using Paramore.Brighter; -using Paramore.Brighter.Dapper; using Paramore.Brighter.Extensions.DependencyInjection; using Paramore.Brighter.MessagingGateway.Kafka; -using Paramore.Brighter.MySql; -using Paramore.Brighter.Sqlite; using Paramore.Darker.AspNetCore; using Paramore.Darker.Policies; using Paramore.Darker.QueryLogging; @@ -123,7 +120,11 @@ private void ConfigureMySql(IServiceCollection services) private void ConfigureDapper(IServiceCollection services) { - services.AddSingleton(new DbConnectionStringProvider(DbConnectionString())); + var configuration = new RelationalDatabaseConfiguration( + DbConnectionString(), + outBoxTableName:_outBoxTableName + ); + services.AddSingleton(configuration); ConfigureDapperByHost(GetDatabaseType(), services); @@ -150,28 +151,29 @@ private static void ConfigureDapperSqlite(IServiceCollection services) { DapperExtensions.DapperExtensions.SqlDialect = new SqliteDialect(); DapperAsyncExtensions.SqlDialect = new SqliteDialect(); - services.AddScoped(); + services.AddScoped(); } private static void ConfigureDapperMySql(IServiceCollection services) { DapperExtensions.DapperExtensions.SqlDialect = new MySqlDialect(); DapperAsyncExtensions.SqlDialect = new MySqlDialect(); - services.AddScoped(); + services.AddScoped(); } private void ConfigureBrighter(IServiceCollection services) { - services.AddSingleton(new DbConnectionStringProvider(DbConnectionString())); + var outboxConfiguration = new RelationalDatabaseConfiguration( + DbConnectionString(), + outBoxTableName:_outBoxTableName + ); + services.AddSingleton(outboxConfiguration); var schemaRegistryConfig = new SchemaRegistryConfig { Url = "http://localhost:8081"}; var cachedSchemaRegistryClient = new CachedSchemaRegistryClient(schemaRegistryConfig); services.AddSingleton(cachedSchemaRegistryClient); - var outboxConfiguration = GetOutboxConfiguration(); - services.AddSingleton(outboxConfiguration); - - var configuration = new KafkaMessagingGatewayConfiguration + var kafkaConfiguration = new KafkaMessagingGatewayConfiguration { Name = "paramore.brighter.greetingsender", BootStrapServers = new[] { "localhost:9092" } @@ -180,14 +182,14 @@ private void ConfigureBrighter(IServiceCollection services) { //we want to use scoped, so make sure everything understands that which needs to options.HandlerLifetime = ServiceLifetime.Scoped; - options.ChannelFactory = new ChannelFactory(new KafkaMessageConsumerFactory(configuration)); + options.ChannelFactory = new ChannelFactory(new KafkaMessageConsumerFactory(kafkaConfiguration)); options.CommandProcessorLifetime = ServiceLifetime.Scoped; options.MapperLifetime = ServiceLifetime.Singleton; options.PolicyRegistry = new GreetingsPolicy(); }) .UseExternalBus( new KafkaProducerRegistryFactory( - configuration, + kafkaConfiguration, new KafkaPublication[] { new KafkaPublication @@ -259,16 +261,5 @@ private string GetConnectionString(DatabaseType databaseType) _ => throw new InvalidOperationException("Could not determine the database type") }; } - - private RelationalDatabaseConfiguration GetOutboxConfiguration() - { - if (_env.IsDevelopment()) - { - return new RelationalDatabaseConfiguration(DbConnectionString(), _outBoxTableName, binaryMessagePayload: true); - } - - return new RelationalDatabaseConfiguration(DbConnectionString(), _outBoxTableName, binaryMessagePayload: true); - } - } } diff --git a/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Program.cs b/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Program.cs index 99ed618d26..d82769ef2a 100644 --- a/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Program.cs +++ b/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Program.cs @@ -11,7 +11,6 @@ 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.MySql; @@ -164,7 +163,6 @@ private static void ConfigureMigration(HostBuilderContext hostBuilderContext, IS 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 }); @@ -189,14 +187,14 @@ private static void ConfigureDapperSqlite(IServiceCollection services) { DapperExtensions.DapperExtensions.SqlDialect = new SqliteDialect(); DapperAsyncExtensions.SqlDialect = new SqliteDialect(); - services.AddScoped(); + services.AddScoped(); } private static void ConfigureDapperMySql(IServiceCollection services) { DapperExtensions.DapperExtensions.SqlDialect = new MySqlDialect(); DapperAsyncExtensions.SqlDialect = new MySqlDialect(); - services.AddScoped(); + services.AddScoped(); } diff --git a/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/SalutationAnalytics.csproj b/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/SalutationAnalytics.csproj index afb273e6ab..ff6d80eef8 100644 --- a/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/SalutationAnalytics.csproj +++ b/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/SalutationAnalytics.csproj @@ -9,13 +9,11 @@ - - diff --git a/samples/WebAPI_Dapper_Kafka/SalutationPorts/Handlers/GreetingMadeHandler.cs b/samples/WebAPI_Dapper_Kafka/SalutationPorts/Handlers/GreetingMadeHandler.cs index adc966d961..26d6fa2668 100644 --- a/samples/WebAPI_Dapper_Kafka/SalutationPorts/Handlers/GreetingMadeHandler.cs +++ b/samples/WebAPI_Dapper_Kafka/SalutationPorts/Handlers/GreetingMadeHandler.cs @@ -1,11 +1,8 @@ using System; using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; using DapperExtensions; using Microsoft.Extensions.Logging; using Paramore.Brighter; -using Paramore.Brighter.Dapper; using Paramore.Brighter.Logging.Attributes; using Paramore.Brighter.Policies.Attributes; using SalutationEntities; @@ -15,7 +12,7 @@ namespace SalutationPorts.Handlers { public class GreetingMadeHandler : RequestHandler { - private readonly IUnitOfWork _uow; + private readonly IAmATransactionConnectionProvider _transactionConnectionProvider; private readonly IAmACommandProcessor _postBox; private readonly ILogger _logger; @@ -28,9 +25,9 @@ public class GreetingMadeHandler : RequestHandler * Instead, rely on being able to partition your topic such that a single thread can handle the number of messages * arriving on that thread with an acceptable latency. */ - public GreetingMadeHandler(IUnitOfWork uow, IAmACommandProcessor postBox, ILogger logger) + public GreetingMadeHandler(IAmATransactionConnectionProvider transactionConnectionProvider, IAmACommandProcessor postBox, ILogger logger) { - _uow = uow; + _transactionConnectionProvider = transactionConnectionProvider; _postBox = postBox; _logger = logger; } @@ -41,13 +38,13 @@ public GreetingMadeHandler(IUnitOfWork uow, IAmACommandProcessor postBox, ILogge public override GreetingMade Handle(GreetingMade @event) { var posts = new List(); - - var tx = _uow.BeginOrGetTransaction(); + + var tx = _transactionConnectionProvider.GetTransaction(); try { var salutation = new Salutation(@event.Greeting); - _uow.Database.Insert(salutation, tx); + _transactionConnectionProvider.GetConnection().Insert(salutation, tx); posts.Add(_postBox.DepositPost(new SalutationReceived(DateTimeOffset.Now))); diff --git a/samples/WebAPI_Dapper_Kafka/SalutationPorts/SalutationPorts.csproj b/samples/WebAPI_Dapper_Kafka/SalutationPorts/SalutationPorts.csproj index e54eee1aa4..4658d4de3b 100644 --- a/samples/WebAPI_Dapper_Kafka/SalutationPorts/SalutationPorts.csproj +++ b/samples/WebAPI_Dapper_Kafka/SalutationPorts/SalutationPorts.csproj @@ -5,7 +5,6 @@ - diff --git a/samples/WebApiWithWorkerAndSweeper/Orders.Sweeper/Extensions/BrighterExtensions.cs b/samples/WebApiWithWorkerAndSweeper/Orders.Sweeper/Extensions/BrighterExtensions.cs index 703b317dc0..cd9de28917 100644 --- a/samples/WebApiWithWorkerAndSweeper/Orders.Sweeper/Extensions/BrighterExtensions.cs +++ b/samples/WebApiWithWorkerAndSweeper/Orders.Sweeper/Extensions/BrighterExtensions.cs @@ -39,11 +39,11 @@ public static WebApplicationBuilder AddBrighter(this WebApplicationBuilder build { if (environmentName != null && environmentName.Equals(_developmentEnvironemntName, StringComparison.InvariantCultureIgnoreCase)) { - outboxType = typeof(MsSqlVisualStudioConnectonProvider); + outboxType = typeof(MsSqlVisualStudioConnectionProvider); } else { - outboxType = typeof(MsSqlDefaultAzureConnectonProvider); + outboxType = typeof(MsSqlDefaultAzureConnectionProvider); } } else diff --git a/src/Paramore.Brighter.Dapper/IUnitOfWork.cs b/src/Paramore.Brighter.Dapper/IUnitOfWork.cs deleted file mode 100644 index 9d3adc12d8..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 : IAmABoxTransactionProvider, 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.Extensions.DependencyInjection/ServiceCollectionExtensions.cs b/src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionExtensions.cs index c57d9372ce..1e01519b96 100644 --- a/src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionExtensions.cs @@ -293,7 +293,7 @@ private static CommandProcessor BuildCommandProcessor(IServiceProvider provider) var outbox = provider.GetService>(); var asyncOutbox = provider.GetService>(); - var overridingConnectionProvider = provider.GetService(); + var overridingConnectionProvider = provider.GetService(); if (outbox == null) outbox = new InMemoryOutbox(); if (asyncOutbox == null) asyncOutbox = new InMemoryOutbox(); @@ -347,7 +347,7 @@ private static INeedARequestContext AddExternalBusMaybe( MessageMapperRegistry messageMapperRegistry, InboxConfiguration inboxConfiguration, IAmAnOutboxSync outbox, - IAmATransactionConnectonProvider overridingProvider, + IAmATransactionConnectionProvider overridingProvider, IUseRpc useRequestResponse, int outboxBulkChunkSize, IAmAMessageTransformerFactory transformerFactory) diff --git a/src/Paramore.Brighter.Inbox.MsSql/MsSqlInbox.cs b/src/Paramore.Brighter.Inbox.MsSql/MsSqlInbox.cs index e93ba8d897..b9f3836713 100644 --- a/src/Paramore.Brighter.Inbox.MsSql/MsSqlInbox.cs +++ b/src/Paramore.Brighter.Inbox.MsSql/MsSqlInbox.cs @@ -46,15 +46,15 @@ public class MsSqlInbox : IAmAnInboxSync, IAmAnInboxAsync private const int MsSqlDuplicateKeyError_UniqueIndexViolation = 2601; private const int MsSqlDuplicateKeyError_UniqueConstraintViolation = 2627; - private readonly RelationalDatabaseConfiguration _configuration; - private readonly RelationalDbConnectionProvider _connectionProvider; + private readonly IAmARelationalDatabaseConfiguration _configuration; + private readonly IAmARelationalDbConnectionProvider _connectionProvider; /// /// Initializes a new instance of the class. /// /// The configuration. /// The Connection Provider. - public MsSqlInbox(RelationalDatabaseConfiguration configuration, RelationalDbConnectionProvider connectionProvider) + public MsSqlInbox(IAmARelationalDatabaseConfiguration configuration, IAmARelationalDbConnectionProvider connectionProvider) { _configuration = configuration; ContinueOnCapturedContext = false; @@ -65,7 +65,7 @@ public MsSqlInbox(RelationalDatabaseConfiguration configuration, RelationalDbCon /// Initializes a new instance of the class. /// /// The configuration. - public MsSqlInbox(RelationalDatabaseConfiguration configuration) : this(configuration, + public MsSqlInbox(IAmARelationalDatabaseConfiguration configuration) : this(configuration, new MsSqlSqlAuthConnectionProvider(configuration)) { } diff --git a/src/Paramore.Brighter.Inbox.MySql/MySqlInbox.cs b/src/Paramore.Brighter.Inbox.MySql/MySqlInbox.cs index c40ca8b2ed..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 RelationalDatabaseConfiguration _configuration; + private readonly IAmARelationalDatabaseConfiguration _configuration; /// /// Initializes a new instance of the class. /// /// The configuration. - public MySqlInbox(RelationalDatabaseConfiguration configuration) + public MySqlInbox(IAmARelationalDatabaseConfiguration configuration) { _configuration = configuration; ContinueOnCapturedContext = false; diff --git a/src/Paramore.Brighter.Inbox.Postgres/PostgresSqlInbox.cs b/src/Paramore.Brighter.Inbox.Postgres/PostgresSqlInbox.cs index ac40fd8b50..9306308197 100644 --- a/src/Paramore.Brighter.Inbox.Postgres/PostgresSqlInbox.cs +++ b/src/Paramore.Brighter.Inbox.Postgres/PostgresSqlInbox.cs @@ -40,7 +40,7 @@ namespace Paramore.Brighter.Inbox.Postgres { public class PostgresSqlInbox : IAmAnInboxSync, IAmAnInboxAsync { - private readonly RelationalDatabaseConfiguration _configuration; + private readonly IAmARelationalDatabaseConfiguration _configuration; private readonly IAmARelationalDbConnectionProvider _connectionProvider; private static readonly ILogger s_logger = ApplicationLogging.CreateLogger(); /// @@ -53,7 +53,7 @@ public class PostgresSqlInbox : IAmAnInboxSync, IAmAnInboxAsync /// public bool ContinueOnCapturedContext { get; set; } - public PostgresSqlInbox(RelationalDatabaseConfiguration configuration, IAmARelationalDbConnectionProvider connectionProvider = null) + public PostgresSqlInbox(IAmARelationalDatabaseConfiguration configuration, IAmARelationalDbConnectionProvider connectionProvider = null) { _configuration = configuration; _connectionProvider = connectionProvider; diff --git a/src/Paramore.Brighter.Inbox.Sqlite/SqliteInbox.cs b/src/Paramore.Brighter.Inbox.Sqlite/SqliteInbox.cs index 5411e30547..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(RelationalDatabaseConfiguration 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 RelationalDatabaseConfiguration Configuration { get; } + public IAmARelationalDatabaseConfiguration Configuration { get; } public string OutboxTableName => Configuration.InBoxTableName; diff --git a/src/Paramore.Brighter.MsSql.Azure/MsSqlAzureConnectonProviderBase.cs b/src/Paramore.Brighter.MsSql.Azure/MsSqlAzureConnectionProviderBase.cs similarity index 90% rename from src/Paramore.Brighter.MsSql.Azure/MsSqlAzureConnectonProviderBase.cs rename to src/Paramore.Brighter.MsSql.Azure/MsSqlAzureConnectionProviderBase.cs index 71256efafb..3def5702f4 100644 --- a/src/Paramore.Brighter.MsSql.Azure/MsSqlAzureConnectonProviderBase.cs +++ b/src/Paramore.Brighter.MsSql.Azure/MsSqlAzureConnectionProviderBase.cs @@ -8,7 +8,7 @@ namespace Paramore.Brighter.MsSql.Azure { - public abstract class MsSqlAzureConnectonProviderBase : IAmATransactionConnectonProvider + public abstract class MsSqlAzureConnectionProviderBase : IAmATransactionConnectionProvider { private readonly bool _cacheTokens; private const string _azureScope = "https://database.windows.net/.default"; @@ -25,7 +25,7 @@ public abstract class MsSqlAzureConnectonProviderBase : IAmATransactionConnecton /// /// Ms Sql Configuration. /// Cache Access Tokens until they have less than 5 minutes of life left. - protected MsSqlAzureConnectonProviderBase(RelationalDatabaseConfiguration configuration, bool cacheTokens = true) + protected MsSqlAzureConnectionProviderBase(RelationalDatabaseConfiguration configuration, bool cacheTokens = true) { _cacheTokens = cacheTokens; _connectionString = configuration.ConnectionString; @@ -103,6 +103,11 @@ public DbTransaction GetTransaction() return null; } + public Task GetTransactionAsync(CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + 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 6cc615db30..5549043200 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 ServiceBusChainedClientConnectonProvider : MsSqlAzureConnectonProviderBase + public class ServiceBusChainedClientConnectionProvider : MsSqlAzureConnectionProviderBase { private readonly ChainedTokenCredential _credential; @@ -15,7 +15,7 @@ public class ServiceBusChainedClientConnectonProvider : MsSqlAzureConnectonProvi /// /// Ms Sql Configuration /// List of Token Providers to use when trying to obtain a token. - public ServiceBusChainedClientConnectonProvider(RelationalDatabaseConfiguration configuration, + public ServiceBusChainedClientConnectionProvider(RelationalDatabaseConfiguration configuration, params TokenCredential[] credentialSources) : base(configuration) { if (credentialSources == null || credentialSources.Length < 1) diff --git a/src/Paramore.Brighter.MsSql.Azure/MsSqlDefaultAzureConnectonProvider.cs b/src/Paramore.Brighter.MsSql.Azure/MsSqlDefaultAzureConnectionProvider.cs similarity index 81% rename from src/Paramore.Brighter.MsSql.Azure/MsSqlDefaultAzureConnectonProvider.cs rename to src/Paramore.Brighter.MsSql.Azure/MsSqlDefaultAzureConnectionProvider.cs index e02773a2e6..07993e598c 100644 --- a/src/Paramore.Brighter.MsSql.Azure/MsSqlDefaultAzureConnectonProvider.cs +++ b/src/Paramore.Brighter.MsSql.Azure/MsSqlDefaultAzureConnectionProvider.cs @@ -5,13 +5,13 @@ namespace Paramore.Brighter.MsSql.Azure { - public class MsSqlDefaultAzureConnectonProvider : MsSqlAzureConnectonProviderBase + public class MsSqlDefaultAzureConnectionProvider : MsSqlAzureConnectionProviderBase { /// /// Initialise a new instance of Ms Sql Connection provider using Default Azure Credentials to acquire Access Tokens. /// /// Ms Sql Configuration - public MsSqlDefaultAzureConnectonProvider(RelationalDatabaseConfiguration configuration) : base(configuration) { } + public MsSqlDefaultAzureConnectionProvider(RelationalDatabaseConfiguration configuration) : base(configuration) { } protected override AccessToken GetAccessTokenFromProvider() { diff --git a/src/Paramore.Brighter.MsSql.Azure/MsSqlManagedIdentityConnectonProvider.cs b/src/Paramore.Brighter.MsSql.Azure/MsSqlManagedIdentityConnectionProvider.cs similarity index 81% rename from src/Paramore.Brighter.MsSql.Azure/MsSqlManagedIdentityConnectonProvider.cs rename to src/Paramore.Brighter.MsSql.Azure/MsSqlManagedIdentityConnectionProvider.cs index f1308a7031..85d589e6e3 100644 --- a/src/Paramore.Brighter.MsSql.Azure/MsSqlManagedIdentityConnectonProvider.cs +++ b/src/Paramore.Brighter.MsSql.Azure/MsSqlManagedIdentityConnectionProvider.cs @@ -5,13 +5,13 @@ namespace Paramore.Brighter.MsSql.Azure { - public class MsSqlManagedIdentityConnectonProvider : MsSqlAzureConnectonProviderBase + public class MsSqlManagedIdentityConnectionProvider : MsSqlAzureConnectionProviderBase { /// /// Initialise a new instance of Ms Sql Connection provider using Managed Identity Credentials to acquire Access Tokens. /// /// Ms Sql Configuration - public MsSqlManagedIdentityConnectonProvider(RelationalDatabaseConfiguration configuration) : base(configuration) + public MsSqlManagedIdentityConnectionProvider(RelationalDatabaseConfiguration configuration) : base(configuration) { } diff --git a/src/Paramore.Brighter.MsSql.Azure/MsSqlSharedTokenCacheConnectonProvider.cs b/src/Paramore.Brighter.MsSql.Azure/MsSqlSharedTokenCacheConnectionProvider.cs similarity index 84% rename from src/Paramore.Brighter.MsSql.Azure/MsSqlSharedTokenCacheConnectonProvider.cs rename to src/Paramore.Brighter.MsSql.Azure/MsSqlSharedTokenCacheConnectionProvider.cs index e5e170012a..b12c0344df 100644 --- a/src/Paramore.Brighter.MsSql.Azure/MsSqlSharedTokenCacheConnectonProvider.cs +++ b/src/Paramore.Brighter.MsSql.Azure/MsSqlSharedTokenCacheConnectionProvider.cs @@ -6,7 +6,7 @@ namespace Paramore.Brighter.MsSql.Azure { - public class MsSqlSharedTokenCacheConnectonProvider : MsSqlAzureConnectonProviderBase + public class MsSqlSharedTokenCacheConnectionProvider : MsSqlAzureConnectionProviderBase { private const string _azureUserNameKey = "AZURE_USERNAME"; private const string _azureTenantIdKey = "AZURE_TENANT_ID"; @@ -18,7 +18,7 @@ public class MsSqlSharedTokenCacheConnectonProvider : MsSqlAzureConnectonProvide /// Initialise a new instance of Ms Sql Connection provider using Shared Token Cache Credentials to acquire Access Tokens. /// /// Ms Sql Configuration - public MsSqlSharedTokenCacheConnectonProvider(RelationalDatabaseConfiguration configuration) : base(configuration) + public MsSqlSharedTokenCacheConnectionProvider(RelationalDatabaseConfiguration configuration) : base(configuration) { _azureUserName = Environment.GetEnvironmentVariable(_azureUserNameKey); _azureTenantId = Environment.GetEnvironmentVariable(_azureTenantIdKey); @@ -28,7 +28,7 @@ public MsSqlSharedTokenCacheConnectonProvider(RelationalDatabaseConfiguration co /// Initialise a new instance of Ms Sql Connection provider using Shared Token Cache Credentials to acquire Access Tokens. /// /// Ms Sql Configuration - public MsSqlSharedTokenCacheConnectonProvider(RelationalDatabaseConfiguration configuration, string userName, string tenantId) : base(configuration) + public MsSqlSharedTokenCacheConnectionProvider(RelationalDatabaseConfiguration configuration, string userName, string tenantId) : base(configuration) { _azureUserName = userName; _azureTenantId = tenantId; diff --git a/src/Paramore.Brighter.MsSql.Azure/MsSqlVisualStudioConnectonProvider.cs b/src/Paramore.Brighter.MsSql.Azure/MsSqlVisualStudioConnectionProvider.cs similarity index 81% rename from src/Paramore.Brighter.MsSql.Azure/MsSqlVisualStudioConnectonProvider.cs rename to src/Paramore.Brighter.MsSql.Azure/MsSqlVisualStudioConnectionProvider.cs index 81944f8d9f..135552487b 100644 --- a/src/Paramore.Brighter.MsSql.Azure/MsSqlVisualStudioConnectonProvider.cs +++ b/src/Paramore.Brighter.MsSql.Azure/MsSqlVisualStudioConnectionProvider.cs @@ -5,13 +5,13 @@ namespace Paramore.Brighter.MsSql.Azure { - public class MsSqlVisualStudioConnectonProvider : MsSqlAzureConnectonProviderBase + public class MsSqlVisualStudioConnectionProvider : MsSqlAzureConnectionProviderBase { /// /// Initialise a new instance of Ms Sql Connection provider using Visual Studio Credentials to acquire Access Tokens. /// /// Ms Sql Configuration - public MsSqlVisualStudioConnectonProvider(RelationalDatabaseConfiguration configuration) : base(configuration) + public MsSqlVisualStudioConnectionProvider(RelationalDatabaseConfiguration configuration) : base(configuration) { } diff --git a/src/Paramore.Brighter.MsSql.Dapper/MsSqlDapperConnectionProvider.cs b/src/Paramore.Brighter.MsSql.Dapper/MsSqlDapperConnectionProvider.cs deleted file mode 100644 index c657a3b74f..0000000000 --- a/src/Paramore.Brighter.MsSql.Dapper/MsSqlDapperConnectionProvider.cs +++ /dev/null @@ -1,49 +0,0 @@ -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 MsSqlDapperConnectionProvider : IAmATransactionConnectonProvider - { - private readonly IUnitOfWork _unitOfWork; - - public MsSqlDapperConnectionProvider(IUnitOfWork unitOfWork) - { - _unitOfWork = unitOfWork; - } - - public DbConnection GetConnection() - { - return (SqlConnection)_unitOfWork.Database; - } - - public Task GetConnectionAsync(CancellationToken cancellationToken = default) - { - var tcs = new TaskCompletionSource(); - tcs.SetResult(GetConnection()); - return tcs.Task; - } - - public DbTransaction 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 deleted file mode 100644 index ca41a8bcf2..0000000000 --- a/src/Paramore.Brighter.MsSql.Dapper/Paramore.Brighter.MsSql.Dapper.csproj +++ /dev/null @@ -1,22 +0,0 @@ - - - - netstandard2.0;net6.0 - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - 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/MsSqlEntityFrameworkCoreConnectonProvider.cs b/src/Paramore.Brighter.MsSql.EntityFrameworkCore/MsSqlEntityFrameworkCoreConnectionProvider.cs similarity index 88% rename from src/Paramore.Brighter.MsSql.EntityFrameworkCore/MsSqlEntityFrameworkCoreConnectonProvider.cs rename to src/Paramore.Brighter.MsSql.EntityFrameworkCore/MsSqlEntityFrameworkCoreConnectionProvider.cs index b9e9c69a75..9e16b88c0a 100644 --- a/src/Paramore.Brighter.MsSql.EntityFrameworkCore/MsSqlEntityFrameworkCoreConnectonProvider.cs +++ b/src/Paramore.Brighter.MsSql.EntityFrameworkCore/MsSqlEntityFrameworkCoreConnectionProvider.cs @@ -7,14 +7,14 @@ namespace Paramore.Brighter.MsSql.EntityFrameworkCore { - public class MsSqlEntityFrameworkCoreConnectonProvider : RelationalDbConnectionProvider, IAmATransactionConnectonProvider where T : DbContext + public class MsSqlEntityFrameworkCoreConnectionProvider : RelationalDbConnectionProvider, IAmATransactionConnectionProvider where T : DbContext { private readonly T _context; /// /// Initialise a new instance of Ms Sql Connection provider using the Database Connection from an Entity Framework Core DbContext. /// - public MsSqlEntityFrameworkCoreConnectonProvider(T context) + public MsSqlEntityFrameworkCoreConnectionProvider(T context) { _context = context; } diff --git a/src/Paramore.Brighter.MsSql/MsSqlSqlAuthConnectionProvider.cs b/src/Paramore.Brighter.MsSql/MsSqlSqlAuthConnectionProvider.cs index c927ccff1b..1bb472560f 100644 --- a/src/Paramore.Brighter.MsSql/MsSqlSqlAuthConnectionProvider.cs +++ b/src/Paramore.Brighter.MsSql/MsSqlSqlAuthConnectionProvider.cs @@ -1,24 +1,27 @@ -using System.Data.Common; +using System; +using System.Data.Common; using Microsoft.Data.SqlClient; namespace Paramore.Brighter.MsSql { - public class MsSqlSqlAuthConnectionProvider : RelationalDbConnectionProvider + /// + /// A connection provider for Sqlite + /// + public class MsSqlSqlAuthConnectionProvider : RelationalDbConnectionProvider, IAmATransactionConnectionProvider { private readonly string _connectionString; - + /// - /// Initialise a new instance of Ms Sql Connection provider using Sql Authentication. + /// Create a connection provider for MSSQL using a connection string for Db access /// - /// Ms Sql Configuration - public MsSqlSqlAuthConnectionProvider(RelationalDatabaseConfiguration configuration) + /// The configuration for this database + public MsSqlSqlAuthConnectionProvider(IAmARelationalDatabaseConfiguration configuration) { + if (string.IsNullOrWhiteSpace(configuration?.ConnectionString)) + throw new ArgumentNullException(nameof(configuration.ConnectionString)); _connectionString = configuration.ConnectionString; } - public override DbConnection GetConnection() - { - return new SqlConnection(_connectionString); - } + public override DbConnection GetConnection() => Connection ?? (Connection = new SqlConnection(_connectionString)); } } diff --git a/src/Paramore.Brighter.MySql.Dapper/MySqlDapperConnectionProvider.cs b/src/Paramore.Brighter.MySql.Dapper/MySqlDapperConnectionProvider.cs deleted file mode 100644 index c2f4b101d6..0000000000 --- a/src/Paramore.Brighter.MySql.Dapper/MySqlDapperConnectionProvider.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System.Data.Common; -using System.Threading; -using System.Threading.Tasks; -using MySqlConnector; -using Paramore.Brighter.Dapper; - -namespace Paramore.Brighter.MySql.Dapper -{ - public class MySqlDapperConnectionProvider : IAmATransactionConnectonProvider - { - private readonly IUnitOfWork _unitOfWork; - - public MySqlDapperConnectionProvider(IUnitOfWork unitOfWork) - { - _unitOfWork = unitOfWork; - } - - public DbConnection GetConnection() - { - return (MySqlConnection)_unitOfWork.Database; - } - - public Task GetConnectionAsync(CancellationToken cancellationToken = default) - { - var tcs = new TaskCompletionSource(); - tcs.SetResult(GetConnection()); - return tcs.Task; - } - - public DbTransaction 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/Paramore.Brighter.MySql.Dapper.csproj b/src/Paramore.Brighter.MySql.Dapper/Paramore.Brighter.MySql.Dapper.csproj deleted file mode 100644 index 83bcf62d03..0000000000 --- a/src/Paramore.Brighter.MySql.Dapper/Paramore.Brighter.MySql.Dapper.csproj +++ /dev/null @@ -1,21 +0,0 @@ - - - - netstandard2.0;net6.0 - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - 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 266abb691a..c7eef2dd4a 100644 --- a/src/Paramore.Brighter.MySql.EntityFrameworkCore/MySqlEntityFrameworkConnectionProvider.cs +++ b/src/Paramore.Brighter.MySql.EntityFrameworkCore/MySqlEntityFrameworkConnectionProvider.cs @@ -11,7 +11,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 : RelationalDbConnectionProvider, IAmATransactionConnectonProvider where T: DbContext + public class MySqlEntityFrameworkConnectionProvider : RelationalDbConnectionProvider, IAmATransactionConnectionProvider where T: DbContext { private readonly T _context; diff --git a/src/Paramore.Brighter.MySql/MySqlConnectionProvider.cs b/src/Paramore.Brighter.MySql/MySqlConnectionProvider.cs index 010ec94bab..853fffa158 100644 --- a/src/Paramore.Brighter.MySql/MySqlConnectionProvider.cs +++ b/src/Paramore.Brighter.MySql/MySqlConnectionProvider.cs @@ -1,27 +1,29 @@ #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.Common; using MySqlConnector; @@ -30,22 +32,21 @@ namespace Paramore.Brighter.MySql /// /// A connection provider that uses the connection string to create a connection /// - public class MySqlConnectionProvider : RelationalDbConnectionProvider + public class MySqlConnectionProvider : RelationalDbConnectionProvider, IAmATransactionConnectionProvider { 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(RelationalDatabaseConfiguration configuration) + /// MySql Configuration + public MySqlConnectionProvider(IAmARelationalDatabaseConfiguration configuration) { + if (string.IsNullOrWhiteSpace(configuration?.ConnectionString)) + throw new ArgumentNullException(nameof(configuration.ConnectionString)); _connectionString = configuration.ConnectionString; } - public override DbConnection GetConnection() - { - return new MySqlConnection(_connectionString); - } + public override DbConnection GetConnection() => Connection ?? (Connection = new MySqlConnection(_connectionString)); } } diff --git a/src/Paramore.Brighter.Outbox.DynamoDB/DynamoDbOutbox.cs b/src/Paramore.Brighter.Outbox.DynamoDB/DynamoDbOutbox.cs index 805424a3a4..518642d87c 100644 --- a/src/Paramore.Brighter.Outbox.DynamoDB/DynamoDbOutbox.cs +++ b/src/Paramore.Brighter.Outbox.DynamoDB/DynamoDbOutbox.cs @@ -76,7 +76,7 @@ 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, IAmATransactionConnectonProvider transactionProvider = null) + public void Add(Message message, int outBoxTimeout = -1, IAmATransactionConnectionProvider transactionProvider = null) { AddAsync(message, outBoxTimeout).ConfigureAwait(ContinueOnCapturedContext).GetAwaiter().GetResult(); } @@ -92,7 +92,7 @@ public async Task AddAsync( Message message, int outBoxTimeout = -1, CancellationToken cancellationToken = default, - IAmATransactionConnectonProvider transactionProvider = null) + IAmATransactionConnectionProvider transactionProvider = null) { var messageToStore = new MessageItem(message); diff --git a/src/Paramore.Brighter.Outbox.EventStore/EventStoreOutboxSync.cs b/src/Paramore.Brighter.Outbox.EventStore/EventStoreOutboxSync.cs index 595eab2d89..f7b14167cd 100644 --- a/src/Paramore.Brighter.Outbox.EventStore/EventStoreOutboxSync.cs +++ b/src/Paramore.Brighter.Outbox.EventStore/EventStoreOutboxSync.cs @@ -73,7 +73,7 @@ public EventStoreOutboxSync(IEventStoreConnection eventStore) /// The message. /// The outBoxTimeout. /// Task. - public void Add(Message message, int outBoxTimeout = -1, IAmATransactionConnectonProvider transactionProvider = null) + public void Add(Message message, int outBoxTimeout = -1, IAmATransactionConnectionProvider transactionProvider = null) { s_logger.LogDebug("Adding message to Event Store Outbox: {Request}", JsonSerializer.Serialize(message, JsonSerialisationOptions.Options)); @@ -98,7 +98,7 @@ public async Task AddAsync( Message message, int outBoxTimeout = -1, CancellationToken cancellationToken = default, - IAmATransactionConnectonProvider transactionProvider = null + IAmATransactionConnectionProvider transactionProvider = null ) { s_logger.LogDebug("Adding message to Event Store Outbox: {Request}", JsonSerializer.Serialize(message, JsonSerialisationOptions.Options)); diff --git a/src/Paramore.Brighter.Outbox.MsSql/MsSqlOutbox.cs b/src/Paramore.Brighter.Outbox.MsSql/MsSqlOutbox.cs index f3737b7911..39a4ce6202 100644 --- a/src/Paramore.Brighter.Outbox.MsSql/MsSqlOutbox.cs +++ b/src/Paramore.Brighter.Outbox.MsSql/MsSqlOutbox.cs @@ -44,7 +44,7 @@ public class MsSqlOutbox : RelationDatabaseOutbox { private const int MsSqlDuplicateKeyError_UniqueIndexViolation = 2601; private const int MsSqlDuplicateKeyError_UniqueConstraintViolation = 2627; - private readonly RelationalDatabaseConfiguration _configuration; + private readonly IAmARelationalDatabaseConfiguration _configuration; private readonly IAmARelationalDbConnectionProvider _connectionProvider; /// @@ -52,7 +52,7 @@ public class MsSqlOutbox : RelationDatabaseOutbox /// /// The configuration. /// The connection factory. - public MsSqlOutbox(RelationalDatabaseConfiguration configuration, IAmARelationalDbConnectionProvider connectionProvider) : base( + public MsSqlOutbox(IAmARelationalDatabaseConfiguration configuration, IAmARelationalDbConnectionProvider connectionProvider) : base( configuration.OutBoxTableName, new MsSqlQueries(), ApplicationLogging.CreateLogger()) { _configuration = configuration; @@ -64,13 +64,13 @@ public MsSqlOutbox(RelationalDatabaseConfiguration configuration, IAmARelational /// Initializes a new instance of the class. /// /// The configuration. - public MsSqlOutbox(RelationalDatabaseConfiguration configuration) : this(configuration, + public MsSqlOutbox(IAmARelationalDatabaseConfiguration configuration) : this(configuration, new MsSqlSqlAuthConnectionProvider(configuration)) { } protected override void WriteToStore( - IAmATransactionConnectonProvider transactionProvider, + IAmATransactionConnectionProvider transactionProvider, Func commandFunc, Action loggingAction) { @@ -112,7 +112,7 @@ protected override void WriteToStore( } protected override async Task WriteToStoreAsync( - IAmATransactionConnectonProvider transactionProvider, + IAmATransactionConnectionProvider transactionProvider, Func commandFunc, Action loggingAction, CancellationToken cancellationToken) diff --git a/src/Paramore.Brighter.Outbox.MySql/MySqlOutbox.cs b/src/Paramore.Brighter.Outbox.MySql/MySqlOutbox.cs index 07731b5376..6bb9724eaf 100644 --- a/src/Paramore.Brighter.Outbox.MySql/MySqlOutbox.cs +++ b/src/Paramore.Brighter.Outbox.MySql/MySqlOutbox.cs @@ -45,10 +45,10 @@ public class MySqlOutbox : RelationDatabaseOutbox private static readonly ILogger s_logger = ApplicationLogging.CreateLogger(); private const int MySqlDuplicateKeyError = 1062; - private readonly RelationalDatabaseConfiguration _configuration; + private readonly IAmARelationalDatabaseConfiguration _configuration; private readonly IAmARelationalDbConnectionProvider _connectionProvider; - public MySqlOutbox(RelationalDatabaseConfiguration configuration, IAmARelationalDbConnectionProvider connectionProvider) + public MySqlOutbox(IAmARelationalDatabaseConfiguration configuration, IAmARelationalDbConnectionProvider connectionProvider) : base(configuration.OutBoxTableName, new MySqlQueries(), ApplicationLogging.CreateLogger()) { _configuration = configuration; @@ -56,13 +56,13 @@ public MySqlOutbox(RelationalDatabaseConfiguration configuration, IAmARelational ContinueOnCapturedContext = false; } - public MySqlOutbox(RelationalDatabaseConfiguration configuration) + public MySqlOutbox(IAmARelationalDatabaseConfiguration configuration) : this(configuration, new MySqlConnectionProvider(configuration)) { } protected override void WriteToStore( - IAmATransactionConnectonProvider transactionProvider, + IAmATransactionConnectionProvider transactionProvider, Func commandFunc, Action loggingAction ) @@ -105,7 +105,7 @@ Action loggingAction } protected override async Task WriteToStoreAsync( - IAmATransactionConnectonProvider transactionProvider, + IAmATransactionConnectionProvider transactionProvider, Func commandFunc, Action loggingAction, CancellationToken cancellationToken diff --git a/src/Paramore.Brighter.Outbox.MySql/ServiceCollectionExtensions.cs b/src/Paramore.Brighter.Outbox.MySql/ServiceCollectionExtensions.cs index 12d0f39bdf..290d26c1f4 100644 --- a/src/Paramore.Brighter.Outbox.MySql/ServiceCollectionExtensions.cs +++ b/src/Paramore.Brighter.Outbox.MySql/ServiceCollectionExtensions.cs @@ -17,7 +17,7 @@ public static class ServiceCollectionExtensions /// 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 + /// -- IAmARelationalDbConnectionProvider: 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 @@ -38,17 +38,19 @@ public static IBrighterBuilder UseMySqlOutbox( /// 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 type of the transaction 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) + this IBrighterBuilder brighterBuilder, + Type transactionProvider, + ServiceLifetime serviceLifetime = ServiceLifetime.Scoped + ) { - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmABoxTransactionProvider), connectionProvider, serviceLifetime)); + brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmABoxTransactionProvider), transactionProvider, serviceLifetime)); return brighterBuilder; } diff --git a/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutbox.cs b/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutbox.cs index 60a075993c..9997851709 100644 --- a/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutbox.cs +++ b/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutbox.cs @@ -42,12 +42,12 @@ public class PostgreSqlOutbox : RelationDatabaseOutbox { private static readonly ILogger s_logger = ApplicationLogging.CreateLogger(); - private readonly RelationalDatabaseConfiguration _configuration; + private readonly IAmARelationalDatabaseConfiguration _configuration; private readonly IAmARelationalDbConnectionProvider _connectionProvider; public PostgreSqlOutbox( - RelationalDatabaseConfiguration configuration, + IAmARelationalDatabaseConfiguration configuration, IAmARelationalDbConnectionProvider connectionProvider) : base( configuration.OutBoxTableName, new PostgreSqlQueries(), ApplicationLogging.CreateLogger()) { @@ -55,18 +55,18 @@ public PostgreSqlOutbox( _connectionProvider = connectionProvider; } - public PostgreSqlOutbox(RelationalDatabaseConfiguration configuration) + public PostgreSqlOutbox(IAmARelationalDatabaseConfiguration configuration) : this(configuration, new PostgreSqlNpgsqlConnectionProvider(configuration)) { } protected override void WriteToStore( - IAmATransactionConnectonProvider transactionConnectonProvider, + IAmATransactionConnectionProvider transactionConnectionProvider, Func commandFunc, Action loggingAction) { var connectionProvider = _connectionProvider; - if (transactionConnectonProvider != null) - connectionProvider = transactionConnectonProvider; + if (transactionConnectionProvider != null) + connectionProvider = transactionConnectionProvider; var connection = connectionProvider.GetConnection(); @@ -76,7 +76,7 @@ protected override void WriteToStore( { try { - if (transactionConnectonProvider != null && connectionProvider.HasOpenTransaction) + if (transactionConnectionProvider != null && connectionProvider.HasOpenTransaction) command.Transaction = connectionProvider.GetTransaction(); command.ExecuteNonQuery(); } @@ -101,7 +101,7 @@ protected override void WriteToStore( } protected override async Task WriteToStoreAsync( - IAmATransactionConnectonProvider transactionConnectionProvider, + IAmATransactionConnectionProvider transactionConnectionProvider, Func commandFunc, Action loggingAction, CancellationToken cancellationToken) diff --git a/src/Paramore.Brighter.Outbox.PostgreSql/ServiceCollectionExtensions.cs b/src/Paramore.Brighter.Outbox.PostgreSql/ServiceCollectionExtensions.cs index 469895721a..4793240dd0 100644 --- a/src/Paramore.Brighter.Outbox.PostgreSql/ServiceCollectionExtensions.cs +++ b/src/Paramore.Brighter.Outbox.PostgreSql/ServiceCollectionExtensions.cs @@ -51,8 +51,8 @@ public static IBrighterBuilder UsePostgreSqlTransactionConnectionProvider( if (connectionProvider is null) throw new ArgumentNullException($"{nameof(connectionProvider)} cannot be null.", nameof(connectionProvider)); - if (!typeof(IAmATransactionConnectonProvider).IsAssignableFrom(connectionProvider)) - throw new Exception($"Unable to register provider of type {connectionProvider.GetType().Name}. Class does not implement interface {nameof(IAmATransactionConnectonProvider)}."); + if (!typeof(IAmATransactionConnectionProvider).IsAssignableFrom(connectionProvider)) + throw new Exception($"Unable to register provider of type {connectionProvider.GetType().Name}. Class does not implement interface {nameof(IAmATransactionConnectionProvider)}."); brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmABoxTransactionProvider), connectionProvider, serviceLifetime)); diff --git a/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutbox.cs b/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutbox.cs index 1e4e7a47ba..2d274f2191 100644 --- a/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutbox.cs +++ b/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutbox.cs @@ -46,7 +46,7 @@ public class SqliteOutbox : RelationDatabaseOutbox private const int SqliteDuplicateKeyError = 1555; private const int SqliteUniqueKeyError = 19; - private readonly RelationalDatabaseConfiguration _configuration; + private readonly IAmARelationalDatabaseConfiguration _configuration; private readonly IAmARelationalDbConnectionProvider _connectionProvider; /// @@ -54,7 +54,7 @@ public class SqliteOutbox : RelationDatabaseOutbox /// /// The configuration to connect to this data store /// Provides a connection to the Db that allows us to enlist in an ambient transaction - public SqliteOutbox(RelationalDatabaseConfiguration configuration, IAmARelationalDbConnectionProvider connectionProvider) + public SqliteOutbox(IAmARelationalDatabaseConfiguration configuration, IAmARelationalDbConnectionProvider connectionProvider) : base(configuration.OutBoxTableName, new SqliteQueries(), ApplicationLogging.CreateLogger()) { _configuration = configuration; @@ -66,13 +66,13 @@ public SqliteOutbox(RelationalDatabaseConfiguration configuration, IAmARelationa /// Initializes a new instance of the class. /// /// The configuration to connect to this data store - public SqliteOutbox(RelationalDatabaseConfiguration configuration) + public SqliteOutbox(IAmARelationalDatabaseConfiguration configuration) : this(configuration, new SqliteConnectionProvider(configuration)) { } protected override void WriteToStore( - IAmATransactionConnectonProvider transactionProvider, + IAmATransactionConnectionProvider transactionProvider, Func commandFunc, Action loggingAction ) @@ -114,7 +114,7 @@ Action loggingAction } protected override async Task WriteToStoreAsync( - IAmATransactionConnectonProvider transactionConnectionProvider, + IAmATransactionConnectionProvider transactionConnectionProvider, Func commandFunc, Action loggingAction, CancellationToken cancellationToken) diff --git a/src/Paramore.Brighter.PostgreSql.EntityFrameworkCore/PostgreSqlEntityFrameworkConnectonProvider.cs b/src/Paramore.Brighter.PostgreSql.EntityFrameworkCore/PostgreSqlEntityFrameworkConnectionProvider.cs similarity index 90% rename from src/Paramore.Brighter.PostgreSql.EntityFrameworkCore/PostgreSqlEntityFrameworkConnectonProvider.cs rename to src/Paramore.Brighter.PostgreSql.EntityFrameworkCore/PostgreSqlEntityFrameworkConnectionProvider.cs index dad29315a2..3b6aeb9eff 100644 --- a/src/Paramore.Brighter.PostgreSql.EntityFrameworkCore/PostgreSqlEntityFrameworkConnectonProvider.cs +++ b/src/Paramore.Brighter.PostgreSql.EntityFrameworkCore/PostgreSqlEntityFrameworkConnectionProvider.cs @@ -12,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 PostgreSqlEntityFrameworkConnectonProvider : RelationalDbConnectionProvider, IAmATransactionConnectonProvider where T : DbContext + public class PostgreSqlEntityFrameworkConnectionProvider : RelationalDbConnectionProvider, IAmATransactionConnectionProvider where T : DbContext { private readonly T _context; @@ -20,7 +20,7 @@ public class PostgreSqlEntityFrameworkConnectonProvider : RelationalDbConnect /// Constructs and instance from a database context /// /// The database context to use - public PostgreSqlEntityFrameworkConnectonProvider(T context) + public PostgreSqlEntityFrameworkConnectionProvider(T context) { _context = context; } diff --git a/src/Paramore.Brighter.PostgreSql/PostgreSqlNpgsqlConnectionProvider.cs b/src/Paramore.Brighter.PostgreSql/PostgreSqlNpgsqlConnectionProvider.cs index 31d2a3c4b0..9d3c87f662 100644 --- a/src/Paramore.Brighter.PostgreSql/PostgreSqlNpgsqlConnectionProvider.cs +++ b/src/Paramore.Brighter.PostgreSql/PostgreSqlNpgsqlConnectionProvider.cs @@ -4,11 +4,18 @@ namespace Paramore.Brighter.PostgreSql { + /// + /// A connection provider that uses the connection string to create a connection + /// public class PostgreSqlNpgsqlConnectionProvider : RelationalDbConnectionProvider { private readonly string _connectionString; - public PostgreSqlNpgsqlConnectionProvider(RelationalDatabaseConfiguration configuration) + /// + /// Initialise a new instance of PostgreSQl Connection provider from a connection string + /// + /// PostgreSQL Configuration + public PostgreSqlNpgsqlConnectionProvider(IAmARelationalDatabaseConfiguration configuration) { if (string.IsNullOrWhiteSpace(configuration?.ConnectionString)) throw new ArgumentNullException(nameof(configuration.ConnectionString)); @@ -16,10 +23,6 @@ public PostgreSqlNpgsqlConnectionProvider(RelationalDatabaseConfiguration config _connectionString = configuration.ConnectionString; } - public override DbConnection GetConnection() - - { - return new NpgsqlConnection(_connectionString); - } + public override DbConnection GetConnection() => Connection ?? (Connection = new NpgsqlConnection(_connectionString)); } } diff --git a/src/Paramore.Brighter.ServiceActivator/ControlBusReceiverBuilder.cs b/src/Paramore.Brighter.ServiceActivator/ControlBusReceiverBuilder.cs index 2016334183..6f801273e3 100644 --- a/src/Paramore.Brighter.ServiceActivator/ControlBusReceiverBuilder.cs +++ b/src/Paramore.Brighter.ServiceActivator/ControlBusReceiverBuilder.cs @@ -185,7 +185,7 @@ public Dispatcher Build(string hostName) /// private class SinkOutboxSync : IAmAnOutboxSync { - public void Add(Message message, int outBoxTimeout = -1, IAmATransactionConnectonProvider transactionProvider = null) + public void Add(Message message, int outBoxTimeout = -1, IAmATransactionConnectionProvider transactionProvider = null) { //discard message } diff --git a/src/Paramore.Brighter.Sqlite.Dapper/Paramore.Brighter.Sqlite.Dapper.csproj b/src/Paramore.Brighter.Sqlite.Dapper/Paramore.Brighter.Sqlite.Dapper.csproj deleted file mode 100644 index 3bf7bbe37c..0000000000 --- a/src/Paramore.Brighter.Sqlite.Dapper/Paramore.Brighter.Sqlite.Dapper.csproj +++ /dev/null @@ -1,20 +0,0 @@ - - - - netstandard2.1;net6.0 - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - diff --git a/src/Paramore.Brighter.Sqlite.Dapper/SqliteDapperConnectionProvider.cs b/src/Paramore.Brighter.Sqlite.Dapper/SqliteDapperConnectionProvider.cs deleted file mode 100644 index 3c0e2bd51c..0000000000 --- a/src/Paramore.Brighter.Sqlite.Dapper/SqliteDapperConnectionProvider.cs +++ /dev/null @@ -1,43 +0,0 @@ -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 SqliteDapperConnectionProvider : RelationalDbConnectionProvider - { - private readonly IUnitOfWork _unitOfWork; - - public SqliteDapperConnectionProvider(IUnitOfWork unitOfWork) - { - _unitOfWork = unitOfWork; - } - - public override DbConnection GetConnection() - { - return (SqliteConnection)_unitOfWork.Database; - } - - public IDbTransaction GetTransaction() - { - return (DbTransaction)_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 11a3246e6a..bfe4977231 100644 --- a/src/Paramore.Brighter.Sqlite.EntityFrameworkCore/SqliteEntityFrameworkConnectionProvider.cs +++ b/src/Paramore.Brighter.Sqlite.EntityFrameworkCore/SqliteEntityFrameworkConnectionProvider.cs @@ -11,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 : RelationalDbConnectionProvider, IAmATransactionConnectonProvider where T: DbContext + public class SqliteEntityFrameworkConnectionProvider : RelationalDbConnectionProvider, IAmATransactionConnectionProvider where T: DbContext { private readonly T _context; diff --git a/src/Paramore.Brighter.Sqlite/SqliteConnectionProvider.cs b/src/Paramore.Brighter.Sqlite/SqliteConnectionProvider.cs index a1354c3136..c315378034 100644 --- a/src/Paramore.Brighter.Sqlite/SqliteConnectionProvider.cs +++ b/src/Paramore.Brighter.Sqlite/SqliteConnectionProvider.cs @@ -22,33 +22,30 @@ THE SOFTWARE. */ #endregion -using System.Data; +using System; using System.Data.Common; -using System.Threading; -using System.Threading.Tasks; using Microsoft.Data.Sqlite; namespace Paramore.Brighter.Sqlite { /// - /// A connection provider that uses the connection string to create a connection + /// A connection provider for Sqlite /// - public class SqliteConnectionProvider : RelationalDbConnectionProvider + public class SqliteConnectionProvider : RelationalDbConnectionProvider, IAmATransactionConnectionProvider { 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(RelationalDatabaseConfiguration 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 override DbConnection GetConnection() - { - return new SqliteConnection(_connectionString); - } + + public override DbConnection GetConnection() => Connection ?? (Connection = new SqliteConnection(_connectionString)); } } diff --git a/src/Paramore.Brighter/CommandProcessor.cs b/src/Paramore.Brighter/CommandProcessor.cs index a242748537..e43ee1baf3 100644 --- a/src/Paramore.Brighter/CommandProcessor.cs +++ b/src/Paramore.Brighter/CommandProcessor.cs @@ -55,7 +55,7 @@ public class CommandProcessor : IAmACommandProcessor private readonly IAmARequestContextFactory _requestContextFactory; private readonly IPolicyRegistry _policyRegistry; private readonly InboxConfiguration _inboxConfiguration; - private readonly IAmATransactionConnectonProvider _transactionConnectonProvider; + private readonly IAmATransactionConnectionProvider _transactionConnectionProvider; private readonly IAmAFeatureSwitchRegistry _featureSwitchRegistry; private readonly IEnumerable _replySubscriptions; private readonly TransformPipelineBuilder _transformPipelineBuilder; @@ -149,7 +149,7 @@ public CommandProcessor( /// 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 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, @@ -160,7 +160,7 @@ public CommandProcessor(IAmARequestContextFactory requestContextFactory, int outboxTimeout = 300, IAmAFeatureSwitchRegistry featureSwitchRegistry = null, InboxConfiguration inboxConfiguration = null, - IAmATransactionConnectonProvider transactionConnectonProvider = null, + IAmATransactionConnectionProvider transactionConnectionProvider = null, int outboxBulkChunkSize = 100, IAmAMessageTransformerFactory messageTransformerFactory = null) { @@ -168,7 +168,7 @@ public CommandProcessor(IAmARequestContextFactory requestContextFactory, _policyRegistry = policyRegistry; _featureSwitchRegistry = featureSwitchRegistry; _inboxConfiguration = inboxConfiguration; - _transactionConnectonProvider = transactionConnectonProvider; + _transactionConnectionProvider = transactionConnectionProvider; _transformPipelineBuilder = new TransformPipelineBuilder(mapperRegistry, messageTransformerFactory); InitExtServiceBus(policyRegistry, outBox, outboxTimeout, producerRegistry, outboxBulkChunkSize); @@ -192,7 +192,7 @@ public CommandProcessor(IAmARequestContextFactory requestContextFactory, /// 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 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, @@ -207,7 +207,7 @@ public CommandProcessor(IAmASubscriberRegistry subscriberRegistry, IAmAFeatureSwitchRegistry featureSwitchRegistry = null, IAmAChannelFactory responseChannelFactory = null, InboxConfiguration inboxConfiguration = null, - IAmATransactionConnectonProvider transactionConnectonProvider = null, + IAmATransactionConnectionProvider transactionConnectionProvider = null, int outboxBulkChunkSize = 100, IAmAMessageTransformerFactory messageTransformerFactory = null) : this(subscriberRegistry, handlerFactory, requestContextFactory, policyRegistry) @@ -215,7 +215,7 @@ public CommandProcessor(IAmASubscriberRegistry subscriberRegistry, _featureSwitchRegistry = featureSwitchRegistry; _responseChannelFactory = responseChannelFactory; _inboxConfiguration = inboxConfiguration; - _transactionConnectonProvider = transactionConnectonProvider; + _transactionConnectionProvider = transactionConnectionProvider; _replySubscriptions = replySubscriptions; _transformPipelineBuilder = new TransformPipelineBuilder(mapperRegistry, messageTransformerFactory); @@ -238,7 +238,7 @@ public CommandProcessor(IAmASubscriberRegistry subscriberRegistry, /// 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 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, @@ -251,13 +251,13 @@ public CommandProcessor(IAmASubscriberRegistry subscriberRegistry, int outboxTimeout = 300, IAmAFeatureSwitchRegistry featureSwitchRegistry = null, InboxConfiguration inboxConfiguration = null, - IAmATransactionConnectonProvider transactionConnectonProvider = null, + IAmATransactionConnectionProvider transactionConnectionProvider = null, int outboxBulkChunkSize = 100, IAmAMessageTransformerFactory messageTransformerFactory = null) : this(subscriberRegistry, handlerFactory, requestContextFactory, policyRegistry, featureSwitchRegistry) { _inboxConfiguration = inboxConfiguration; - _transactionConnectonProvider = transactionConnectonProvider; + _transactionConnectionProvider = transactionConnectionProvider; _transformPipelineBuilder = new TransformPipelineBuilder(mapperRegistry, messageTransformerFactory); InitExtServiceBus(policyRegistry, outBox, outboxTimeout, producerRegistry, outboxBulkChunkSize); @@ -537,7 +537,7 @@ public async Task PostAsync(T request, bool continueOnCapturedContext = false /// The Id of the Message that has been deposited. public Guid DepositPost(T request) where T : class, IRequest { - return DepositPost(request, _transactionConnectonProvider); + return DepositPost(request, _transactionConnectionProvider); } /// @@ -552,10 +552,10 @@ public Guid DepositPost(T request) where T : class, IRequest /// The Id of the Message that has been deposited. public Guid[] DepositPost(IEnumerable requests) where T : class, IRequest { - return DepositPost(requests, _transactionConnectonProvider); + return DepositPost(requests, _transactionConnectionProvider); } - private Guid DepositPost(T request, IAmATransactionConnectonProvider provider) + private Guid DepositPost(T request, IAmATransactionConnectionProvider provider) where T : class, IRequest { s_logger.LogInformation("Save request: {RequestType} {Id}", request.GetType(), request.Id); @@ -572,7 +572,7 @@ private Guid DepositPost(T request, IAmATransactionConnectonProvider provider return message.Id; } - private Guid[] DepositPost(IEnumerable requests, IAmATransactionConnectonProvider provider) + private Guid[] DepositPost(IEnumerable requests, IAmATransactionConnectionProvider provider) where T : class, IRequest { if (!_bus.HasBulkOutbox()) @@ -609,7 +609,7 @@ private Guid[] DepositPost(IEnumerable requests, IAmATransactionConnectonP public async Task DepositPostAsync(T request, bool continueOnCapturedContext = false, CancellationToken cancellationToken = default) where T : class, IRequest { - return await DepositPostAsync(request, _transactionConnectonProvider, continueOnCapturedContext, + return await DepositPostAsync(request, _transactionConnectionProvider, continueOnCapturedContext, cancellationToken); } @@ -631,12 +631,12 @@ public Task DepositPostAsync( CancellationToken cancellationToken = default ) where T : class, IRequest { - return DepositPostAsync(requests, _transactionConnectonProvider, continueOnCapturedContext, cancellationToken); + return DepositPostAsync(requests, _transactionConnectionProvider, continueOnCapturedContext, cancellationToken); } private async Task DepositPostAsync( T request, - IAmATransactionConnectonProvider provider, + IAmATransactionConnectionProvider provider, bool continueOnCapturedContext = false, CancellationToken cancellationToken = default) where T : class, IRequest { @@ -879,7 +879,7 @@ private static void InitExtServiceBus( } private async Task DepositPostAsync(IEnumerable requests, - IAmATransactionConnectonProvider provider, + IAmATransactionConnectionProvider provider, bool continueOnCapturedContext = false, CancellationToken cancellationToken = default ) where T : class, IRequest diff --git a/src/Paramore.Brighter/CommandProcessorBuilder.cs b/src/Paramore.Brighter/CommandProcessorBuilder.cs index dbd4cf8876..642decf2bc 100644 --- a/src/Paramore.Brighter/CommandProcessorBuilder.cs +++ b/src/Paramore.Brighter/CommandProcessorBuilder.cs @@ -84,7 +84,7 @@ public class CommandProcessorBuilder : INeedAHandlers, INeedPolicy, INeedMessagi private bool _useExternalBus = false; private bool _useRequestReplyQueues = false; private IEnumerable _replySubscriptions; - private IAmATransactionConnectonProvider _overridingBoxTransactionProvider = null; + private IAmATransactionConnectionProvider _overridingBoxTransactionProvider = null; private int _outboxBulkChunkSize; private CommandProcessorBuilder() @@ -162,14 +162,14 @@ public INeedMessaging DefaultPolicy() /// /// The Task Queues configuration. /// The Outbox. - /// + /// /// INeedARequestContext. - public INeedARequestContext ExternalBus(ExternalBusConfiguration configuration, IAmAnOutbox outbox, IAmATransactionConnectonProvider transactionConnectonProvider = null) + public INeedARequestContext ExternalBus(ExternalBusConfiguration configuration, IAmAnOutbox outbox, IAmATransactionConnectionProvider transactionConnectionProvider = null) { _useExternalBus = true; _producers = configuration.ProducerRegistry; _outbox = outbox; - _overridingBoxTransactionProvider = transactionConnectonProvider; + _overridingBoxTransactionProvider = transactionConnectionProvider; _messageMapperRegistry = configuration.MessageMapperRegistry; _outboxWriteTimeout = configuration.OutboxWriteTimeout; _outboxBulkChunkSize = configuration.OutboxBulkChunkSize; @@ -247,7 +247,7 @@ public CommandProcessor Build() producerRegistry: _producers, outboxTimeout: _outboxWriteTimeout, featureSwitchRegistry: _featureSwitchRegistry, - transactionConnectonProvider: _overridingBoxTransactionProvider, + transactionConnectionProvider: _overridingBoxTransactionProvider, outboxBulkChunkSize: _outboxBulkChunkSize, messageTransformerFactory: _transformerFactory ); @@ -263,7 +263,7 @@ public CommandProcessor Build() outBox: _outbox, producerRegistry: _producers, replySubscriptions: _replySubscriptions, - responseChannelFactory: _responseChannelFactory, transactionConnectonProvider: _overridingBoxTransactionProvider); + responseChannelFactory: _responseChannelFactory, transactionConnectionProvider: _overridingBoxTransactionProvider); } else { @@ -323,9 +323,9 @@ public interface INeedMessaging /// /// The configuration. /// The outbox. - /// The connection provider to use when adding messages to the bus + /// The connection provider to use when adding messages to the bus /// INeedARequestContext. - INeedARequestContext ExternalBus(ExternalBusConfiguration configuration, IAmAnOutbox outbox, IAmATransactionConnectonProvider transactionConnectonProvider = null); + INeedARequestContext ExternalBus(ExternalBusConfiguration configuration, IAmAnOutbox outbox, IAmATransactionConnectionProvider transactionConnectionProvider = null); /// /// We don't send messages out of process /// diff --git a/src/Paramore.Brighter/ExternalBusServices.cs b/src/Paramore.Brighter/ExternalBusServices.cs index f3ef9866c1..31bb3bc071 100644 --- a/src/Paramore.Brighter/ExternalBusServices.cs +++ b/src/Paramore.Brighter/ExternalBusServices.cs @@ -65,7 +65,7 @@ protected virtual void Dispose(bool disposing) } - internal async Task AddToOutboxAsync(T request, bool continueOnCapturedContext, CancellationToken cancellationToken, Message message, IAmATransactionConnectonProvider overridingAmATransactionProvider = null) + internal async Task AddToOutboxAsync(T request, bool continueOnCapturedContext, CancellationToken cancellationToken, Message message, IAmATransactionConnectionProvider overridingAmATransactionProvider = null) where T : class, IRequest { CheckOutboxOutstandingLimit(); @@ -79,7 +79,7 @@ internal async Task AddToOutboxAsync(T request, bool continueOnCapturedContex tags: new ActivityTagsCollection {{"MessageId", message.Id}})); } - internal async Task AddToOutboxAsync(IEnumerable messages, bool continueOnCapturedContext, CancellationToken cancellationToken, IAmATransactionConnectonProvider overridingAmATransactionProvider = null) + internal async Task AddToOutboxAsync(IEnumerable messages, bool continueOnCapturedContext, CancellationToken cancellationToken, IAmATransactionConnectionProvider overridingAmATransactionProvider = null) { CheckOutboxOutstandingLimit(); @@ -107,7 +107,7 @@ await box.AddAsync(chunk, OutboxTimeout, ct, overridingAmATransactionProvider) } } - internal void AddToOutbox(T request, Message message, IAmATransactionConnectonProvider overridingAmATransactionProvider = null) where T : class, IRequest + internal void AddToOutbox(T request, Message message, IAmATransactionConnectionProvider overridingAmATransactionProvider = null) where T : class, IRequest { CheckOutboxOutstandingLimit(); @@ -119,7 +119,7 @@ internal void AddToOutbox(T request, Message message, IAmATransactionConnecto tags: new ActivityTagsCollection {{"MessageId", message.Id}})); } - internal void AddToOutbox(IEnumerable messages, IAmATransactionConnectonProvider overridingAmATransactionProvider = null) + internal void AddToOutbox(IEnumerable messages, IAmATransactionConnectionProvider overridingAmATransactionProvider = null) { CheckOutboxOutstandingLimit(); diff --git a/src/Paramore.Brighter/IAmABoxTransactionProvider.cs b/src/Paramore.Brighter/IAmABoxTransactionProvider.cs index c0de5a5c95..870dde2447 100644 --- a/src/Paramore.Brighter/IAmABoxTransactionProvider.cs +++ b/src/Paramore.Brighter/IAmABoxTransactionProvider.cs @@ -1,7 +1,7 @@ namespace Paramore.Brighter { - public interface IAmABoxTransactionProvider - { - - } + /// + /// This is a marker interface to indicate that this connection provides access to an ambient transaction + /// + public interface IAmABoxTransactionProvider { } } diff --git a/src/Paramore.Brighter/IAmABulkOutboxAsync.cs b/src/Paramore.Brighter/IAmABulkOutboxAsync.cs index f61ac20576..382665f31b 100644 --- a/src/Paramore.Brighter/IAmABulkOutboxAsync.cs +++ b/src/Paramore.Brighter/IAmABulkOutboxAsync.cs @@ -48,6 +48,6 @@ public interface IAmABulkOutboxAsync : IAmAnOutboxAsync where T : Messa /// Allows the sender to cancel the request pipeline. Optional /// The Connection Provider to use for this call /// . - Task AddAsync(IEnumerable messages, int outBoxTimeout = -1, CancellationToken cancellationToken = default, IAmATransactionConnectonProvider amATransactionProvider = null); + Task AddAsync(IEnumerable messages, int outBoxTimeout = -1, CancellationToken cancellationToken = default, IAmATransactionConnectionProvider amATransactionProvider = null); } } diff --git a/src/Paramore.Brighter/IAmABulkOutboxSync.cs b/src/Paramore.Brighter/IAmABulkOutboxSync.cs index 346a888e6e..0c4555f12c 100644 --- a/src/Paramore.Brighter/IAmABulkOutboxSync.cs +++ b/src/Paramore.Brighter/IAmABulkOutboxSync.cs @@ -45,6 +45,6 @@ public interface IAmABulkOutboxSync : IAmAnOutboxSync where T : 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, IAmATransactionConnectonProvider amATransactionProvider = null); + void Add(IEnumerable messages, int outBoxTimeout = -1, IAmATransactionConnectionProvider amATransactionProvider = null); } } 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 index a3cd01f9b2..00965e9c2e 100644 --- a/src/Paramore.Brighter/IAmARelationalDbConnectionProvider.cs +++ b/src/Paramore.Brighter/IAmARelationalDbConnectionProvider.cs @@ -11,6 +11,8 @@ public interface IAmARelationalDbConnectionProvider Task GetConnectionAsync(CancellationToken cancellationToken = default); DbTransaction GetTransaction(); + + Task GetTransactionAsync(CancellationToken cancellationToken = default); bool HasOpenTransaction { get; } diff --git a/src/Paramore.Brighter/IAmATransactionConnectionProvider.cs b/src/Paramore.Brighter/IAmATransactionConnectionProvider.cs new file mode 100644 index 0000000000..9346b7f6fe --- /dev/null +++ b/src/Paramore.Brighter/IAmATransactionConnectionProvider.cs @@ -0,0 +1,4 @@ +namespace Paramore.Brighter +{ + public interface IAmATransactionConnectionProvider : IAmARelationalDbConnectionProvider, IAmABoxTransactionProvider { } +} diff --git a/src/Paramore.Brighter/IAmATransactionConnectonProvider.cs b/src/Paramore.Brighter/IAmATransactionConnectonProvider.cs deleted file mode 100644 index 5024f23ffd..0000000000 --- a/src/Paramore.Brighter/IAmATransactionConnectonProvider.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace Paramore.Brighter -{ - public interface IAmATransactionConnectonProvider : IAmARelationalDbConnectionProvider, IAmABoxTransactionProvider { } -} diff --git a/src/Paramore.Brighter/IAmAnOutboxAsync.cs b/src/Paramore.Brighter/IAmAnOutboxAsync.cs index f98f97d16e..91649f62cd 100644 --- a/src/Paramore.Brighter/IAmAnOutboxAsync.cs +++ b/src/Paramore.Brighter/IAmAnOutboxAsync.cs @@ -55,7 +55,7 @@ public interface IAmAnOutboxAsync : IAmAnOutbox where T : Message /// Allows the sender to cancel the request pipeline. Optional /// The Connection Provider to use for this call /// . - Task AddAsync(T message, int outBoxTimeout = -1, CancellationToken cancellationToken = default, IAmATransactionConnectonProvider amATransactionProvider = null); + Task AddAsync(T message, int outBoxTimeout = -1, CancellationToken cancellationToken = default, IAmATransactionConnectionProvider amATransactionProvider = null); /// /// Awaitable Get the specified message identifier. diff --git a/src/Paramore.Brighter/IAmAnOutboxSync.cs b/src/Paramore.Brighter/IAmAnOutboxSync.cs index 02d339e4e9..bdb44708b7 100644 --- a/src/Paramore.Brighter/IAmAnOutboxSync.cs +++ b/src/Paramore.Brighter/IAmAnOutboxSync.cs @@ -42,7 +42,7 @@ public interface IAmAnOutboxSync : IAmAnOutbox where T : 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, IAmATransactionConnectonProvider amATransactionProvider = null); + void Add(T message, int outBoxTimeout = -1, IAmATransactionConnectionProvider amATransactionProvider = null); /// /// Gets the specified message identifier. diff --git a/src/Paramore.Brighter/InMemoryOutbox.cs b/src/Paramore.Brighter/InMemoryOutbox.cs index 5eb87ad84d..29d3a666c4 100644 --- a/src/Paramore.Brighter/InMemoryOutbox.cs +++ b/src/Paramore.Brighter/InMemoryOutbox.cs @@ -99,7 +99,7 @@ public class InMemoryOutbox : InMemoryBox, IAmABulkOutboxSync /// /// This is not used for the In Memory Outbox. - public void Add(Message message, int outBoxTimeout = -1, IAmATransactionConnectonProvider amATransactionProvider = null) + public void Add(Message message, int outBoxTimeout = -1, IAmATransactionConnectionProvider amATransactionProvider = null) { ClearExpiredMessages(); EnforceCapacityLimit(); @@ -120,7 +120,7 @@ public void Add(Message message, int outBoxTimeout = -1, IAmATransactionConnecto /// /// /// This is not used for the In Memory Outbox. - public void Add(IEnumerable messages, int outBoxTimeout = -1, IAmATransactionConnectonProvider amATransactionProvider = null) + public void Add(IEnumerable messages, int outBoxTimeout = -1, IAmATransactionConnectionProvider amATransactionProvider = null) { ClearExpiredMessages(); EnforceCapacityLimit(); @@ -143,7 +143,7 @@ public Task AddAsync( Message message, int outBoxTimeout = -1, CancellationToken cancellationToken = default, - IAmATransactionConnectonProvider amATransactionProvider = null + IAmATransactionConnectionProvider amATransactionProvider = null ) { var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); @@ -172,7 +172,7 @@ public Task AddAsync( IEnumerable messages, int outBoxTimeout = -1, CancellationToken cancellationToken = default, - IAmATransactionConnectonProvider amATransactionProvider = null + IAmATransactionConnectionProvider amATransactionProvider = null ) { var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); diff --git a/src/Paramore.Brighter/RelationDatabaseOutbox.cs b/src/Paramore.Brighter/RelationDatabaseOutbox.cs index 4f5b0c71a4..f20147ace6 100644 --- a/src/Paramore.Brighter/RelationDatabaseOutbox.cs +++ b/src/Paramore.Brighter/RelationDatabaseOutbox.cs @@ -44,7 +44,7 @@ protected RelationDatabaseOutbox(string outboxTableName, IRelationDatabaseOutbox public void Add( Message message, int outBoxTimeout = -1, - IAmATransactionConnectonProvider amATransactionProvider = null) + IAmATransactionConnectionProvider amATransactionProvider = null) { var parameters = InitAddDbParameters(message); WriteToStore(amATransactionProvider, connection => InitAddDbCommand(connection, parameters), () => @@ -65,7 +65,7 @@ public void Add( public void Add( IEnumerable messages, int outBoxTimeout = -1, - IAmATransactionConnectonProvider amATransactionProvider = null + IAmATransactionConnectionProvider amATransactionProvider = null ) { WriteToStore(amATransactionProvider, @@ -95,7 +95,7 @@ public Task AddAsync( Message message, int outBoxTimeout = -1, CancellationToken cancellationToken = default, - IAmATransactionConnectonProvider amATransactionProvider = null + IAmATransactionConnectionProvider amATransactionProvider = null ) { var parameters = InitAddDbParameters(message); @@ -121,7 +121,7 @@ public Task AddAsync( IEnumerable messages, int outBoxTimeout = -1, CancellationToken cancellationToken = default, - IAmATransactionConnectonProvider amATransactionProvider = null + IAmATransactionConnectionProvider amATransactionProvider = null ) { return WriteToStoreAsync(amATransactionProvider, @@ -344,13 +344,13 @@ public Task DeleteAsync(CancellationToken cancellationToken, params Guid[] messa #endregion protected abstract void WriteToStore( - IAmATransactionConnectonProvider provider, + IAmATransactionConnectionProvider provider, Func commandFunc, Action loggingAction ); protected abstract Task WriteToStoreAsync( - IAmATransactionConnectonProvider transactionConnectionProvider, + IAmATransactionConnectionProvider transactionConnectionProvider, Func commandFunc, Action loggingAction, CancellationToken cancellationToken diff --git a/src/Paramore.Brighter/RelationalDatabaseConfiguration.cs b/src/Paramore.Brighter/RelationalDatabaseConfiguration.cs index ccb4eb2ae4..36145ef053 100644 --- a/src/Paramore.Brighter/RelationalDatabaseConfiguration.cs +++ b/src/Paramore.Brighter/RelationalDatabaseConfiguration.cs @@ -1,6 +1,6 @@ namespace Paramore.Brighter { - public class RelationalDatabaseConfiguration + public class RelationalDatabaseConfiguration : IAmARelationalDatabaseConfiguration { /// diff --git a/src/Paramore.Brighter/RelationalDbConnectionProvider.cs b/src/Paramore.Brighter/RelationalDbConnectionProvider.cs index ca541c11e4..b81fd621c9 100644 --- a/src/Paramore.Brighter/RelationalDbConnectionProvider.cs +++ b/src/Paramore.Brighter/RelationalDbConnectionProvider.cs @@ -1,4 +1,5 @@ -using System.Data.Common; +using System; +using System.Data.Common; using System.Threading; using System.Threading.Tasks; @@ -6,8 +7,22 @@ namespace Paramore.Brighter { public abstract class RelationalDbConnectionProvider : IAmARelationalDbConnectionProvider { + private bool _disposed = false; + protected DbConnection Connection; + protected DbTransaction Transaction; + + + /// + /// Get a database connection from the underlying provider + /// + /// public abstract DbConnection GetConnection(); + /// + /// Get a database connection from the underlying provider + /// + /// + /// public virtual async Task GetConnectionAsync(CancellationToken cancellationToken = default) { var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); @@ -15,14 +30,63 @@ public virtual async Task GetConnectionAsync(CancellationToken can return await tcs.Task; } + /// + /// Returns a transaction against the underlying database + /// + /// public virtual DbTransaction GetTransaction() { - //This connection factory does not support transactions - return null; + if (!HasOpenTransaction) + Transaction = GetConnection().BeginTransaction(); + return Transaction; + } + + /// + /// Returns a transaction against the underlying database + /// + /// + /// + public virtual Task GetTransactionAsync(CancellationToken cancellationToken = default) + { + var tcs = new TaskCompletionSource(); + + if(cancellationToken.IsCancellationRequested) + tcs.SetCanceled(); + + tcs.SetResult(GetTransaction()); + return tcs.Task; } - public virtual bool HasOpenTransaction { get => false; } + /// + /// Is there already a transaction open against the underlying database + /// + public virtual bool HasOpenTransaction { get { return Transaction != null; } } - public virtual bool IsSharedConnection { get => false; } + /// + /// Does the underlying provider share connections + /// + public virtual bool IsSharedConnection { get => true; } + + ~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) + { + Connection?.Dispose(); + Transaction?.Dispose(); + } + _disposed = true; + } + } } } diff --git a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/TestDoubles/FakeOutboxSync.cs b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/TestDoubles/FakeOutboxSync.cs index cb2522f83b..2ceb3649bb 100644 --- a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/TestDoubles/FakeOutboxSync.cs +++ b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/TestDoubles/FakeOutboxSync.cs @@ -37,12 +37,12 @@ public class FakeOutboxSync : IAmABulkOutboxSync, IAmABulkOutboxAsync messages, int outBoxTimeout = -1, - IAmATransactionConnectonProvider transactionProvider = null) + IAmATransactionConnectionProvider transactionProvider = null) { foreach (Message message in messages) { @@ -198,7 +198,7 @@ public void Add(IEnumerable messages, int outBoxTimeout = -1, public async Task AddAsync(IEnumerable messages, int outBoxTimeout = -1, CancellationToken cancellationToken = default, - IAmATransactionConnectonProvider transactionProvider = null) + IAmATransactionConnectionProvider transactionProvider = null) { foreach (var message in messages) { From b0859ca3fa8a0d4fb7555b48ffbac23c240585a7 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Mon, 8 May 2023 15:52:11 +0100 Subject: [PATCH 38/89] Breadth of interface for async scenarios --- .../.idea/httpRequests/http-requests-log.http | 163 ++++++++++-------- .../Handlers/AddGreetingHandlerAsync.cs | 10 +- .../Handlers/AddGreetingHandlerAsync.cs | 62 +++---- .../Handlers/AddPersonHandlerAsync.cs | 10 +- .../Handlers/DeletePersonHandlerAsync.cs | 35 ++-- .../Orders.Data/SqlConnectionProvider.cs | 2 +- .../MsSqlAzureConnectionProviderBase.cs | 2 + .../MsSqlSqlAuthConnectionProvider.cs | 1 + .../MySqlConnectionProvider.cs | 14 ++ .../PostgreSqlNpgsqlConnectionProvider.cs | 6 + ...SqliteEntityFrameworkConnectionProvider.cs | 9 +- .../SqliteConnectionProvider.cs | 18 ++ .../IAmARelationalDbConnectionProvider.cs | 41 ++++- .../RelationalDbConnectionProvider.cs | 47 +++-- 14 files changed, 271 insertions(+), 149 deletions(-) diff --git a/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http b/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http index 060dde5811..217a87c23f 100644 --- a/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http +++ b/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http @@ -9,6 +9,96 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } +<> 2023-05-08T132600.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-05-08T132419.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-05-08T131957.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-05-08T131953.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-05-08T131712.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" +} + +<> 2023-05-08T131709.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-05-08T131704.500.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-05-08T131659.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" +} + <> 2023-05-06T164909.500.json ### @@ -495,76 +585,3 @@ Content-Type: application/json ### -POST http://localhost:5000/People/new -Content-Type: application/json - -{ - "Name" : "Tyrion" -} - -<> 2022-10-19T201347.200.json - -### - -GET http://localhost:5000/People/Tyrion - -<> 2022-10-19T201322.500.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 -Content-Type: application/json - -{ - "Name" : "Tyrion" -} - -<> 2022-10-19T194012.500.json - -### - -POST http://localhost:5000/People/new -Content-Type: application/json - -{ - "Name" : "Tyrion" -} - -<> 2022-10-19T190032.500.json - -### - -POST http://localhost:5000/People/new -Content-Type: application/json - -{ - "Name" : "Tyrion" -} - -<> 2022-10-19T185850.500.json - -### - -POST http://localhost:5000/People/new -Content-Type: application/json - -{ - "Name" : "Tyrion" -} - -<> 2022-10-19T184041.500.json - -### - diff --git a/samples/WebAPI_Dapper/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs b/samples/WebAPI_Dapper/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs index a0fb7c7e83..ef7665b1df 100644 --- a/samples/WebAPI_Dapper/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs +++ b/samples/WebAPI_Dapper/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs @@ -18,12 +18,12 @@ public class AddGreetingHandlerAsync: RequestHandlerAsync { private readonly IAmACommandProcessor _postBox; private readonly ILogger _logger; - private readonly RelationalDbConnectionProvider _uow; + private readonly IAmATransactionConnectionProvider _transactionProvider; - public AddGreetingHandlerAsync(RelationalDbConnectionProvider uow, IAmACommandProcessor postBox, ILogger logger) + public AddGreetingHandlerAsync(IAmATransactionConnectionProvider transactionProvider, IAmACommandProcessor postBox, ILogger logger) { - _uow = 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; } @@ -37,9 +37,9 @@ 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); + var conn = await _transactionProvider.GetConnectionAsync(cancellationToken); await conn.OpenAsync(cancellationToken); - var tx = _uow.GetTransaction(); + var tx = _transactionProvider.GetTransaction(); try { var searchbyName = Predicates.Field(p => p.Name, Operator.Eq, addGreeting.Name); diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs index a0fb7c7e83..7874f0692a 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs +++ b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs @@ -18,12 +18,12 @@ public class AddGreetingHandlerAsync: RequestHandlerAsync { private readonly IAmACommandProcessor _postBox; private readonly ILogger _logger; - private readonly RelationalDbConnectionProvider _uow; + private readonly IAmATransactionConnectionProvider _transactionConnectionProvider; - public AddGreetingHandlerAsync(RelationalDbConnectionProvider uow, IAmACommandProcessor postBox, ILogger logger) + public AddGreetingHandlerAsync(IAmATransactionConnectionProvider transactionConnectionProvider, IAmACommandProcessor postBox, ILogger logger) { - _uow = uow; //We want to take the dependency on the same instance that will be used via the Outbox, so use the marker interface + _transactionConnectionProvider = transactionConnectionProvider; //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; } @@ -33,36 +33,38 @@ public AddGreetingHandlerAsync(RelationalDbConnectionProvider uow, IAmACommandPr public override async Task HandleAsync(AddGreeting addGreeting, CancellationToken cancellationToken = default) { var posts = new List(); - - //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(); - try + using (var conn = await _transactionConnectionProvider.GetConnectionAsync(cancellationToken)) { - 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); + await conn.OpenAsync(cancellationToken); + using (var tx = _transactionConnectionProvider.GetTransaction()) + { + try + { + var searchbyName = Predicates.Field(p => p.Name, Operator.Eq, addGreeting.Name); + var people = await conn.GetListAsync(searchbyName, transaction: tx); + var person = people.Single(); - //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); - } - 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(addGreeting, cancellationToken); + var greeting = new Greeting(addGreeting.Greeting, person); + + //write the added child entity to the Db + await conn.InsertAsync(greeting, 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()), + cancellationToken: cancellationToken)); + + //commit both new greeting and outgoing message + await tx.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); + return await base.HandleAsync(addGreeting, cancellationToken); + } + } } //Send this message via a transport. We need the ids to send just the messages here, not all outstanding ones. diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/AddPersonHandlerAsync.cs b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/AddPersonHandlerAsync.cs index 7934753bd9..1ba829da8a 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/AddPersonHandlerAsync.cs +++ b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/AddPersonHandlerAsync.cs @@ -1,4 +1,5 @@ -using System.Threading; +using System.Data.Common; +using System.Threading; using System.Threading.Tasks; using DapperExtensions; using GreetingsEntities; @@ -22,8 +23,11 @@ public AddPersonHandlerAsync(IAmATransactionConnectionProvider transactionConnec [UsePolicyAsync(step:1, policy: Policies.Retry.EXPONENTIAL_RETRYPOLICYASYNC)] public override async Task HandleAsync(AddPerson addPerson, CancellationToken cancellationToken = default) { - await _transactionConnectionProvider.GetConnection().InsertAsync(new Person(addPerson.Name)); - + using (var connection = await _transactionConnectionProvider.GetConnectionAsync(cancellationToken)) + { + await connection.InsertAsync(new Person(addPerson.Name)); + } + return await base.HandleAsync(addPerson, cancellationToken); } } diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/DeletePersonHandlerAsync.cs b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/DeletePersonHandlerAsync.cs index 3964d9f195..c801a54981 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/DeletePersonHandlerAsync.cs +++ b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/DeletePersonHandlerAsync.cs @@ -25,24 +25,29 @@ public DeletePersonHandlerAsync(IAmATransactionConnectionProvider transactionCon [UsePolicyAsync(step:1, policy: Policies.Retry.EXPONENTIAL_RETRYPOLICYASYNC)] public async override Task HandleAsync(DeletePerson deletePerson, CancellationToken cancellationToken = default) { - var tx = await _transactionConnectionProvider.GetTransactionAsync(cancellationToken); - try + using (var connection = await _transactionConnectionProvider.GetConnectionAsync(cancellationToken)) { + await connection.OpenAsync(cancellationToken); + var tx = await _transactionConnectionProvider.GetTransactionAsync(cancellationToken); + try + { - var searchbyName = Predicates.Field(p => p.Name, Operator.Eq, deletePerson.Name); - var people = await _transactionConnectionProvider.GetConnection().GetListAsync(searchbyName, transaction: tx); - var person = people.Single(); + var searchbyName = Predicates.Field(p => p.Name, Operator.Eq, deletePerson.Name); + var people = await _transactionConnectionProvider.GetConnection() + .GetListAsync(searchbyName, transaction: tx); + var person = people.Single(); - var deleteById = Predicates.Field(g => g.RecipientId, Operator.Eq, person.Id); - await _transactionConnectionProvider.GetConnection().DeleteAsync(deleteById, tx); - - await tx.CommitAsync(cancellationToken); - } - catch (Exception) - { - //it went wrong, rollback the entity change and the downstream message - await tx.RollbackAsync(cancellationToken); - return await base.HandleAsync(deletePerson, cancellationToken); + var deleteById = Predicates.Field(g => g.RecipientId, Operator.Eq, person.Id); + await _transactionConnectionProvider.GetConnection().DeleteAsync(deleteById, tx); + + await tx.CommitAsync(cancellationToken); + } + catch (Exception) + { + //it went wrong, rollback the entity change and the downstream message + await tx.RollbackAsync(cancellationToken); + return await base.HandleAsync(deletePerson, cancellationToken); + } } return await base.HandleAsync(deletePerson, cancellationToken); diff --git a/samples/WebApiWithWorkerAndSweeper/Orders.Data/SqlConnectionProvider.cs b/samples/WebApiWithWorkerAndSweeper/Orders.Data/SqlConnectionProvider.cs index 172f41c85e..fcaf04521d 100644 --- a/samples/WebApiWithWorkerAndSweeper/Orders.Data/SqlConnectionProvider.cs +++ b/samples/WebApiWithWorkerAndSweeper/Orders.Data/SqlConnectionProvider.cs @@ -18,7 +18,7 @@ public override DbConnection GetConnection() return _sqlConnection.Connection; } - public override SqlTransaction? GetTransaction() + public override DbTransaction GetTransaction() { return _sqlConnection.Transaction; } diff --git a/src/Paramore.Brighter.MsSql.Azure/MsSqlAzureConnectionProviderBase.cs b/src/Paramore.Brighter.MsSql.Azure/MsSqlAzureConnectionProviderBase.cs index 3def5702f4..96c4f759e3 100644 --- a/src/Paramore.Brighter.MsSql.Azure/MsSqlAzureConnectionProviderBase.cs +++ b/src/Paramore.Brighter.MsSql.Azure/MsSqlAzureConnectionProviderBase.cs @@ -32,6 +32,8 @@ protected MsSqlAzureConnectionProviderBase(RelationalDatabaseConfiguration confi _authenticationTokenScopes = new string[1] {_azureScope}; } + public void Close() { } + public DbConnection GetConnection() { var sqlConnection = new SqlConnection(_connectionString); diff --git a/src/Paramore.Brighter.MsSql/MsSqlSqlAuthConnectionProvider.cs b/src/Paramore.Brighter.MsSql/MsSqlSqlAuthConnectionProvider.cs index 1bb472560f..94a2388338 100644 --- a/src/Paramore.Brighter.MsSql/MsSqlSqlAuthConnectionProvider.cs +++ b/src/Paramore.Brighter.MsSql/MsSqlSqlAuthConnectionProvider.cs @@ -23,5 +23,6 @@ public MsSqlSqlAuthConnectionProvider(IAmARelationalDatabaseConfiguration config } public override DbConnection GetConnection() => Connection ?? (Connection = new SqlConnection(_connectionString)); + } } diff --git a/src/Paramore.Brighter.MySql/MySqlConnectionProvider.cs b/src/Paramore.Brighter.MySql/MySqlConnectionProvider.cs index 853fffa158..d16627f71d 100644 --- a/src/Paramore.Brighter.MySql/MySqlConnectionProvider.cs +++ b/src/Paramore.Brighter.MySql/MySqlConnectionProvider.cs @@ -24,7 +24,10 @@ 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 @@ -48,5 +51,16 @@ public MySqlConnectionProvider(IAmARelationalDatabaseConfiguration configuration } public override DbConnection GetConnection() => Connection ?? (Connection = new MySqlConnection(_connectionString)); + + public override async Task GetTransactionAsync(CancellationToken cancellationToken = default) + { + if (Connection == null) Connection = GetConnection(); + if (Connection.State != ConnectionState.Open) + await Connection.OpenAsync(cancellationToken); + if (!HasOpenTransaction) + Transaction = await ((MySqlConnection) Connection).BeginTransactionAsync(cancellationToken); + return Transaction; + } + } } diff --git a/src/Paramore.Brighter.PostgreSql/PostgreSqlNpgsqlConnectionProvider.cs b/src/Paramore.Brighter.PostgreSql/PostgreSqlNpgsqlConnectionProvider.cs index 9d3c87f662..b1d232e540 100644 --- a/src/Paramore.Brighter.PostgreSql/PostgreSqlNpgsqlConnectionProvider.cs +++ b/src/Paramore.Brighter.PostgreSql/PostgreSqlNpgsqlConnectionProvider.cs @@ -23,6 +23,12 @@ public PostgreSqlNpgsqlConnectionProvider(IAmARelationalDatabaseConfiguration co _connectionString = configuration.ConnectionString; } + /// + /// 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() => Connection ?? (Connection = new NpgsqlConnection(_connectionString)); + } } diff --git a/src/Paramore.Brighter.Sqlite.EntityFrameworkCore/SqliteEntityFrameworkConnectionProvider.cs b/src/Paramore.Brighter.Sqlite.EntityFrameworkCore/SqliteEntityFrameworkConnectionProvider.cs index bfe4977231..6b62b20e31 100644 --- a/src/Paramore.Brighter.Sqlite.EntityFrameworkCore/SqliteEntityFrameworkConnectionProvider.cs +++ b/src/Paramore.Brighter.Sqlite.EntityFrameworkCore/SqliteEntityFrameworkConnectionProvider.cs @@ -23,7 +23,7 @@ public SqliteEntityFrameworkConnectionProvider(T context) { _context = context; } - + /// /// Get the current connection of the DB context /// @@ -56,7 +56,14 @@ public override DbTransaction GetTransaction() return _context.Database.CurrentTransaction?.GetDbTransaction(); } + /// + /// 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/SqliteConnectionProvider.cs b/src/Paramore.Brighter.Sqlite/SqliteConnectionProvider.cs index c315378034..7903503dd1 100644 --- a/src/Paramore.Brighter.Sqlite/SqliteConnectionProvider.cs +++ b/src/Paramore.Brighter.Sqlite/SqliteConnectionProvider.cs @@ -23,7 +23,10 @@ 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 @@ -46,6 +49,21 @@ public SqliteConnectionProvider(IAmARelationalDatabaseConfiguration configuratio _connectionString = configuration.ConnectionString; } + /// + /// 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() => Connection ?? (Connection = new SqliteConnection(_connectionString)); + + public override async Task GetTransactionAsync(CancellationToken cancellationToken = default) + { + Connection ??= GetConnection(); + if (Connection.State != ConnectionState.Open) + await Connection.OpenAsync(cancellationToken); + if (!HasOpenTransaction) + Transaction = await Connection.BeginTransactionAsync(cancellationToken); + return Transaction; + } } } diff --git a/src/Paramore.Brighter/IAmARelationalDbConnectionProvider.cs b/src/Paramore.Brighter/IAmARelationalDbConnectionProvider.cs index 00965e9c2e..ba56b520a1 100644 --- a/src/Paramore.Brighter/IAmARelationalDbConnectionProvider.cs +++ b/src/Paramore.Brighter/IAmARelationalDbConnectionProvider.cs @@ -4,18 +4,53 @@ 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 { + /// + /// Close any open connection or transaction + /// + void 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 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); - Task GetConnectionAsync(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 DbTransaction GetTransaction(); - Task GetTransactionAsync(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 + 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; } } } diff --git a/src/Paramore.Brighter/RelationalDbConnectionProvider.cs b/src/Paramore.Brighter/RelationalDbConnectionProvider.cs index b81fd621c9..46e6ea4a8a 100644 --- a/src/Paramore.Brighter/RelationalDbConnectionProvider.cs +++ b/src/Paramore.Brighter/RelationalDbConnectionProvider.cs @@ -1,4 +1,5 @@ using System; +using System.Data; using System.Data.Common; using System.Threading; using System.Threading.Tasks; @@ -10,42 +11,52 @@ public abstract class RelationalDbConnectionProvider : IAmARelationalDbConnectio private bool _disposed = false; protected DbConnection Connection; protected DbTransaction Transaction; - + + /// + /// Close any open connection or transaction + /// + public virtual void Close() => Dispose(true); /// - /// Get a database connection from the underlying provider + /// 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 abstract DbConnection GetConnection(); - + /// - /// Get a database connection from the underlying provider + /// 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 /// - /// - /// - public virtual async Task GetConnectionAsync(CancellationToken cancellationToken = default) + /// A database connection + public virtual Task GetConnectionAsync(CancellationToken cancellationToken = default) { - var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var tcs = new TaskCompletionSource(); tcs.SetResult(GetConnection()); - return await tcs.Task; + return tcs.Task; } /// - /// Returns a transaction against the underlying database + /// 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 DbTransaction GetTransaction() { + Connection ??= GetConnection(); + if (Connection.State != ConnectionState.Open) + Connection.Open(); if (!HasOpenTransaction) - Transaction = GetConnection().BeginTransaction(); + Transaction = Connection.BeginTransaction(); return Transaction; } /// - /// Returns a transaction against the underlying database + /// 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(); @@ -58,12 +69,12 @@ public virtual Task GetTransactionAsync(CancellationToken cancell } /// - /// Is there already a transaction open against the underlying database + /// Is there a transaction open? /// public virtual bool HasOpenTransaction { get { return Transaction != null; } } /// - /// Does the underlying provider share connections + /// Is there a shared connection? (Do we maintain state of just create anew) /// public virtual bool IsSharedConnection { get => true; } From 0d7b218498331d4f7e9301ef6b0d4bf9bcb6e9d2 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Mon, 8 May 2023 20:42:30 +0100 Subject: [PATCH 39/89] Prevent sweeper running multiple threads --- .../.idea/httpRequests/http-requests-log.http | 105 +++++++++--------- .../Handlers/AddGreetingHandlerAsync.cs | 2 +- .../FIndGreetingsForPersonHandlerAsync.cs | 37 +++--- .../Handlers/FindPersonByNameHandlerAsync.cs | 10 +- .../TimedOutboxSweeper.cs | 17 ++- .../ServiceCollectionExtensions.cs | 2 +- .../SqliteOutbox.cs | 5 +- .../SqliteConnectionProvider.cs | 12 +- .../RelationalDbConnectionProvider.cs | 27 ++++- 9 files changed, 133 insertions(+), 84 deletions(-) diff --git a/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http b/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http index 217a87c23f..e3d23c6597 100644 --- a/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http +++ b/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http @@ -9,6 +9,61 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } +<> 2023-05-08T200327.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-05-08T200232.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-05-08T155920.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" +} + +<> 2023-05-08T155916.500.json + +### + +DELETE 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 + +### + +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-05-08T132600.200.json ### @@ -535,53 +590,3 @@ GET http://localhost:5000/People/Tyrion ### -POST http://localhost:5000/People/new -Content-Type: application/json - -{ - "Name" : "Tyrion" -} - -<> 2022-10-19T215040.200.json - -### - -GET http://localhost:5000/People/Tyrion - -<> 2022-10-19T214755.500.json - -### - -POST http://localhost:5000/Greetings/Tyrion/new -Content-Type: application/json - -{ - "Greeting" : "I drink, and I know things" -} - -<> 2022-10-19T204831.200.json - -### - -POST http://localhost:5000/Greetings/Tyrion/new -Content-Type: application/json - -{ - "Greeting" : "I drink, and I know things" -} - -<> 2022-10-19T204823.200.json - -### - -POST http://localhost:5000/Greetings/Tyrion/new -Content-Type: application/json - -{ - "Greeting" : "I drink, and I know things" -} - -<> 2022-10-19T204723.200.json - -### - diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs index 7874f0692a..08459a84cf 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs +++ b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs @@ -37,7 +37,7 @@ public override async Task HandleAsync(AddGreeting addGreeting, Can using (var conn = await _transactionConnectionProvider.GetConnectionAsync(cancellationToken)) { await conn.OpenAsync(cancellationToken); - using (var tx = _transactionConnectionProvider.GetTransaction()) + using (var tx = await _transactionConnectionProvider.GetTransactionAsync(cancellationToken)) { try { diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/FIndGreetingsForPersonHandlerAsync.cs b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/FIndGreetingsForPersonHandlerAsync.cs index 9fd4385c36..147394f1c1 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/FIndGreetingsForPersonHandlerAsync.cs +++ b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/FIndGreetingsForPersonHandlerAsync.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System.Data.Common; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Dapper; @@ -33,27 +34,29 @@ public FIndGreetingsForPersonHandlerAsync(IAmATransactionConnectionProvider tran 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 _transactionConnectionProvider.GetConnection().QueryAsync(sql, (person, greeting) => - { - person.Greetings.Add(greeting); - return person; - }, splitOn: "Id"); - var peopleGreetings = people.GroupBy(p => p.Id).Select(grp => + using (var connection = _transactionConnectionProvider.GetConnection()) { - var groupedPerson = grp.First(); - groupedPerson.Greetings = grp.Select(p => p.Greetings.Single()).ToList(); - return groupedPerson; - }); + var people = await connection.QueryAsync(sql, (person, greeting) => + { + person.Greetings.Add(greeting); + return person; + }, splitOn: "Id"); - var person = peopleGreetings.Single(); + var peopleGreetings = people.GroupBy(p => p.Id).Select(grp => + { + var groupedPerson = grp.First(); + groupedPerson.Greetings = grp.Select(p => p.Greetings.Single()).ToList(); + return groupedPerson; + }); - return new FindPersonsGreetings - { - Name = person.Name, - Greetings = person.Greetings.Select(g => new Salutation(g.Greet())) - }; + var person = peopleGreetings.Single(); + return new FindPersonsGreetings + { + Name = person.Name, Greetings = person.Greetings.Select(g => new Salutation(g.Greet())) + }; + } } } diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/FindPersonByNameHandlerAsync.cs b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/FindPersonByNameHandlerAsync.cs index aace04b21c..89480b8a5f 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/FindPersonByNameHandlerAsync.cs +++ b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/FindPersonByNameHandlerAsync.cs @@ -1,3 +1,4 @@ +using System.Data.Common; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -28,10 +29,13 @@ public FindPersonByNameHandlerAsync(IAmATransactionConnectionProvider transactio 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 _transactionConnectionProvider.GetConnection().GetListAsync(searchbyName); - var person = people.Single(); + using (var connection = await _transactionConnectionProvider.GetConnectionAsync(cancellationToken)) + { + var people = await connection.GetListAsync(searchbyName); + var person = people.Single(); - return new FindPersonResult(person); + return new FindPersonResult(person); + } } } } 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.Outbox.Sqlite/ServiceCollectionExtensions.cs b/src/Paramore.Brighter.Outbox.Sqlite/ServiceCollectionExtensions.cs index 1380d6d3df..7e30881a70 100644 --- a/src/Paramore.Brighter.Outbox.Sqlite/ServiceCollectionExtensions.cs +++ b/src/Paramore.Brighter.Outbox.Sqlite/ServiceCollectionExtensions.cs @@ -48,7 +48,7 @@ public static IBrighterBuilder UseSqliteTransactionConnectionProvider( this IBrighterBuilder brighterBuilder, Type connectionProvider, ServiceLifetime serviceLifetime = ServiceLifetime.Scoped) { - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmABoxTransactionProvider), connectionProvider, serviceLifetime)); + brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmATransactionConnectionProvider), connectionProvider, serviceLifetime)); return brighterBuilder; } diff --git a/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutbox.cs b/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutbox.cs index 2d274f2191..98b43e1924 100644 --- a/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutbox.cs +++ b/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutbox.cs @@ -105,10 +105,7 @@ Action loggingAction } finally { - if (!connectionProvider.IsSharedConnection) - connection.Dispose(); - else if (!connectionProvider.HasOpenTransaction) - connection.Close(); + connection.Close(); } } } diff --git a/src/Paramore.Brighter.Sqlite/SqliteConnectionProvider.cs b/src/Paramore.Brighter.Sqlite/SqliteConnectionProvider.cs index 7903503dd1..f6fa80566e 100644 --- a/src/Paramore.Brighter.Sqlite/SqliteConnectionProvider.cs +++ b/src/Paramore.Brighter.Sqlite/SqliteConnectionProvider.cs @@ -48,17 +48,23 @@ public SqliteConnectionProvider(IAmARelationalDatabaseConfiguration configuratio throw new ArgumentNullException(nameof(configuration.ConnectionString)); _connectionString = configuration.ConnectionString; } - + /// /// 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() => Connection ?? (Connection = new SqliteConnection(_connectionString)); + public override DbConnection GetConnection() + { + if (Connection == null) { Connection = new SqliteConnection(_connectionString);} + if (Connection.State != ConnectionState.Open) + Connection.Open(); + return Connection; + } public override async Task GetTransactionAsync(CancellationToken cancellationToken = default) { - Connection ??= GetConnection(); + if (Connection == null) { Connection = new SqliteConnection(_connectionString);} if (Connection.State != ConnectionState.Open) await Connection.OpenAsync(cancellationToken); if (!HasOpenTransaction) diff --git a/src/Paramore.Brighter/RelationalDbConnectionProvider.cs b/src/Paramore.Brighter/RelationalDbConnectionProvider.cs index 46e6ea4a8a..daeebe074f 100644 --- a/src/Paramore.Brighter/RelationalDbConnectionProvider.cs +++ b/src/Paramore.Brighter/RelationalDbConnectionProvider.cs @@ -11,11 +11,32 @@ public abstract class RelationalDbConnectionProvider : IAmARelationalDbConnectio private bool _disposed = false; protected DbConnection Connection; protected DbTransaction Transaction; + + /// + /// debugging + /// + public Guid Instance = Guid.NewGuid(); /// /// Close any open connection or transaction /// - public virtual void Close() => Dispose(true); + public virtual void Close() + { + Transaction?.Dispose(); + Connection?.Close(); + } + + /// + /// Commit the transaction + /// + public void Commit() + { + if (HasOpenTransaction) + { + Transaction.Commit(); + Transaction = null; + } + } /// /// Gets a existing Connection; creates a new one if it does not exist @@ -39,7 +60,7 @@ public virtual Task GetConnectionAsync(CancellationToken cancellat /// /// Gets an existing transaction; creates a new one from the connection if it does not exist. - /// You are responsible for committing the transaction. + /// YOu should use the commit transaction using the Commit method. /// /// A database transaction public virtual DbTransaction GetTransaction() @@ -96,6 +117,8 @@ protected virtual void Dispose(bool disposing) Connection?.Dispose(); Transaction?.Dispose(); } + Connection = null; + Transaction = null; _disposed = true; } } From e0de4d02c3e18a46db03f3d2b12fc46c9c7a18fe Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Wed, 10 May 2023 11:24:55 +0100 Subject: [PATCH 40/89] unit of work and connection provider --- .../GreetingsWeb/Database/OutboxExtensions.cs | 4 +- .../GreetingsWeb/Database/OutboxExtensions.cs | 5 +- ...qlEntityFrameworkCoreConnectionProvider.cs | 10 +- .../MsSqlSqlAuthConnectionProvider.cs | 29 +++- .../MsSqlSqlAuthUnitOfWork.cs | 78 +++++++++++ .../MySqlEntityFrameworkConnectionProvider.cs | 2 +- .../MySqlConnectionProvider.cs | 33 +++-- .../MySqlUnitOfWork.cs | 106 +++++++++++++++ .../PostgreSqlNpgsqlConnectionProvider.cs | 22 +++- .../PostgreSqlNpgsqlUnitOfWork.cs | 83 ++++++++++++ .../SqliteConnectionProvider.cs | 27 ++-- .../SqliteUnitOfWork.cs | 109 +++++++++++++++ .../RelationalDbConnectionProvider.cs | 57 ++------ .../RelationalDbTransactionProvider.cs | 124 ++++++++++++++++++ 14 files changed, 608 insertions(+), 81 deletions(-) create mode 100644 src/Paramore.Brighter.MsSql/MsSqlSqlAuthUnitOfWork.cs create mode 100644 src/Paramore.Brighter.MySql/MySqlUnitOfWork.cs create mode 100644 src/Paramore.Brighter.PostgreSql/PostgreSqlNpgsqlUnitOfWork.cs create mode 100644 src/Paramore.Brighter.Sqlite/SqliteUnitOfWork.cs create mode 100644 src/Paramore.Brighter/RelationalDbTransactionProvider.cs diff --git a/samples/WebAPI_Dapper/GreetingsWeb/Database/OutboxExtensions.cs b/samples/WebAPI_Dapper/GreetingsWeb/Database/OutboxExtensions.cs index b87334a6ab..f5203c2e5a 100644 --- a/samples/WebAPI_Dapper/GreetingsWeb/Database/OutboxExtensions.cs +++ b/samples/WebAPI_Dapper/GreetingsWeb/Database/OutboxExtensions.cs @@ -41,7 +41,7 @@ private static void AddMySqlOutbox(IBrighterBuilder brighterBuilder, string dbCo new RelationalDatabaseConfiguration(dbConnectionString, outBoxTableName), typeof(MySqlConnectionProvider), ServiceLifetime.Singleton) - .UseMySqTransactionConnectionProvider(typeof(Paramore.Brighter.MySql.MySqlConnectionProvider), ServiceLifetime.Scoped) + .UseMySqTransactionConnectionProvider(typeof(MySqlUnitOfWork), ServiceLifetime.Scoped) .UseOutboxSweeper(); } @@ -51,7 +51,7 @@ private static void AddSqliteOutBox(IBrighterBuilder brighterBuilder, string dbC new RelationalDatabaseConfiguration(dbConnectionString, outBoxTableName), typeof(SqliteConnectionProvider), ServiceLifetime.Singleton) - .UseSqliteTransactionConnectionProvider(typeof(Paramore.Brighter.Sqlite.SqliteConnectionProvider), ServiceLifetime.Scoped) + .UseSqliteTransactionConnectionProvider(typeof(SqliteUnitOfWork), ServiceLifetime.Scoped) .UseOutboxSweeper(options => { options.TimerInterval = 5; diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/OutboxExtensions.cs b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/OutboxExtensions.cs index cbb62c424f..7793a3e271 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/OutboxExtensions.cs +++ b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/OutboxExtensions.cs @@ -44,12 +44,11 @@ private static void AddMySqlOutbox(IBrighterBuilder brighterBuilder, RelationalDatabaseConfiguration configuration) { brighterBuilder.UseMySqlOutbox( - //new MySqlConfiguration(dbConnectionString, outBoxTableName, binaryMessagePayload: binaryMessagePayload), configuration, typeof(MySqlConnectionProvider), ServiceLifetime.Singleton) .UseMySqTransactionConnectionProvider( - typeof(Paramore.Brighter.MySql.MySqlConnectionProvider), ServiceLifetime.Scoped) + typeof(MySqlUnitOfWork), ServiceLifetime.Scoped) .UseOutboxSweeper(); } @@ -61,7 +60,7 @@ private static void AddSqliteOutBox(IBrighterBuilder brighterBuilder, typeof(SqliteConnectionProvider), ServiceLifetime.Singleton) .UseSqliteTransactionConnectionProvider( - typeof(Paramore.Brighter.Sqlite.SqliteConnectionProvider), ServiceLifetime.Scoped) + typeof(SqliteUnitOfWork), ServiceLifetime.Scoped) .UseOutboxSweeper(options => { options.TimerInterval = 5; diff --git a/src/Paramore.Brighter.MsSql.EntityFrameworkCore/MsSqlEntityFrameworkCoreConnectionProvider.cs b/src/Paramore.Brighter.MsSql.EntityFrameworkCore/MsSqlEntityFrameworkCoreConnectionProvider.cs index 9e16b88c0a..604ae2d32b 100644 --- a/src/Paramore.Brighter.MsSql.EntityFrameworkCore/MsSqlEntityFrameworkCoreConnectionProvider.cs +++ b/src/Paramore.Brighter.MsSql.EntityFrameworkCore/MsSqlEntityFrameworkCoreConnectionProvider.cs @@ -7,7 +7,7 @@ namespace Paramore.Brighter.MsSql.EntityFrameworkCore { - public class MsSqlEntityFrameworkCoreConnectionProvider : RelationalDbConnectionProvider, IAmATransactionConnectionProvider where T : DbContext + public class MsSqlEntityFrameworkCoreConnectionProvider : RelationalDbConnectionProvider where T : DbContext { private readonly T _context; @@ -23,14 +23,18 @@ 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 _context.Database.GetDbConnection(); + var connection = _context.Database.GetDbConnection(); + if (connection.State != System.Data.ConnectionState.Open) connection.Open(); + return 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 _context.Database.GetDbConnection(); + var connection = _context.Database.GetDbConnection(); + if (connection.State != System.Data.ConnectionState.Open) await connection.OpenAsync(cancellationToken); + return connection; } public override DbTransaction GetTransaction() diff --git a/src/Paramore.Brighter.MsSql/MsSqlSqlAuthConnectionProvider.cs b/src/Paramore.Brighter.MsSql/MsSqlSqlAuthConnectionProvider.cs index 94a2388338..282a99a876 100644 --- a/src/Paramore.Brighter.MsSql/MsSqlSqlAuthConnectionProvider.cs +++ b/src/Paramore.Brighter.MsSql/MsSqlSqlAuthConnectionProvider.cs @@ -1,5 +1,7 @@ using System; using System.Data.Common; +using System.Threading; +using System.Threading.Tasks; using Microsoft.Data.SqlClient; namespace Paramore.Brighter.MsSql @@ -7,7 +9,7 @@ namespace Paramore.Brighter.MsSql /// /// A connection provider for Sqlite /// - public class MsSqlSqlAuthConnectionProvider : RelationalDbConnectionProvider, IAmATransactionConnectionProvider + public class MsSqlSqlAuthConnectionProvider : RelationalDbConnectionProvider { private readonly string _connectionString; @@ -22,7 +24,28 @@ public MsSqlSqlAuthConnectionProvider(IAmARelationalDatabaseConfiguration config _connectionString = configuration.ConnectionString; } - public override DbConnection GetConnection() => Connection ?? (Connection = new SqlConnection(_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/MsSqlSqlAuthUnitOfWork.cs b/src/Paramore.Brighter.MsSql/MsSqlSqlAuthUnitOfWork.cs new file mode 100644 index 0000000000..2e2a274409 --- /dev/null +++ b/src/Paramore.Brighter.MsSql/MsSqlSqlAuthUnitOfWork.cs @@ -0,0 +1,78 @@ +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 MsSqlSqlAuthUnitOfWork : RelationalDbTransactionProvider + { + private readonly string _connectionString; + + /// + /// Create a connection provider for MSSQL using a connection string for Db access + /// + /// The configuration for this database + public MsSqlSqlAuthUnitOfWork(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 (Connection.State != ConnectionState.Open) Connection.Open(); + 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 (Connection.State != ConnectionState.Open) await Connection.OpenAsync(cancellationToken); + if (!HasOpenTransaction) + Transaction = ((SqlConnection) Connection).BeginTransaction(); + return Transaction; + } + } +} diff --git a/src/Paramore.Brighter.MySql.EntityFrameworkCore/MySqlEntityFrameworkConnectionProvider.cs b/src/Paramore.Brighter.MySql.EntityFrameworkCore/MySqlEntityFrameworkConnectionProvider.cs index c7eef2dd4a..0ec87bdb47 100644 --- a/src/Paramore.Brighter.MySql.EntityFrameworkCore/MySqlEntityFrameworkConnectionProvider.cs +++ b/src/Paramore.Brighter.MySql.EntityFrameworkCore/MySqlEntityFrameworkConnectionProvider.cs @@ -11,7 +11,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 : RelationalDbConnectionProvider, IAmATransactionConnectionProvider where T: DbContext + public class MySqlEntityFrameworkConnectionProvider : RelationalDbTransactionProvider where T: DbContext { private readonly T _context; diff --git a/src/Paramore.Brighter.MySql/MySqlConnectionProvider.cs b/src/Paramore.Brighter.MySql/MySqlConnectionProvider.cs index d16627f71d..a16aa16057 100644 --- a/src/Paramore.Brighter.MySql/MySqlConnectionProvider.cs +++ b/src/Paramore.Brighter.MySql/MySqlConnectionProvider.cs @@ -35,7 +35,7 @@ namespace Paramore.Brighter.MySql /// /// A connection provider that uses the connection string to create a connection /// - public class MySqlConnectionProvider : RelationalDbConnectionProvider, IAmATransactionConnectionProvider + public class MySqlConnectionProvider : RelationalDbConnectionProvider { private readonly string _connectionString; @@ -50,17 +50,28 @@ public MySqlConnectionProvider(IAmARelationalDatabaseConfiguration configuration _connectionString = configuration.ConnectionString; } - public override DbConnection GetConnection() => Connection ?? (Connection = new MySqlConnection(_connectionString)); - - public override async Task GetTransactionAsync(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 connection = new MySqlConnection(_connectionString); + if (connection.State != ConnectionState.Open) connection.Open(); + return connection; + } + + /// + /// 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) { - if (Connection == null) Connection = GetConnection(); - if (Connection.State != ConnectionState.Open) - await Connection.OpenAsync(cancellationToken); - if (!HasOpenTransaction) - Transaction = await ((MySqlConnection) Connection).BeginTransactionAsync(cancellationToken); - return Transaction; + var connection = new MySqlConnection(_connectionString); + if (connection.State != ConnectionState.Open) await connection.OpenAsync(cancellationToken); + return connection; } - } } diff --git a/src/Paramore.Brighter.MySql/MySqlUnitOfWork.cs b/src/Paramore.Brighter.MySql/MySqlUnitOfWork.cs new file mode 100644 index 0000000000..bae6317655 --- /dev/null +++ b/src/Paramore.Brighter.MySql/MySqlUnitOfWork.cs @@ -0,0 +1,106 @@ +#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; + } + + /// + /// 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 (Connection.State != ConnectionState.Open) Connection.Open(); + 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 (Connection.State != ConnectionState.Open) + await Connection.OpenAsync(cancellationToken); + if (!HasOpenTransaction) + Transaction = await ((MySqlConnection) Connection).BeginTransactionAsync(cancellationToken); + return Transaction; + } + + } +} diff --git a/src/Paramore.Brighter.PostgreSql/PostgreSqlNpgsqlConnectionProvider.cs b/src/Paramore.Brighter.PostgreSql/PostgreSqlNpgsqlConnectionProvider.cs index b1d232e540..3c285ab4b3 100644 --- a/src/Paramore.Brighter.PostgreSql/PostgreSqlNpgsqlConnectionProvider.cs +++ b/src/Paramore.Brighter.PostgreSql/PostgreSqlNpgsqlConnectionProvider.cs @@ -1,5 +1,7 @@ using System; using System.Data.Common; +using System.Threading; +using System.Threading.Tasks; using Npgsql; namespace Paramore.Brighter.PostgreSql @@ -28,7 +30,23 @@ public PostgreSqlNpgsqlConnectionProvider(IAmARelationalDatabaseConfiguration co /// The connection is not opened, you need to open it yourself. /// /// A database connection - public override DbConnection GetConnection() => Connection ?? (Connection = new NpgsqlConnection(_connectionString)); - + public override DbConnection GetConnection() + { + 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 = new NpgsqlConnection(_connectionString); + if (connection.State != System.Data.ConnectionState.Open) await connection.OpenAsync(cancellationToken); + return connection; + } } } diff --git a/src/Paramore.Brighter.PostgreSql/PostgreSqlNpgsqlUnitOfWork.cs b/src/Paramore.Brighter.PostgreSql/PostgreSqlNpgsqlUnitOfWork.cs new file mode 100644 index 0000000000..aa9d8d2ccd --- /dev/null +++ b/src/Paramore.Brighter.PostgreSql/PostgreSqlNpgsqlUnitOfWork.cs @@ -0,0 +1,83 @@ +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 PostgreSqlNpgsqlUnitOfWork : RelationalDbTransactionProvider + { + private readonly string _connectionString; + + /// + /// Initialise a new instance of PostgreSQl Connection provider from a connection string + /// + /// PostgreSQL Configuration + public PostgreSqlNpgsqlUnitOfWork(IAmARelationalDatabaseConfiguration configuration) + { + if (string.IsNullOrWhiteSpace(configuration?.ConnectionString)) + throw new ArgumentNullException(nameof(configuration.ConnectionString)); + + _connectionString = configuration.ConnectionString; + } + + /// + /// 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) 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) 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 (Connection.State != ConnectionState.Open) + Connection.Open(); + 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 (Connection.State != ConnectionState.Open) + await Connection.OpenAsync(cancellationToken); + if (!HasOpenTransaction) + Transaction = ((NpgsqlConnection)Connection).BeginTransaction(); + return Transaction; + } + } +} diff --git a/src/Paramore.Brighter.Sqlite/SqliteConnectionProvider.cs b/src/Paramore.Brighter.Sqlite/SqliteConnectionProvider.cs index f6fa80566e..1763db6b20 100644 --- a/src/Paramore.Brighter.Sqlite/SqliteConnectionProvider.cs +++ b/src/Paramore.Brighter.Sqlite/SqliteConnectionProvider.cs @@ -34,7 +34,7 @@ namespace Paramore.Brighter.Sqlite /// /// A connection provider for Sqlite /// - public class SqliteConnectionProvider : RelationalDbConnectionProvider, IAmATransactionConnectionProvider + public class SqliteConnectionProvider : RelationalDbConnectionProvider { private readonly string _connectionString; @@ -56,20 +56,23 @@ public SqliteConnectionProvider(IAmARelationalDatabaseConfiguration configuratio /// A database connection public override DbConnection GetConnection() { - if (Connection == null) { Connection = new SqliteConnection(_connectionString);} - if (Connection.State != ConnectionState.Open) - Connection.Open(); - return Connection; + var connection = new SqliteConnection(_connectionString); + if (connection.State != ConnectionState.Open) + connection.Open(); + return connection; } - public override async Task GetTransactionAsync(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. + /// + /// 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); - if (!HasOpenTransaction) - Transaction = await Connection.BeginTransactionAsync(cancellationToken); - return Transaction; + var connection = new SqliteConnection(_connectionString); + if (connection.State != ConnectionState.Open) + await connection.OpenAsync(cancellationToken); + return connection; } } } diff --git a/src/Paramore.Brighter.Sqlite/SqliteUnitOfWork.cs b/src/Paramore.Brighter.Sqlite/SqliteUnitOfWork.cs new file mode 100644 index 0000000000..4a96c15421 --- /dev/null +++ b/src/Paramore.Brighter.Sqlite/SqliteUnitOfWork.cs @@ -0,0 +1,109 @@ +#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; + } + + /// + /// 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 = new SqliteConnection(_connectionString);} + if (Connection.State != ConnectionState.Open) + Connection.Open(); + 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 = new SqliteConnection(_connectionString);} + if (Connection.State != ConnectionState.Open) + await Connection.OpenAsync(cancellationToken); + if (!HasOpenTransaction) + Transaction = await Connection.BeginTransactionAsync(cancellationToken); + return Transaction; + } + } +} diff --git a/src/Paramore.Brighter/RelationalDbConnectionProvider.cs b/src/Paramore.Brighter/RelationalDbConnectionProvider.cs index daeebe074f..5e4f5c6580 100644 --- a/src/Paramore.Brighter/RelationalDbConnectionProvider.cs +++ b/src/Paramore.Brighter/RelationalDbConnectionProvider.cs @@ -9,8 +9,6 @@ namespace Paramore.Brighter public abstract class RelationalDbConnectionProvider : IAmARelationalDbConnectionProvider { private bool _disposed = false; - protected DbConnection Connection; - protected DbTransaction Transaction; /// /// debugging @@ -18,29 +16,19 @@ public abstract class RelationalDbConnectionProvider : IAmARelationalDbConnectio public Guid Instance = Guid.NewGuid(); /// - /// Close any open connection or transaction + /// Does not retain shared connections or transactions, so nothing to commit /// - public virtual void Close() - { - Transaction?.Dispose(); - Connection?.Close(); - } + public virtual void Close() { } /// - /// Commit the transaction + /// Does not support shared transactions, so nothing to commit, manage transactions independently /// - public void Commit() - { - if (HasOpenTransaction) - { - Transaction.Commit(); - Transaction = null; - } - } + public void Commit() { } /// /// Gets a existing Connection; creates a new one if it does not exist - /// The connection is not opened, you need to open it yourself. + /// Opens the connection if it is not opened + /// This is not a shared connection /// /// A database connection public abstract DbConnection GetConnection(); @@ -59,45 +47,29 @@ public virtual Task GetConnectionAsync(CancellationToken cancellat } /// - /// 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. + /// Does not support shared transactions, create a transaction of the DbConnection instead /// /// A database transaction - public virtual DbTransaction GetTransaction() - { - Connection ??= GetConnection(); - if (Connection.State != ConnectionState.Open) - Connection.Open(); - if (!HasOpenTransaction) - Transaction = Connection.BeginTransaction(); - return Transaction; - } + public virtual DbTransaction GetTransaction() { return null; } /// - /// Gets an existing transaction; creates a new one from the connection if it does not exist. - /// You are responsible for committing the transaction. + /// Does not support shared transactions, create a transaction of the DbConnection instead /// /// 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; + return null; } /// /// Is there a transaction open? /// - public virtual bool HasOpenTransaction { get { return Transaction != null; } } + public virtual bool HasOpenTransaction { get => false; } /// /// Is there a shared connection? (Do we maintain state of just create anew) /// - public virtual bool IsSharedConnection { get => true; } + public virtual bool IsSharedConnection { get => false; } ~RelationalDbConnectionProvider() => Dispose(false); @@ -114,11 +86,8 @@ protected virtual void Dispose(bool disposing) { if (disposing) { - Connection?.Dispose(); - Transaction?.Dispose(); + /* No shared transactions, nothing to do */ } - Connection = null; - Transaction = null; _disposed = true; } } diff --git a/src/Paramore.Brighter/RelationalDbTransactionProvider.cs b/src/Paramore.Brighter/RelationalDbTransactionProvider.cs new file mode 100644 index 0000000000..136035c117 --- /dev/null +++ b/src/Paramore.Brighter/RelationalDbTransactionProvider.cs @@ -0,0 +1,124 @@ +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() + { + Transaction?.Dispose(); + Transaction = null; + Connection.Close(); + if (!IsSharedConnection) + Connection?.Close(); + } + + /// + /// Commit the transaction + /// + public void Commit() + { + if (HasOpenTransaction) + { + Transaction.Commit(); + Transaction = null; + } + } + + /// + /// 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; } + + ~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; + } + } + } +} From b8be74961686c5722ac95f5552644d766e0f6ac0 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Wed, 10 May 2023 13:36:45 +0100 Subject: [PATCH 41/89] Ensure we register the new interfaces --- samples/WebAPI_Dapper/GreetingsWeb/Startup.cs | 2 -- .../WebAPI_Dapper_Kafka/SalutationAnalytics/Program.cs | 8 ++++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs b/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs index d4e7d14a20..3a629f50be 100644 --- a/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs +++ b/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs @@ -144,14 +144,12 @@ private static void ConfigureDapperSqlite(IServiceCollection services) { DapperExtensions.DapperExtensions.SqlDialect = new SqliteDialect(); DapperAsyncExtensions.SqlDialect = new SqliteDialect(); - services.AddScoped(); } private static void ConfigureDapperMySql(IServiceCollection services) { DapperExtensions.DapperExtensions.SqlDialect = new MySqlDialect(); DapperAsyncExtensions.SqlDialect = new MySqlDialect(); - services.AddScoped(); } private void ConfigureBrighter(IServiceCollection services) diff --git a/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Program.cs b/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Program.cs index d82769ef2a..9e14c94e33 100644 --- a/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Program.cs +++ b/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Program.cs @@ -16,8 +16,10 @@ using Paramore.Brighter.Inbox.MySql; using Paramore.Brighter.Inbox.Sqlite; using Paramore.Brighter.MessagingGateway.Kafka; +using Paramore.Brighter.MySql; using Paramore.Brighter.ServiceActivator.Extensions.DependencyInjection; using Paramore.Brighter.ServiceActivator.Extensions.Hosting; +using Paramore.Brighter.Sqlite; using SalutationAnalytics.Database; using SalutationPorts.EntityMappers; using SalutationPorts.Policies; @@ -187,14 +189,16 @@ 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(); } From 9d690a5c71f6061155174942315b3be933fe4722 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Thu, 11 May 2023 08:21:02 +0100 Subject: [PATCH 42/89] entity framework is a type of transaction provider --- .../.idea/httpRequests/http-requests-log.http | 134 ++++++++++-------- .../SalutationAnalytics/Program.cs | 24 ++-- .../Handlers/AddGreetingHandlerAsync.cs | 3 + .../Handlers/AddPersonHandlerAsync.cs | 11 +- .../Handlers/DeletePersonHandlerAsync.cs | 16 ++- .../GreetingsWeb/Startup.cs | 6 +- .../SalutationAnalytics/Program.cs | 13 +- .../MsSqlAzureConnectionProviderBase.cs | 60 +++----- .../MsSqlChainedConnectionProvider.cs | 2 +- .../MsSqlDefaultAzureConnectionProvider.cs | 2 +- .../MsSqlManagedIdentityConnectionProvider.cs | 2 +- ...MsSqlSharedTokenCacheConnectionProvider.cs | 2 +- .../MsSqlVisualStudioConnectionProvider.cs | 2 +- ...qlEntityFrameworkCoreConnectionProvider.cs | 27 +++- .../MySqlEntityFrameworkConnectionProvider.cs | 25 ++++ .../MySqlUnitOfWork.cs | 15 ++ .../ServiceCollectionExtensions.cs | 2 +- ...greSqlEntityFrameworkConnectionProvider.cs | 27 +++- .../PostgreSqlNpgsqlUnitOfWork.cs | 15 ++ ...SqliteEntityFrameworkConnectionProvider.cs | 27 +++- .../SqliteUnitOfWork.cs | 15 ++ .../IAmARelationalDbConnectionProvider.cs | 12 ++ .../RelationalDbConnectionProvider.cs | 5 + .../RelationalDbTransactionProvider.cs | 17 ++- 24 files changed, 330 insertions(+), 134 deletions(-) diff --git a/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http b/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http index e3d23c6597..5ead0a4d29 100644 --- a/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http +++ b/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http @@ -1,3 +1,81 @@ +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" +} + +<> 2023-05-10T193837.500.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-05-10T193523.500.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" +} + +<> 2023-05-10T193516.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" +} + +<> 2023-05-10T140942.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-05-10T140922.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" +} + +<> 2023-05-10T140918.500.json + +### + POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json Content-Length: 47 @@ -534,59 +612,3 @@ Content-Type: application/json ### -POST http://localhost:5000/People/new -Content-Type: application/json - -{ - "Name" : "Tyrion" -} - -<> 2022-11-08T211124.200.json - -### - -POST http://localhost:5000/People/new -Content-Type: application/json - -{ - "Name" : "Tyrion" -} - -<> 2022-11-08T200350.200.json - -### - -POST http://localhost:5000/Greetings/Tyrion/new -Content-Type: application/json - -{ - "Greeting" : "I drink, and I know things" -} - -<> 2022-10-21T115924.200.json - -### - -GET http://localhost:5000/People/Tyrion - -<> 2022-10-21T115848.200.json - -### - -POST http://localhost:5000/People/new -Content-Type: application/json - -{ - "Name" : "Tyrion" -} - -<> 2022-10-21T115844.200.json - -### - -GET http://localhost:5000/People/Tyrion - -<> 2022-10-21T115838.500.json - -### - diff --git a/samples/WebAPI_Dapper/SalutationAnalytics/Program.cs b/samples/WebAPI_Dapper/SalutationAnalytics/Program.cs index be479e73e4..aaaa7079a1 100644 --- a/samples/WebAPI_Dapper/SalutationAnalytics/Program.cs +++ b/samples/WebAPI_Dapper/SalutationAnalytics/Program.cs @@ -14,8 +14,10 @@ using Paramore.Brighter.Inbox.MySql; using Paramore.Brighter.Inbox.Sqlite; using Paramore.Brighter.MessagingGateway.RMQ; +using Paramore.Brighter.MySql; using Paramore.Brighter.ServiceActivator.Extensions.DependencyInjection; using Paramore.Brighter.ServiceActivator.Extensions.Hosting; +using Paramore.Brighter.Sqlite; using SalutationAnalytics.Database; using SalutationPorts.EntityMappers; using SalutationPorts.Policies; @@ -73,21 +75,17 @@ private static void ConfigureBrighter(HostBuilderContext hostContext, IServiceCo makeChannels: OnMissingChannel.Create), //change to OnMissingChannel.Validate if you have infrastructure declared elsewhere }; - var host = hostContext.HostingEnvironment.IsDevelopment() ? "localhost" : "rabbitmq"; - + var relationalDatabaseConfiguration = + new RelationalDatabaseConfiguration(DbConnectionString(hostContext), SchemaCreation.INBOX_TABLE_NAME); + services.AddSingleton(relationalDatabaseConfiguration); + var rmqConnection = new RmqMessagingGatewayConnection { - AmpqUri = new AmqpUriSpecification(new Uri($"amqp://guest:guest@{host}:5672")), Exchange = new Exchange("paramore.brighter.exchange") + AmpqUri = new AmqpUriSpecification(new Uri($"amqp://guest:guest@localhost:5672")), Exchange = new Exchange("paramore.brighter.exchange") }; var rmqMessageConsumerFactory = new RmqMessageConsumerFactory(rmqConnection); - var dbConfiguration = new RelationalDatabaseConfiguration( - DbConnectionString(hostContext), - inboxTableName:SchemaCreation.INBOX_TABLE_NAME); - - services.AddSingleton(dbConfiguration); - services.AddServiceActivator(options => { options.Subscriptions = subscriptions; @@ -120,7 +118,7 @@ private static void ConfigureBrighter(HostBuilderContext hostContext, IServiceCo ) .AutoFromAssemblies() .UseExternalInbox( - CreateInbox(hostContext, dbConfiguration), + CreateInbox(hostContext, relationalDatabaseConfiguration), new InboxConfiguration( scope: InboxScope.Commands, onceOnly: true, @@ -181,14 +179,16 @@ 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(); } diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs index 08459a84cf..519b909eea 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs +++ b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs @@ -37,6 +37,9 @@ public override async Task HandleAsync(AddGreeting addGreeting, Can using (var conn = await _transactionConnectionProvider.GetConnectionAsync(cancellationToken)) { await conn.OpenAsync(cancellationToken); + + //NOTE: We are using a transaction, but as we are using the Outbox, we ask the transaction connection provider for a transaction + //and allow it to manage the transaction for us. This is because we want to use the same transaction for the outgoing message using (var tx = await _transactionConnectionProvider.GetTransactionAsync(cancellationToken)) { try diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/AddPersonHandlerAsync.cs b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/AddPersonHandlerAsync.cs index 1ba829da8a..9385f37556 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/AddPersonHandlerAsync.cs +++ b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/AddPersonHandlerAsync.cs @@ -1,5 +1,4 @@ -using System.Data.Common; -using System.Threading; +using System.Threading; using System.Threading.Tasks; using DapperExtensions; using GreetingsEntities; @@ -12,18 +11,18 @@ namespace GreetingsPorts.Handlers { public class AddPersonHandlerAsync : RequestHandlerAsync { - private readonly IAmATransactionConnectionProvider _transactionConnectionProvider; + private readonly IAmARelationalDbConnectionProvider _connectionProvider; - public AddPersonHandlerAsync(IAmATransactionConnectionProvider transactionConnectionProvider) + public AddPersonHandlerAsync(IAmARelationalDbConnectionProvider connectionProvider) { - _transactionConnectionProvider = transactionConnectionProvider; + _connectionProvider = connectionProvider; } [RequestLoggingAsync(0, HandlerTiming.Before)] [UsePolicyAsync(step:1, policy: Policies.Retry.EXPONENTIAL_RETRYPOLICYASYNC)] public override async Task HandleAsync(AddPerson addPerson, CancellationToken cancellationToken = default) { - using (var connection = await _transactionConnectionProvider.GetConnectionAsync(cancellationToken)) + using (var connection = await _connectionProvider.GetConnectionAsync(cancellationToken)) { await connection.InsertAsync(new Person(addPerson.Name)); } diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/DeletePersonHandlerAsync.cs b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/DeletePersonHandlerAsync.cs index c801a54981..45928d42f5 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/DeletePersonHandlerAsync.cs +++ b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/DeletePersonHandlerAsync.cs @@ -14,31 +14,33 @@ namespace GreetingsPorts.Handlers { public class DeletePersonHandlerAsync : RequestHandlerAsync { - private readonly IAmATransactionConnectionProvider _transactionConnectionProvider; + private readonly IAmARelationalDbConnectionProvider _connectionProvider; - public DeletePersonHandlerAsync(IAmATransactionConnectionProvider transactionConnectionProvider) + public DeletePersonHandlerAsync(IAmARelationalDbConnectionProvider connectionProvider) { - _transactionConnectionProvider = transactionConnectionProvider; + _connectionProvider = connectionProvider; } [RequestLoggingAsync(0, HandlerTiming.Before)] [UsePolicyAsync(step:1, policy: Policies.Retry.EXPONENTIAL_RETRYPOLICYASYNC)] public async override Task HandleAsync(DeletePerson deletePerson, CancellationToken cancellationToken = default) { - using (var connection = await _transactionConnectionProvider.GetConnectionAsync(cancellationToken)) + using (var connection = await _connectionProvider.GetConnectionAsync(cancellationToken)) { await connection.OpenAsync(cancellationToken); - var tx = await _transactionConnectionProvider.GetTransactionAsync(cancellationToken); + + //NOTE: we are using a transaction, but a connection provider will not manage one for us, so we need to do it ourselves + var tx = await connection.BeginTransactionAsync(cancellationToken); try { var searchbyName = Predicates.Field(p => p.Name, Operator.Eq, deletePerson.Name); - var people = await _transactionConnectionProvider.GetConnection() + var people = await _connectionProvider.GetConnection() .GetListAsync(searchbyName, transaction: tx); var person = people.Single(); var deleteById = Predicates.Field(g => g.RecipientId, Operator.Eq, person.Id); - await _transactionConnectionProvider.GetConnection().DeleteAsync(deleteById, tx); + await _connectionProvider.GetConnection().DeleteAsync(deleteById, tx); await tx.CommitAsync(cancellationToken); } diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs index bedc412c60..be52a21589 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs +++ b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs @@ -19,6 +19,8 @@ using Paramore.Brighter; using Paramore.Brighter.Extensions.DependencyInjection; using Paramore.Brighter.MessagingGateway.Kafka; +using Paramore.Brighter.MySql; +using Paramore.Brighter.Sqlite; using Paramore.Darker.AspNetCore; using Paramore.Darker.Policies; using Paramore.Darker.QueryLogging; @@ -151,15 +153,13 @@ private static void ConfigureDapperSqlite(IServiceCollection services) { DapperExtensions.DapperExtensions.SqlDialect = new SqliteDialect(); DapperAsyncExtensions.SqlDialect = new SqliteDialect(); - services.AddScoped(); } private static void ConfigureDapperMySql(IServiceCollection services) { DapperExtensions.DapperExtensions.SqlDialect = new MySqlDialect(); DapperAsyncExtensions.SqlDialect = new MySqlDialect(); - services.AddScoped(); - } + } private void ConfigureBrighter(IServiceCollection services) { diff --git a/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Program.cs b/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Program.cs index 9e14c94e33..2b358b130e 100644 --- a/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Program.cs +++ b/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Program.cs @@ -89,7 +89,14 @@ private static void ConfigureBrighter(HostBuilderContext hostContext, IServiceCo new KafkaMessagingGatewayConfiguration { Name = "paramore.brighter", BootStrapServers = new[] { "localhost:9092" } } ); - var host = hostContext.HostingEnvironment.IsDevelopment() ? "localhost" : "rabbitmq"; + var relationalDatabaseConfiguration = + new RelationalDatabaseConfiguration( + DbConnectionString(hostContext), + SchemaCreation.INBOX_TABLE_NAME, + binaryMessagePayload:true + ); + + services.AddSingleton(relationalDatabaseConfiguration); services.AddServiceActivator(options => { @@ -128,7 +135,7 @@ private static void ConfigureBrighter(HostBuilderContext hostContext, IServiceCo ) .AutoFromAssemblies() .UseExternalInbox( - ConfigureInbox(hostContext), + ConfigureInbox(hostContext, relationalDatabaseConfiguration), new InboxConfiguration( scope: InboxScope.Commands, onceOnly: true, @@ -202,7 +209,7 @@ private static void ConfigureDapperMySql(IServiceCollection services) } - private static IAmAnInbox ConfigureInbox(HostBuilderContext hostContext) + private static IAmAnInbox ConfigureInbox(HostBuilderContext hostContext, IAmARelationalDatabaseConfiguration configuration) { if (hostContext.HostingEnvironment.IsDevelopment()) { diff --git a/src/Paramore.Brighter.MsSql.Azure/MsSqlAzureConnectionProviderBase.cs b/src/Paramore.Brighter.MsSql.Azure/MsSqlAzureConnectionProviderBase.cs index 96c4f759e3..bfb3af6347 100644 --- a/src/Paramore.Brighter.MsSql.Azure/MsSqlAzureConnectionProviderBase.cs +++ b/src/Paramore.Brighter.MsSql.Azure/MsSqlAzureConnectionProviderBase.cs @@ -8,17 +8,17 @@ namespace Paramore.Brighter.MsSql.Azure { - public abstract class MsSqlAzureConnectionProviderBase : IAmATransactionConnectionProvider + 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 @@ -29,26 +29,27 @@ protected MsSqlAzureConnectionProviderBase(RelationalDatabaseConfiguration confi { _cacheTokens = cacheTokens; _connectionString = configuration.ConnectionString; - _authenticationTokenScopes = new string[1] {_azureScope}; + AuthenticationTokenScopes = new string[1] {AZURE_SCOPE}; } - public void Close() { } - - public DbConnection 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() @@ -58,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; } @@ -80,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; } @@ -99,18 +98,5 @@ private async Task GetAccessTokenAsync(CancellationToken cancellationTok protected abstract Task GetAccessTokenFromProviderAsync(CancellationToken cancellationToken); - public DbTransaction GetTransaction() - { - //This Connection Factory does not support Transactions - return null; - } - - public Task GetTransactionAsync(CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - 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 5549043200..1cfd35d21d 100644 --- a/src/Paramore.Brighter.MsSql.Azure/MsSqlChainedConnectionProvider.cs +++ b/src/Paramore.Brighter.MsSql.Azure/MsSqlChainedConnectionProvider.cs @@ -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 07993e598c..e263cd0573 100644 --- a/src/Paramore.Brighter.MsSql.Azure/MsSqlDefaultAzureConnectionProvider.cs +++ b/src/Paramore.Brighter.MsSql.Azure/MsSqlDefaultAzureConnectionProvider.cs @@ -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 85d589e6e3..b77fcffdd5 100644 --- a/src/Paramore.Brighter.MsSql.Azure/MsSqlManagedIdentityConnectionProvider.cs +++ b/src/Paramore.Brighter.MsSql.Azure/MsSqlManagedIdentityConnectionProvider.cs @@ -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 b12c0344df..765110b511 100644 --- a/src/Paramore.Brighter.MsSql.Azure/MsSqlSharedTokenCacheConnectionProvider.cs +++ b/src/Paramore.Brighter.MsSql.Azure/MsSqlSharedTokenCacheConnectionProvider.cs @@ -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 135552487b..84b6308882 100644 --- a/src/Paramore.Brighter.MsSql.Azure/MsSqlVisualStudioConnectionProvider.cs +++ b/src/Paramore.Brighter.MsSql.Azure/MsSqlVisualStudioConnectionProvider.cs @@ -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.EntityFrameworkCore/MsSqlEntityFrameworkCoreConnectionProvider.cs b/src/Paramore.Brighter.MsSql.EntityFrameworkCore/MsSqlEntityFrameworkCoreConnectionProvider.cs index 604ae2d32b..9497a100c6 100644 --- a/src/Paramore.Brighter.MsSql.EntityFrameworkCore/MsSqlEntityFrameworkCoreConnectionProvider.cs +++ b/src/Paramore.Brighter.MsSql.EntityFrameworkCore/MsSqlEntityFrameworkCoreConnectionProvider.cs @@ -7,7 +7,7 @@ namespace Paramore.Brighter.MsSql.EntityFrameworkCore { - public class MsSqlEntityFrameworkCoreConnectionProvider : RelationalDbConnectionProvider where T : DbContext + public class MsSqlEntityFrameworkCoreConnectionProvider : RelationalDbTransactionProvider where T : DbContext { private readonly T _context; @@ -19,6 +19,31 @@ public MsSqlEntityFrameworkCoreConnectionProvider(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; + } + 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 diff --git a/src/Paramore.Brighter.MySql.EntityFrameworkCore/MySqlEntityFrameworkConnectionProvider.cs b/src/Paramore.Brighter.MySql.EntityFrameworkCore/MySqlEntityFrameworkConnectionProvider.cs index 0ec87bdb47..8e82d5d6cc 100644 --- a/src/Paramore.Brighter.MySql.EntityFrameworkCore/MySqlEntityFrameworkConnectionProvider.cs +++ b/src/Paramore.Brighter.MySql.EntityFrameworkCore/MySqlEntityFrameworkConnectionProvider.cs @@ -24,6 +24,31 @@ 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 /// diff --git a/src/Paramore.Brighter.MySql/MySqlUnitOfWork.cs b/src/Paramore.Brighter.MySql/MySqlUnitOfWork.cs index bae6317655..43a14e0b6d 100644 --- a/src/Paramore.Brighter.MySql/MySqlUnitOfWork.cs +++ b/src/Paramore.Brighter.MySql/MySqlUnitOfWork.cs @@ -49,6 +49,21 @@ public MySqlUnitOfWork(IAmARelationalDatabaseConfiguration configuration) 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 diff --git a/src/Paramore.Brighter.Outbox.MySql/ServiceCollectionExtensions.cs b/src/Paramore.Brighter.Outbox.MySql/ServiceCollectionExtensions.cs index 290d26c1f4..f87f5fcdea 100644 --- a/src/Paramore.Brighter.Outbox.MySql/ServiceCollectionExtensions.cs +++ b/src/Paramore.Brighter.Outbox.MySql/ServiceCollectionExtensions.cs @@ -50,7 +50,7 @@ public static IBrighterBuilder UseMySqTransactionConnectionProvider( ServiceLifetime serviceLifetime = ServiceLifetime.Scoped ) { - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmABoxTransactionProvider), transactionProvider, serviceLifetime)); + brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmATransactionConnectionProvider), transactionProvider, serviceLifetime)); return brighterBuilder; } diff --git a/src/Paramore.Brighter.PostgreSql.EntityFrameworkCore/PostgreSqlEntityFrameworkConnectionProvider.cs b/src/Paramore.Brighter.PostgreSql.EntityFrameworkCore/PostgreSqlEntityFrameworkConnectionProvider.cs index 3b6aeb9eff..5e11982224 100644 --- a/src/Paramore.Brighter.PostgreSql.EntityFrameworkCore/PostgreSqlEntityFrameworkConnectionProvider.cs +++ b/src/Paramore.Brighter.PostgreSql.EntityFrameworkCore/PostgreSqlEntityFrameworkConnectionProvider.cs @@ -12,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 : RelationalDbConnectionProvider, IAmATransactionConnectionProvider where T : DbContext + public class PostgreSqlEntityFrameworkConnectionProvider : RelationalDbTransactionProvider where T : DbContext { private readonly T _context; @@ -24,6 +24,31 @@ 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 diff --git a/src/Paramore.Brighter.PostgreSql/PostgreSqlNpgsqlUnitOfWork.cs b/src/Paramore.Brighter.PostgreSql/PostgreSqlNpgsqlUnitOfWork.cs index aa9d8d2ccd..5d8469ea9c 100644 --- a/src/Paramore.Brighter.PostgreSql/PostgreSqlNpgsqlUnitOfWork.cs +++ b/src/Paramore.Brighter.PostgreSql/PostgreSqlNpgsqlUnitOfWork.cs @@ -25,6 +25,21 @@ public PostgreSqlNpgsqlUnitOfWork(IAmARelationalDatabaseConfiguration configurat _connectionString = configuration.ConnectionString; } + + /// + /// 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 diff --git a/src/Paramore.Brighter.Sqlite.EntityFrameworkCore/SqliteEntityFrameworkConnectionProvider.cs b/src/Paramore.Brighter.Sqlite.EntityFrameworkCore/SqliteEntityFrameworkConnectionProvider.cs index 6b62b20e31..1d966f51bf 100644 --- a/src/Paramore.Brighter.Sqlite.EntityFrameworkCore/SqliteEntityFrameworkConnectionProvider.cs +++ b/src/Paramore.Brighter.Sqlite.EntityFrameworkCore/SqliteEntityFrameworkConnectionProvider.cs @@ -11,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 : RelationalDbConnectionProvider, IAmATransactionConnectionProvider where T: DbContext + public class SqliteEntityFrameworkConnectionProvider : RelationalDbTransactionProvider where T: DbContext { private readonly T _context; @@ -23,6 +23,31 @@ 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 diff --git a/src/Paramore.Brighter.Sqlite/SqliteUnitOfWork.cs b/src/Paramore.Brighter.Sqlite/SqliteUnitOfWork.cs index 4a96c15421..21311fb7e7 100644 --- a/src/Paramore.Brighter.Sqlite/SqliteUnitOfWork.cs +++ b/src/Paramore.Brighter.Sqlite/SqliteUnitOfWork.cs @@ -48,6 +48,21 @@ public SqliteUnitOfWork(IAmARelationalDatabaseConfiguration configuration) 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 diff --git a/src/Paramore.Brighter/IAmARelationalDbConnectionProvider.cs b/src/Paramore.Brighter/IAmARelationalDbConnectionProvider.cs index ba56b520a1..59b0c4a735 100644 --- a/src/Paramore.Brighter/IAmARelationalDbConnectionProvider.cs +++ b/src/Paramore.Brighter/IAmARelationalDbConnectionProvider.cs @@ -14,6 +14,18 @@ public interface IAmARelationalDbConnectionProvider /// Close any open connection or transaction /// void Close(); + + /// + /// Commit any transaction that we are managing + /// + void Commit(); + + /// + /// Commit any transaction that we are managing + /// + /// A cancellation token + /// + Task CommitAsync(CancellationToken cancellationToken = default); /// /// Gets a existing Connection; creates a new one if it does not exist diff --git a/src/Paramore.Brighter/RelationalDbConnectionProvider.cs b/src/Paramore.Brighter/RelationalDbConnectionProvider.cs index 5e4f5c6580..d851504f32 100644 --- a/src/Paramore.Brighter/RelationalDbConnectionProvider.cs +++ b/src/Paramore.Brighter/RelationalDbConnectionProvider.cs @@ -25,6 +25,11 @@ public virtual void Close() { } /// 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 diff --git a/src/Paramore.Brighter/RelationalDbTransactionProvider.cs b/src/Paramore.Brighter/RelationalDbTransactionProvider.cs index 136035c117..65784c0e92 100644 --- a/src/Paramore.Brighter/RelationalDbTransactionProvider.cs +++ b/src/Paramore.Brighter/RelationalDbTransactionProvider.cs @@ -27,7 +27,7 @@ public virtual void Close() /// /// Commit the transaction /// - public void Commit() + public virtual void Commit() { if (HasOpenTransaction) { @@ -35,6 +35,21 @@ public void 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 From ddab3821f98a527e5483cfec7ad890739a973d58 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Thu, 11 May 2023 15:25:22 +0100 Subject: [PATCH 43/89] Add commit and rollback to the provider --- .../.idea/httpRequests/http-requests-log.http | 252 +++++++++--------- .../Handlers/AddGreetingHandlerAsync.cs | 59 ++-- samples/WebAPI_Dapper_Kafka/tests.http | 2 +- ...qlEntityFrameworkCoreConnectionProvider.cs | 33 ++- .../MsSqlSqlAuthUnitOfWork.cs | 2 - .../MySqlEntityFrameworkConnectionProvider.cs | 17 +- .../MySqlUnitOfWork.cs | 3 - .../PostgreSqlNpgsqlUnitOfWork.cs | 4 - .../SqliteUnitOfWork.cs | 8 +- .../IAmARelationalDbConnectionProvider.cs | 10 + .../RelationalDbConnectionProvider.cs | 21 +- .../RelationalDbTransactionProvider.cs | 37 ++- 12 files changed, 265 insertions(+), 183 deletions(-) diff --git a/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http b/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http index 5ead0a4d29..b3ef1a309a 100644 --- a/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http +++ b/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http @@ -1,3 +1,129 @@ +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-05-11T110559.200.json + +### + +POST http://localhost:5000/People/new +Content-Type: application/json +Content-Length: 26 +Connection: Keep-Alive +User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) +Accept-Encoding: br,deflate,gzip,x-gzip + +{ + "Name" : "Ned Stark" +} + +<> 2023-05-11T110539.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-05-11T083445.500.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-05-11T083438.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-05-11T083357.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-05-11T083353.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-05-11T082416.500.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-05-11T082408.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-05-11T082332.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-05-11T082326.200.json + +### + POST http://localhost:5000/People/new Content-Type: application/json Content-Length: 23 @@ -486,129 +612,3 @@ Accept-Encoding: br,deflate,gzip,x-gzip ### -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-05-03T190816.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-05-03T190627.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-05-03T190622.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-05-03T190440.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-05-03T190431.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" -} - -<> 2023-05-03T190407.500.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" -} - -<> 2023-05-03T190358.200.json - -### - -POST http://localhost:5000/Greetings/Tyrion/new -Content-Type: application/json - -{ - "Greeting" : "I drink, and I know things" -} - -<> 2022-11-08T211212.200.json - -### - -POST http://localhost:5000/Greetings/Tyrion/new -Content-Type: application/json - -{ - "Greeting" : "I drink, and I know things" -} - -<> 2022-11-08T211208.200.json - -### - -POST http://localhost:5000/Greetings/Tyrion/new -Content-Type: application/json - -{ - "Greeting" : "I drink, and I know things" -} - -<> 2022-11-08T211200.200.json - -### - diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs index 519b909eea..389a0121d4 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs +++ b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs @@ -34,40 +34,41 @@ public override async Task HandleAsync(AddGreeting addGreeting, Can { var posts = new List(); - using (var conn = await _transactionConnectionProvider.GetConnectionAsync(cancellationToken)) + //NOTE: We are using the transaction connection provider to get a connection, so we do not own the connection + //and we should use the transaction connection provider to manage it + var conn = await _transactionConnectionProvider.GetConnectionAsync(cancellationToken); + //NOTE: We are using a transaction, but as we are using the Outbox, we ask the transaction connection provider for a transaction + //and allow it to manage the transaction for us. This is because we want to use the same transaction for the outgoing message + var tx = await _transactionConnectionProvider.GetTransactionAsync(cancellationToken); + try { - await conn.OpenAsync(cancellationToken); - - //NOTE: We are using a transaction, but as we are using the Outbox, we ask the transaction connection provider for a transaction - //and allow it to manage the transaction for us. This is because we want to use the same transaction for the outgoing message - using (var tx = await _transactionConnectionProvider.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 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); + var greeting = new Greeting(addGreeting.Greeting, person); - //write the added child entity to the Db - await conn.InsertAsync(greeting, tx); + //write the added child entity to the Db + await conn.InsertAsync(greeting, 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()), - cancellationToken: cancellationToken)); + //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); - } - 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(addGreeting, cancellationToken); - } - } + //commit both new greeting and outgoing message + await _transactionConnectionProvider.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); + return await base.HandleAsync(addGreeting, cancellationToken); + } + finally + { + //in the finally block, tell the transaction provider that we are done. + _transactionConnectionProvider.Close(); } //Send this message via a transport. We need the ids to send just the messages here, not all outstanding ones. diff --git a/samples/WebAPI_Dapper_Kafka/tests.http b/samples/WebAPI_Dapper_Kafka/tests.http index 56382375bb..30db899ee4 100644 --- a/samples/WebAPI_Dapper_Kafka/tests.http +++ b/samples/WebAPI_Dapper_Kafka/tests.http @@ -8,7 +8,7 @@ POST http://localhost:5000/People/new HTTP/1.1 Content-Type: application/json { - "Name" : "Tyrion" + "Name" : "" } ### Now see that person diff --git a/src/Paramore.Brighter.MsSql.EntityFrameworkCore/MsSqlEntityFrameworkCoreConnectionProvider.cs b/src/Paramore.Brighter.MsSql.EntityFrameworkCore/MsSqlEntityFrameworkCoreConnectionProvider.cs index 9497a100c6..2246105037 100644 --- a/src/Paramore.Brighter.MsSql.EntityFrameworkCore/MsSqlEntityFrameworkCoreConnectionProvider.cs +++ b/src/Paramore.Brighter.MsSql.EntityFrameworkCore/MsSqlEntityFrameworkCoreConnectionProvider.cs @@ -1,4 +1,5 @@ -using System.Data.Common; +using System; +using System.Data.Common; using System.Threading; using System.Threading.Tasks; using Microsoft.Data.SqlClient; @@ -43,7 +44,11 @@ public override 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 + /// + /// 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 @@ -53,6 +58,12 @@ public override DbConnection GetConnection() 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. + /// 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 @@ -62,13 +73,27 @@ public override async Task GetConnectionAsync(CancellationToken ca return connection; } + /// + /// 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 override bool HasOpenTransaction { get => _context.Database.CurrentTransaction != null; } - public override 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/MsSqlSqlAuthUnitOfWork.cs b/src/Paramore.Brighter.MsSql/MsSqlSqlAuthUnitOfWork.cs index 2e2a274409..3244c98e47 100644 --- a/src/Paramore.Brighter.MsSql/MsSqlSqlAuthUnitOfWork.cs +++ b/src/Paramore.Brighter.MsSql/MsSqlSqlAuthUnitOfWork.cs @@ -55,7 +55,6 @@ public override async Task GetConnectionAsync(CancellationToken ca public override DbTransaction GetTransaction() { if (Connection == null) Connection = GetConnection(); - if (Connection.State != ConnectionState.Open) Connection.Open(); if (!HasOpenTransaction) Transaction = ((SqlConnection) Connection).BeginTransaction(); return Transaction; @@ -69,7 +68,6 @@ public override DbTransaction GetTransaction() public override async Task GetTransactionAsync(CancellationToken cancellationToken = default) { if (Connection == null) Connection = await GetConnectionAsync(cancellationToken); - if (Connection.State != ConnectionState.Open) await Connection.OpenAsync(cancellationToken); if (!HasOpenTransaction) Transaction = ((SqlConnection) Connection).BeginTransaction(); return Transaction; diff --git a/src/Paramore.Brighter.MySql.EntityFrameworkCore/MySqlEntityFrameworkConnectionProvider.cs b/src/Paramore.Brighter.MySql.EntityFrameworkCore/MySqlEntityFrameworkConnectionProvider.cs index 8e82d5d6cc..abdbe63ff5 100644 --- a/src/Paramore.Brighter.MySql.EntityFrameworkCore/MySqlEntityFrameworkConnectionProvider.cs +++ b/src/Paramore.Brighter.MySql.EntityFrameworkCore/MySqlEntityFrameworkConnectionProvider.cs @@ -1,4 +1,5 @@ -using System.Data.Common; +using System; +using System.Data.Common; using System.Threading; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; @@ -80,8 +81,18 @@ public override DbTransaction GetTransaction() { return _context.Database.CurrentTransaction?.GetDbTransaction(); } + + /// + /// Rolls back a transaction + /// + public override async Task RollbackAsync(CancellationToken cancellationToken = default) + { + if (HasOpenTransaction) + { + try { await ((MySqlTransaction)GetTransaction()).RollbackAsync(cancellationToken); } catch (Exception) { /* Ignore*/} + Transaction = null; + } + } - public override bool HasOpenTransaction { get => _context.Database.CurrentTransaction != null; } - public override bool IsSharedConnection { get => true; } } } diff --git a/src/Paramore.Brighter.MySql/MySqlUnitOfWork.cs b/src/Paramore.Brighter.MySql/MySqlUnitOfWork.cs index 43a14e0b6d..a30c435d20 100644 --- a/src/Paramore.Brighter.MySql/MySqlUnitOfWork.cs +++ b/src/Paramore.Brighter.MySql/MySqlUnitOfWork.cs @@ -96,7 +96,6 @@ public override async Task GetConnectionAsync(CancellationToken ca public override DbTransaction GetTransaction() { if (Connection == null) Connection = GetConnection(); - if (Connection.State != ConnectionState.Open) Connection.Open(); if (!HasOpenTransaction) Transaction = ((MySqlConnection) Connection).BeginTransaction(); return Transaction; @@ -110,8 +109,6 @@ public override DbTransaction GetTransaction() public override async Task GetTransactionAsync(CancellationToken cancellationToken = default) { if (Connection == null) Connection = await GetConnectionAsync(cancellationToken); - if (Connection.State != ConnectionState.Open) - await Connection.OpenAsync(cancellationToken); if (!HasOpenTransaction) Transaction = await ((MySqlConnection) Connection).BeginTransactionAsync(cancellationToken); return Transaction; diff --git a/src/Paramore.Brighter.PostgreSql/PostgreSqlNpgsqlUnitOfWork.cs b/src/Paramore.Brighter.PostgreSql/PostgreSqlNpgsqlUnitOfWork.cs index 5d8469ea9c..ac0c5b374e 100644 --- a/src/Paramore.Brighter.PostgreSql/PostgreSqlNpgsqlUnitOfWork.cs +++ b/src/Paramore.Brighter.PostgreSql/PostgreSqlNpgsqlUnitOfWork.cs @@ -73,8 +73,6 @@ public override async Task GetConnectionAsync(CancellationToken ca public override DbTransaction GetTransaction() { if (Connection == null) Connection = GetConnection(); - if (Connection.State != ConnectionState.Open) - Connection.Open(); if (!HasOpenTransaction) Transaction = ((NpgsqlConnection)Connection).BeginTransaction(); return Transaction; @@ -88,8 +86,6 @@ public override DbTransaction GetTransaction() public override async Task GetTransactionAsync(CancellationToken cancellationToken = default) { if (Connection == null) Connection = await GetConnectionAsync(cancellationToken); - if (Connection.State != ConnectionState.Open) - await Connection.OpenAsync(cancellationToken); if (!HasOpenTransaction) Transaction = ((NpgsqlConnection)Connection).BeginTransaction(); return Transaction; diff --git a/src/Paramore.Brighter.Sqlite/SqliteUnitOfWork.cs b/src/Paramore.Brighter.Sqlite/SqliteUnitOfWork.cs index 21311fb7e7..36d0a73704 100644 --- a/src/Paramore.Brighter.Sqlite/SqliteUnitOfWork.cs +++ b/src/Paramore.Brighter.Sqlite/SqliteUnitOfWork.cs @@ -98,9 +98,7 @@ public override async Task GetConnectionAsync(CancellationToken ca /// DbTransaction public override DbTransaction GetTransaction() { - if (Connection == null) {Connection = new SqliteConnection(_connectionString);} - if (Connection.State != ConnectionState.Open) - Connection.Open(); + if (Connection == null) Connection = GetConnection(); if (!HasOpenTransaction) Transaction = Connection.BeginTransaction(); return Transaction; @@ -113,9 +111,7 @@ public override DbTransaction GetTransaction() /// DbTransaction public override async Task GetTransactionAsync(CancellationToken cancellationToken = default) { - if (Connection == null) { Connection = new SqliteConnection(_connectionString);} - if (Connection.State != ConnectionState.Open) - await Connection.OpenAsync(cancellationToken); + if (Connection == null) Connection = await GetConnectionAsync(cancellationToken); if (!HasOpenTransaction) Transaction = await Connection.BeginTransactionAsync(cancellationToken); return Transaction; diff --git a/src/Paramore.Brighter/IAmARelationalDbConnectionProvider.cs b/src/Paramore.Brighter/IAmARelationalDbConnectionProvider.cs index 59b0c4a735..98889dfbc3 100644 --- a/src/Paramore.Brighter/IAmARelationalDbConnectionProvider.cs +++ b/src/Paramore.Brighter/IAmARelationalDbConnectionProvider.cs @@ -64,5 +64,15 @@ public interface IAmARelationalDbConnectionProvider /// 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/RelationalDbConnectionProvider.cs b/src/Paramore.Brighter/RelationalDbConnectionProvider.cs index d851504f32..6607c3b5f1 100644 --- a/src/Paramore.Brighter/RelationalDbConnectionProvider.cs +++ b/src/Paramore.Brighter/RelationalDbConnectionProvider.cs @@ -68,14 +68,33 @@ public virtual Task GetTransactionAsync(CancellationToken cancell /// /// 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. diff --git a/src/Paramore.Brighter/RelationalDbTransactionProvider.cs b/src/Paramore.Brighter/RelationalDbTransactionProvider.cs index 65784c0e92..0c3a196db4 100644 --- a/src/Paramore.Brighter/RelationalDbTransactionProvider.cs +++ b/src/Paramore.Brighter/RelationalDbTransactionProvider.cs @@ -17,9 +17,12 @@ public abstract class RelationalDbTransactionProvider : IAmATransactionConnectio /// public virtual void Close() { - Transaction?.Dispose(); - Transaction = null; - Connection.Close(); + if (!HasOpenTransaction) + { + Transaction?.Dispose(); + Transaction = null; + } + if (!IsSharedConnection) Connection?.Close(); } @@ -73,7 +76,7 @@ public virtual Task GetConnectionAsync(CancellationToken cancellat /// /// 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. + /// You should use the commit transaction using the Commit method. /// /// A database transaction public virtual DbTransaction GetTransaction() @@ -112,6 +115,32 @@ public virtual Task GetTransactionAsync(CancellationToken cancell /// 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. From 9479b902146715ad6e47679cfd41db0aa300c7c0 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Mon, 15 May 2023 11:05:07 +0100 Subject: [PATCH 44/89] Binary not being set; relational db connection not transaction --- .../.idea/httpRequests/http-requests-log.http | 260 +++++++++--------- .../FIndGreetingsForPersonHandlerAsync.cs | 8 +- .../Handlers/FindPersonByNameHandlerAsync.cs | 8 +- .../GreetingsWeb/Database/SchemaCreation.cs | 26 +- .../GreetingsWeb/Program.cs | 4 +- .../GreetingsWeb/Startup.cs | 4 +- .../Migrations/20220527_MySqlMigrations.cs | 2 +- samples/WebAPI_Dapper_Kafka/tests.http | 2 +- .../MySqlOutbox.cs | 22 +- .../MySqlQueries.cs | 14 +- 10 files changed, 188 insertions(+), 162 deletions(-) diff --git a/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http b/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http index b3ef1a309a..4034ac6fcf 100644 --- a/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http +++ b/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http @@ -9,22 +9,22 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-05-11T110559.200.json +<> 2023-05-15T110426.200.json ### -POST http://localhost:5000/People/new +POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json -Content-Length: 26 +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" : "Ned Stark" + "Greeting" : "I drink, and I know things" } -<> 2023-05-11T110539.200.json +<> 2023-05-15T110422.200.json ### @@ -39,7 +39,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-05-11T083445.500.json +<> 2023-05-15T102620.200.json ### @@ -48,7 +48,7 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-05-11T083438.200.json +<> 2023-05-15T102555.200.json ### @@ -63,7 +63,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-05-11T083357.200.json +<> 2023-05-15T101101.200.json ### @@ -72,22 +72,22 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-05-11T083353.200.json +<> 2023-05-15T101027.200.json ### -POST http://localhost:5000/Greetings/Tyrion/new +POST http://localhost:5000/People/new Content-Type: application/json -Content-Length: 47 +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" } -<> 2023-05-11T082416.500.json +<> 2023-05-15T101025.200.json ### @@ -96,7 +96,7 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-05-11T082408.200.json +<> 2023-05-15T101020.500.json ### @@ -111,7 +111,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-05-11T082332.200.json +<> 2023-05-14T210531.500.json ### @@ -120,7 +120,7 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-05-11T082326.200.json +<> 2023-05-14T210524.200.json ### @@ -135,16 +135,20 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Name" : "Tyrion" } -<> 2023-05-10T193837.500.json +<> 2023-05-14T210520.200.json ### -GET http://localhost:5000/People/Tyrion +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 -<> 2023-05-10T193523.500.json +{ + "Name" : "Tyrion" +} ### @@ -159,7 +163,20 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Name" : "Tyrion" } -<> 2023-05-10T193516.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" +} + +<> 2023-05-14T203228.200.json ### @@ -174,7 +191,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-05-10T140942.200.json +<> 2023-05-14T203141.200.json ### @@ -183,46 +200,52 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-05-10T140922.200.json +<> 2023-05-14T203136.200.json ### -POST http://localhost:5000/People/new +POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json -Content-Length: 23 +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" } -<> 2023-05-10T140918.500.json +<> 2023-05-11T110559.200.json ### -POST http://localhost:5000/Greetings/Tyrion/new +POST http://localhost:5000/People/new Content-Type: application/json -Content-Length: 47 +Content-Length: 26 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" : "Ned Stark" } -<> 2023-05-08T200327.200.json +<> 2023-05-11T110539.200.json ### -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 -<> 2023-05-08T200232.200.json +{ + "Greeting" : "I drink, and I know things" +} + +<> 2023-05-11T083445.500.json ### @@ -231,30 +254,32 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-05-08T155920.200.json +<> 2023-05-11T083438.200.json ### -POST http://localhost:5000/People/new +POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json -Content-Length: 23 +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" } -<> 2023-05-08T155916.500.json +<> 2023-05-11T083357.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 +<> 2023-05-11T083353.200.json + ### POST http://localhost:5000/Greetings/Tyrion/new @@ -268,7 +293,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-05-08T132600.200.json +<> 2023-05-11T082416.500.json ### @@ -277,7 +302,7 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-05-08T132419.200.json +<> 2023-05-11T082408.200.json ### @@ -292,7 +317,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-05-08T131957.200.json +<> 2023-05-11T082332.200.json ### @@ -301,7 +326,22 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-05-08T131953.200.json +<> 2023-05-11T082326.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" +} + +<> 2023-05-10T193837.500.json ### @@ -310,7 +350,7 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-05-08T131712.200.json +<> 2023-05-10T193523.500.json ### @@ -325,16 +365,22 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Name" : "Tyrion" } -<> 2023-05-08T131709.200.json +<> 2023-05-10T193516.500.json ### -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 -<> 2023-05-08T131704.500.json +{ + "Greeting" : "I drink, and I know things" +} + +<> 2023-05-10T140942.200.json ### @@ -343,22 +389,22 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-05-08T131659.500.json +<> 2023-05-10T140922.200.json ### -POST http://localhost:5000/Greetings/Tyrion/new +POST http://localhost:5000/People/new Content-Type: application/json -Content-Length: 47 +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" } -<> 2023-05-06T164909.500.json +<> 2023-05-10T140918.500.json ### @@ -373,22 +419,16 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-05-06T164825.500.json +<> 2023-05-08T200327.200.json ### -POST http://localhost:5000/Greetings/Tyrion/new -Content-Type: application/json -Content-Length: 47 +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-05-06T164716.500.json +<> 2023-05-08T200232.200.json ### @@ -397,7 +437,7 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-05-06T164705.200.json +<> 2023-05-08T155920.200.json ### @@ -412,52 +452,54 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Name" : "Tyrion" } -<> 2023-05-06T164701.200.json +<> 2023-05-08T155916.500.json ### -GET http://localhost:5000/People/Tyrion +DELETE 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-05-06T164657.500.json - ### -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 -<> 2023-05-06T164601.200.json +{ + "Greeting" : "I drink, and I know things" +} + +<> 2023-05-08T132600.200.json ### -POST http://localhost:5000/People/new -Content-Type: application/json -Content-Length: 23 +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 -{ - "Name" : "Tyrion" -} +<> 2023-05-08T132419.200.json ### -POST http://localhost:5000/People/new +POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json -Content-Length: 23 +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" } +<> 2023-05-08T131957.200.json + ### GET http://localhost:5000/People/Tyrion @@ -465,6 +507,8 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip +<> 2023-05-08T131953.200.json + ### GET http://localhost:5000/People/Tyrion @@ -472,50 +516,40 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip +<> 2023-05-08T131712.200.json + ### -POST http://localhost:5000/Greetings/Tyrion/new +POST http://localhost:5000/People/new Content-Type: application/json -Content-Length: 47 +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" } -<> 2023-05-05T082700.200.json +<> 2023-05-08T131709.200.json ### -POST http://localhost:5000/Greetings/Tyrion/new -Content-Type: application/json -Content-Length: 47 +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-05-05T082658.200.json +<> 2023-05-08T131704.500.json ### -POST http://localhost:5000/Greetings/Tyrion/new -Content-Type: application/json -Content-Length: 47 +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-05-05T082656.200.json +<> 2023-05-08T131659.500.json ### @@ -530,7 +564,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-05-05T082325.200.json +<> 2023-05-06T164909.500.json ### @@ -545,31 +579,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-05-05T081253.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-05-05T081244.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" -} - -<> 2023-05-05T081241.200.json +<> 2023-05-06T164825.500.json ### @@ -584,7 +594,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-05-04T205523.200.json +<> 2023-05-06T164716.500.json ### @@ -593,7 +603,7 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-05-04T205517.200.json +<> 2023-05-06T164705.200.json ### @@ -608,7 +618,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Name" : "Tyrion" } -<> 2023-05-04T205509.500.json +<> 2023-05-06T164701.200.json ### diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/FIndGreetingsForPersonHandlerAsync.cs b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/FIndGreetingsForPersonHandlerAsync.cs index 147394f1c1..e51de8c714 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/FIndGreetingsForPersonHandlerAsync.cs +++ b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/FIndGreetingsForPersonHandlerAsync.cs @@ -16,11 +16,11 @@ namespace GreetingsPorts.Handlers { public class FIndGreetingsForPersonHandlerAsync : QueryHandlerAsync { - private readonly IAmATransactionConnectionProvider _transactionConnectionProvider; + private readonly IAmARelationalDbConnectionProvider _relationalDbConnectionProvider; - public FIndGreetingsForPersonHandlerAsync(IAmATransactionConnectionProvider transactionConnectionProvider) + public FIndGreetingsForPersonHandlerAsync(IAmARelationalDbConnectionProvider relationalDbConnectionProvider) { - _transactionConnectionProvider = transactionConnectionProvider; + _relationalDbConnectionProvider = relationalDbConnectionProvider; } [QueryLogging(0)] @@ -35,7 +35,7 @@ public FIndGreetingsForPersonHandlerAsync(IAmATransactionConnectionProvider tran from Person p inner join Greeting g on g.Recipient_Id = p.Id"; - using (var connection = _transactionConnectionProvider.GetConnection()) + using (var connection = _relationalDbConnectionProvider.GetConnection()) { var people = await connection.QueryAsync(sql, (person, greeting) => { diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/FindPersonByNameHandlerAsync.cs b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/FindPersonByNameHandlerAsync.cs index 89480b8a5f..d10c65e23d 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/FindPersonByNameHandlerAsync.cs +++ b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/FindPersonByNameHandlerAsync.cs @@ -17,11 +17,11 @@ namespace GreetingsPorts.Handlers { public class FindPersonByNameHandlerAsync : QueryHandlerAsync { - private readonly IAmATransactionConnectionProvider _transactionConnectionProvider; + private readonly IAmARelationalDbConnectionProvider _relationalDbConnectionProvider; - public FindPersonByNameHandlerAsync(IAmATransactionConnectionProvider transactionConnectionProvider) + public FindPersonByNameHandlerAsync(IAmARelationalDbConnectionProvider relationalDbConnectionProvider) { - _transactionConnectionProvider = transactionConnectionProvider; + _relationalDbConnectionProvider = relationalDbConnectionProvider; } [QueryLogging(0)] @@ -29,7 +29,7 @@ public FindPersonByNameHandlerAsync(IAmATransactionConnectionProvider transactio public override async Task ExecuteAsync(FindPersonByName query, CancellationToken cancellationToken = new CancellationToken()) { var searchbyName = Predicates.Field(p => p.Name, Operator.Eq, query.Name); - using (var connection = await _transactionConnectionProvider.GetConnectionAsync(cancellationToken)) + using (var connection = await _relationalDbConnectionProvider .GetConnectionAsync(cancellationToken)) { var people = await connection.GetListAsync(searchbyName); var person = people.Single(); diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/SchemaCreation.cs b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/SchemaCreation.cs index 08dc59bc2a..843c248b92 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/SchemaCreation.cs +++ b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/SchemaCreation.cs @@ -61,7 +61,7 @@ public static IHost MigrateDatabase(this IHost webHost) return webHost; } - public static IHost CreateOutbox(this IHost webHost) + public static IHost CreateOutbox(this IHost webHost, bool hasBinaryPayload) { using (var scope = webHost.Services.CreateScope()) { @@ -69,7 +69,7 @@ public static IHost CreateOutbox(this IHost webHost) var env = services.GetService(); var config = services.GetService(); - CreateOutbox(config, env); + CreateOutbox(config, env, hasBinaryPayload); } return webHost; @@ -85,16 +85,16 @@ private static void CreateDatabaseIfNotExists(DbConnection conn) } - 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: hasBinaryPayload); else - CreateOutboxProduction(GetDatabaseType(config), connectionString); + CreateOutboxProduction(GetDatabaseType(config), connectionString, hasBinaryPayload: hasBinaryPayload); } catch (System.Exception e) { @@ -104,12 +104,12 @@ private static void CreateOutbox(IConfiguration config, IWebHostEnvironment env) } } - private static void CreateOutboxDevelopment(string connectionString) + private static void CreateOutboxDevelopment(string connectionString, bool hasBinaryPayload) { - CreateOutboxSqlite(connectionString); + CreateOutboxSqlite(connectionString, hasBinaryPayload); } - private static void CreateOutboxSqlite(string connectionString) + private static void CreateOutboxSqlite(string connectionString, bool hasBinaryPayload) { using var sqlConnection = new SqliteConnection(connectionString); sqlConnection.Open(); @@ -121,23 +121,23 @@ private static void CreateOutboxSqlite(string connectionString) if (reader.HasRows) return; using var command = sqlConnection.CreateCommand(); - command.CommandText = SqliteOutboxBuilder.GetDDL(OUTBOX_TABLE_NAME); + command.CommandText = SqliteOutboxBuilder.GetDDL(OUTBOX_TABLE_NAME, hasBinaryMessagePayload: hasBinaryPayload); command.ExecuteScalar(); } - 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; default: throw new InvalidOperationException("Could not create instance of Outbox for unknown Db type"); } } - private static void CreateOutboxMySql(string connectionString) + private static void CreateOutboxMySql(string connectionString, bool hasBinaryPayload) { using var sqlConnection = new MySqlConnection(connectionString); sqlConnection.Open(); @@ -149,7 +149,7 @@ private static void CreateOutboxMySql(string connectionString) if (exists) return; using var command = sqlConnection.CreateCommand(); - command.CommandText = MySqlOutboxBuilder.GetDDL(OUTBOX_TABLE_NAME); + command.CommandText = MySqlOutboxBuilder.GetDDL(OUTBOX_TABLE_NAME, hasBinaryMessagePayload: hasBinaryPayload); command.ExecuteScalar(); } diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Program.cs b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Program.cs index 749522914a..13e578f898 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Program.cs +++ b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Program.cs @@ -16,7 +16,9 @@ public static void Main(string[] args) host.CheckDbIsUp(); host.MigrateDatabase(); - host.CreateOutbox(); + //NOTE: Because we use the Serdes serializer with Kafka, that adds scheme registry info to the message payload + //we use a binary payload outbox to avoid corruption. + host.CreateOutbox(hasBinaryPayload: true); host.Run(); } diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs index be52a21589..d580dad5e6 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs +++ b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs @@ -165,7 +165,9 @@ private void ConfigureBrighter(IServiceCollection services) { var outboxConfiguration = new RelationalDatabaseConfiguration( DbConnectionString(), - outBoxTableName:_outBoxTableName + outBoxTableName:_outBoxTableName, + //NOTE: With the Serdes serializer, if we don't use a binary payload, the payload will be corrupted + binaryMessagePayload: true ); services.AddSingleton(outboxConfiguration); diff --git a/samples/WebAPI_Dapper_Kafka/Salutations_mySqlMigrations/Migrations/20220527_MySqlMigrations.cs b/samples/WebAPI_Dapper_Kafka/Salutations_mySqlMigrations/Migrations/20220527_MySqlMigrations.cs index d1f4e4aec2..6b8a2c9ecc 100644 --- a/samples/WebAPI_Dapper_Kafka/Salutations_mySqlMigrations/Migrations/20220527_MySqlMigrations.cs +++ b/samples/WebAPI_Dapper_Kafka/Salutations_mySqlMigrations/Migrations/20220527_MySqlMigrations.cs @@ -10,7 +10,7 @@ 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().WithDefault(SystemMethods.CurrentDateTime); } public override void Down() diff --git a/samples/WebAPI_Dapper_Kafka/tests.http b/samples/WebAPI_Dapper_Kafka/tests.http index 30db899ee4..56382375bb 100644 --- a/samples/WebAPI_Dapper_Kafka/tests.http +++ b/samples/WebAPI_Dapper_Kafka/tests.http @@ -8,7 +8,7 @@ POST http://localhost:5000/People/new HTTP/1.1 Content-Type: application/json { - "Name" : "" + "Name" : "Tyrion" } ### Now see that person diff --git a/src/Paramore.Brighter.Outbox.MySql/MySqlOutbox.cs b/src/Paramore.Brighter.Outbox.MySql/MySqlOutbox.cs index 6bb9724eaf..e398b17a86 100644 --- a/src/Paramore.Brighter.Outbox.MySql/MySqlOutbox.cs +++ b/src/Paramore.Brighter.Outbox.MySql/MySqlOutbox.cs @@ -27,6 +27,7 @@ THE SOFTWARE. */ 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; @@ -395,11 +396,22 @@ private Message MapAMessage(IDataReader dr) private byte[] GetBodyAsBytes(MySqlDataReader dr) { var i = dr.GetOrdinal("Body"); - var body = dr.GetStream(i); - long bodyLength = body.Length; - var buffer = new byte[bodyLength]; - body.Read(buffer, 0, (int)bodyLength); - return buffer; + 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 Dictionary GetContextBag(IDataReader dr) diff --git a/src/Paramore.Brighter.Outbox.MySql/MySqlQueries.cs b/src/Paramore.Brighter.Outbox.MySql/MySqlQueries.cs index 602819d6a9..bbf13d6134 100644 --- a/src/Paramore.Brighter.Outbox.MySql/MySqlQueries.cs +++ b/src/Paramore.Brighter.Outbox.MySql/MySqlQueries.cs @@ -2,14 +2,14 @@ { 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 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 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 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} )"; } From aa757cab3e4a516abca98741c8467abb632630ae Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Tue, 16 May 2023 08:46:04 +0100 Subject: [PATCH 45/89] use transactionconnectionprovider for lifetime --- .../GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs | 2 +- .../SalutationPorts/Handlers/GreetingMadeHandler.cs | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs index 389a0121d4..d4cf2d72b7 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs +++ b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs @@ -62,7 +62,7 @@ public override async Task HandleAsync(AddGreeting addGreeting, Can { _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 _transactionConnectionProvider.RollbackAsync(cancellationToken); return await base.HandleAsync(addGreeting, cancellationToken); } finally diff --git a/samples/WebAPI_Dapper_Kafka/SalutationPorts/Handlers/GreetingMadeHandler.cs b/samples/WebAPI_Dapper_Kafka/SalutationPorts/Handlers/GreetingMadeHandler.cs index 26d6fa2668..1da7a15221 100644 --- a/samples/WebAPI_Dapper_Kafka/SalutationPorts/Handlers/GreetingMadeHandler.cs +++ b/samples/WebAPI_Dapper_Kafka/SalutationPorts/Handlers/GreetingMadeHandler.cs @@ -3,6 +3,7 @@ using DapperExtensions; using Microsoft.Extensions.Logging; using Paramore.Brighter; +using Paramore.Brighter.Inbox.Attributes; using Paramore.Brighter.Logging.Attributes; using Paramore.Brighter.Policies.Attributes; using SalutationEntities; @@ -32,7 +33,7 @@ public GreetingMadeHandler(IAmATransactionConnectionProvider transactionConnecti _logger = logger; } - //[UseInboxAsync(step:0, contextKey: typeof(GreetingMadeHandlerAsync), onceOnly: true )] -- we are using a global inbox, so need to be explicit!! + [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) @@ -48,14 +49,14 @@ public override GreetingMade Handle(GreetingMade @event) posts.Add(_postBox.DepositPost(new SalutationReceived(DateTimeOffset.Now))); - tx.Commit(); + _transactionConnectionProvider.Commit(); } catch (Exception e) { _logger.LogError(e, "Could not save salutation"); //if it went wrong rollback entity write and Outbox write - tx.Rollback(); + _transactionConnectionProvider.Rollback(); return base.Handle(@event); } From 316cd1d17b50384b86269803ea352e73e254ccbf Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Tue, 16 May 2023 09:11:39 +0100 Subject: [PATCH 46/89] update non-binary Dapper sample --- .../.idea/httpRequests/http-requests-log.http | 108 +++++++++--------- .../Handlers/AddPersonHandlerAsync.cs | 14 ++- .../Handlers/DeletePersonHandlerAsync.cs | 24 ++-- .../FIndGreetingsForPersonHandlerAsync.cs | 35 +++--- .../Handlers/FindPersonByNameHandlerAsync.cs | 15 ++- .../GreetingsWeb/Database/OutboxExtensions.cs | 88 +++++++------- samples/WebAPI_Dapper/GreetingsWeb/Startup.cs | 6 +- .../Handlers/DeletePersonHandlerAsync.cs | 53 +++++---- .../FIndGreetingsForPersonHandlerAsync.cs | 2 +- .../GreetingsWeb/Startup.cs | 6 - 10 files changed, 186 insertions(+), 165 deletions(-) diff --git a/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http b/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http index 4034ac6fcf..abe5eae21d 100644 --- a/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http +++ b/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http @@ -9,6 +9,60 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } +<> 2023-05-16T084354.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-05-16T083851.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-05-16T081219.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-05-16T081215.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-05-15T110426.200.json ### @@ -568,57 +622,3 @@ Accept-Encoding: br,deflate,gzip,x-gzip ### -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-05-06T164825.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" -} - -<> 2023-05-06T164716.500.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-05-06T164705.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" -} - -<> 2023-05-06T164701.200.json - -### - diff --git a/samples/WebAPI_Dapper/GreetingsPorts/Handlers/AddPersonHandlerAsync.cs b/samples/WebAPI_Dapper/GreetingsPorts/Handlers/AddPersonHandlerAsync.cs index 27af764f2e..d3361e0ee3 100644 --- a/samples/WebAPI_Dapper/GreetingsPorts/Handlers/AddPersonHandlerAsync.cs +++ b/samples/WebAPI_Dapper/GreetingsPorts/Handlers/AddPersonHandlerAsync.cs @@ -11,20 +11,22 @@ namespace GreetingsPorts.Handlers { public class AddPersonHandlerAsync : RequestHandlerAsync { - private readonly IAmATransactionConnectionProvider _transactionConnectionProvider; + private readonly IAmARelationalDbConnectionProvider _relationalDbConnectionProvider; - public AddPersonHandlerAsync(IAmATransactionConnectionProvider transactionConnectionProvider) + public AddPersonHandlerAsync(IAmARelationalDbConnectionProvider transactionConnectionProvider) { - _transactionConnectionProvider = transactionConnectionProvider; + _relationalDbConnectionProvider = transactionConnectionProvider; } [RequestLoggingAsync(0, HandlerTiming.Before)] [UsePolicyAsync(step:1, policy: Policies.Retry.EXPONENTIAL_RETRYPOLICYASYNC)] public override async Task HandleAsync(AddPerson addPerson, CancellationToken cancellationToken = default) { - await _transactionConnectionProvider.GetConnection().InsertAsync(new Person(addPerson.Name)); - - return await base.HandleAsync(addPerson, cancellationToken); + using (var connection = await _relationalDbConnectionProvider.GetConnectionAsync(cancellationToken)) + { + await connection.InsertAsync(new Person(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 b7e9956210..5c484a6bce 100644 --- a/samples/WebAPI_Dapper/GreetingsPorts/Handlers/DeletePersonHandlerAsync.cs +++ b/samples/WebAPI_Dapper/GreetingsPorts/Handlers/DeletePersonHandlerAsync.cs @@ -14,28 +14,30 @@ namespace GreetingsPorts.Handlers { public class DeletePersonHandlerAsync : RequestHandlerAsync { - private readonly IAmATransactionConnectionProvider _transactionConnectionProvider; + private readonly IAmARelationalDbConnectionProvider _relationalDbConnectionProvider; - public DeletePersonHandlerAsync(IAmATransactionConnectionProvider transactionConnectionProvider) + public DeletePersonHandlerAsync(IAmATransactionConnectionProvider relationalDbConnectionProvider) { - _transactionConnectionProvider = transactionConnectionProvider; + _relationalDbConnectionProvider = relationalDbConnectionProvider; } [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 _transactionConnectionProvider.GetTransactionAsync(cancellationToken); + var connection = await _relationalDbConnectionProvider.GetConnectionAsync(cancellationToken); + var tx = await connection.BeginTransactionAsync(cancellationToken); try { var searchbyName = Predicates.Field(p => p.Name, Operator.Eq, deletePerson.Name); - var people = await _transactionConnectionProvider.GetConnection().GetListAsync(searchbyName, transaction: tx); + var people = await connection + .GetListAsync(searchbyName, transaction: tx); var person = people.Single(); var deleteById = Predicates.Field(g => g.RecipientId, Operator.Eq, person.Id); - await _transactionConnectionProvider.GetConnection().DeleteAsync(deleteById, tx); - + await connection.DeleteAsync(deleteById, tx); + await tx.CommitAsync(cancellationToken); } catch (Exception) @@ -44,6 +46,12 @@ public async override Task HandleAsync(DeletePerson deletePerson, 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 355ecd5a5e..84a63f883c 100644 --- a/samples/WebAPI_Dapper/GreetingsPorts/Handlers/FIndGreetingsForPersonHandlerAsync.cs +++ b/samples/WebAPI_Dapper/GreetingsPorts/Handlers/FIndGreetingsForPersonHandlerAsync.cs @@ -33,27 +33,28 @@ public FIndGreetingsForPersonHandlerAsync(IAmATransactionConnectionProvider tran 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 _transactionConnectionProvider.GetConnection().QueryAsync(sql, (person, greeting) => + using (var connection = await _transactionConnectionProvider.GetConnectionAsync(cancellationToken)) { - person.Greetings.Add(greeting); - return person; - }, splitOn: "Id"); + var people = await connection.QueryAsync(sql, (person, greeting) => + { + person.Greetings.Add(greeting); + return person; + }, splitOn: "Id"); - var peopleGreetings = people.GroupBy(p => p.Id).Select(grp => - { - var groupedPerson = grp.First(); - groupedPerson.Greetings = grp.Select(p => p.Greetings.Single()).ToList(); - return groupedPerson; - }); - - var person = peopleGreetings.Single(); + var peopleGreetings = people.GroupBy(p => p.Id).Select(grp => + { + var groupedPerson = grp.First(); + groupedPerson.Greetings = grp.Select(p => p.Greetings.Single()).ToList(); + return groupedPerson; + }); - return new FindPersonsGreetings - { - Name = person.Name, - Greetings = person.Greetings.Select(g => new Salutation(g.Greet())) - }; + var person = peopleGreetings.Single(); + return new FindPersonsGreetings + { + 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 9682055c2d..91fde6fae0 100644 --- a/samples/WebAPI_Dapper/GreetingsPorts/Handlers/FindPersonByNameHandlerAsync.cs +++ b/samples/WebAPI_Dapper/GreetingsPorts/Handlers/FindPersonByNameHandlerAsync.cs @@ -16,11 +16,11 @@ namespace GreetingsPorts.Handlers { public class FindPersonByNameHandlerAsync : QueryHandlerAsync { - private readonly IAmATransactionConnectionProvider _transactionConnectionProvider; + private readonly IAmARelationalDbConnectionProvider _relationalDbConnectionProvider; - public FindPersonByNameHandlerAsync(IAmATransactionConnectionProvider transactionConnectionProvider) + public FindPersonByNameHandlerAsync(IAmARelationalDbConnectionProvider relationalDbConnectionProvider) { - _transactionConnectionProvider = transactionConnectionProvider; + _relationalDbConnectionProvider = relationalDbConnectionProvider; } [QueryLogging(0)] @@ -28,10 +28,13 @@ public FindPersonByNameHandlerAsync(IAmATransactionConnectionProvider transactio 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 _transactionConnectionProvider.GetConnection().GetListAsync(searchbyName); - var person = people.Single(); + using (var connection = await _relationalDbConnectionProvider .GetConnectionAsync(cancellationToken)) + { + var people = await connection.GetListAsync(searchbyName); + var person = people.Single(); - return new FindPersonResult(person); + return new FindPersonResult(person); + } } } } diff --git a/samples/WebAPI_Dapper/GreetingsWeb/Database/OutboxExtensions.cs b/samples/WebAPI_Dapper/GreetingsWeb/Database/OutboxExtensions.cs index f5203c2e5a..7793a3e271 100644 --- a/samples/WebAPI_Dapper/GreetingsWeb/Database/OutboxExtensions.cs +++ b/samples/WebAPI_Dapper/GreetingsWeb/Database/OutboxExtensions.cs @@ -10,52 +10,62 @@ using Paramore.Brighter.Outbox.Sqlite; using Paramore.Brighter.Sqlite; -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 static class OutboxExtensions { - if (env.IsDevelopment()) - { - AddSqliteOutBox(brighterBuilder, dbConnectionString, outBoxTableName); - } - else + public static IBrighterBuilder AddOutbox( + this IBrighterBuilder brighterBuilder, + IWebHostEnvironment env, + DatabaseType databaseType, + RelationalDatabaseConfiguration configuration) { - switch (databaseType) + if (env.IsDevelopment()) { - case DatabaseType.MySql: - AddMySqlOutbox(brighterBuilder, dbConnectionString, outBoxTableName); - break; - default: - throw new InvalidOperationException("Unknown Db type for Outbox configuration"); + AddSqliteOutBox(brighterBuilder, configuration); } + else + { + switch (databaseType) + { + case DatabaseType.MySql: + AddMySqlOutbox(brighterBuilder, configuration); + break; + default: + throw new InvalidOperationException("Unknown Db type for Outbox configuration"); + } + } + + return brighterBuilder; } - return brighterBuilder; - } - private static void AddMySqlOutbox(IBrighterBuilder brighterBuilder, string dbConnectionString, string outBoxTableName) - { - brighterBuilder.UseMySqlOutbox( - new RelationalDatabaseConfiguration(dbConnectionString, outBoxTableName), - typeof(MySqlConnectionProvider), - ServiceLifetime.Singleton) - .UseMySqTransactionConnectionProvider(typeof(MySqlUnitOfWork), ServiceLifetime.Scoped) - .UseOutboxSweeper(); - } + private static void AddMySqlOutbox(IBrighterBuilder brighterBuilder, + RelationalDatabaseConfiguration configuration) + { + brighterBuilder.UseMySqlOutbox( + configuration, + typeof(MySqlConnectionProvider), + ServiceLifetime.Singleton) + .UseMySqTransactionConnectionProvider( + typeof(MySqlUnitOfWork), ServiceLifetime.Scoped) + .UseOutboxSweeper(); + } - private static void AddSqliteOutBox(IBrighterBuilder brighterBuilder, string dbConnectionString, string outBoxTableName) - { - brighterBuilder.UseSqliteOutbox( - new RelationalDatabaseConfiguration(dbConnectionString, outBoxTableName), - typeof(SqliteConnectionProvider), - ServiceLifetime.Singleton) - .UseSqliteTransactionConnectionProvider(typeof(SqliteUnitOfWork), ServiceLifetime.Scoped) - .UseOutboxSweeper(options => - { - options.TimerInterval = 5; - options.MinimumMessageAge = 5000; - }); + private static void AddSqliteOutBox(IBrighterBuilder brighterBuilder, + RelationalDatabaseConfiguration configuration) + { + brighterBuilder.UseSqliteOutbox( + configuration, + typeof(SqliteConnectionProvider), + ServiceLifetime.Singleton) + .UseSqliteTransactionConnectionProvider( + typeof(SqliteUnitOfWork), ServiceLifetime.Scoped) + .UseOutboxSweeper(options => + { + options.TimerInterval = 5; + options.MinimumMessageAge = 5000; + }); + } } } diff --git a/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs b/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs index 3a629f50be..c40db03711 100644 --- a/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs +++ b/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs @@ -154,11 +154,11 @@ private static void ConfigureDapperMySql(IServiceCollection services) private void ConfigureBrighter(IServiceCollection services) { - var configuration = new RelationalDatabaseConfiguration( + var outboxConfiguration = new RelationalDatabaseConfiguration( DbConnectionString(), outBoxTableName:_outBoxTableName ); - services.AddSingleton(configuration); + services.AddSingleton(outboxConfiguration); services.AddBrighter(options => { @@ -191,7 +191,7 @@ private void ConfigureBrighter(IServiceCollection services) //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) + .AddOutbox(_env, GetDatabaseType(), outboxConfiguration) .AutoFromAssemblies(typeof(AddPersonHandlerAsync).Assembly); } diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/DeletePersonHandlerAsync.cs b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/DeletePersonHandlerAsync.cs index 45928d42f5..24f7b3055c 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/DeletePersonHandlerAsync.cs +++ b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/DeletePersonHandlerAsync.cs @@ -14,42 +14,45 @@ namespace GreetingsPorts.Handlers { public class DeletePersonHandlerAsync : RequestHandlerAsync { - private readonly IAmARelationalDbConnectionProvider _connectionProvider; + private readonly IAmARelationalDbConnectionProvider _relationalDbConnectionProvider; - public DeletePersonHandlerAsync(IAmARelationalDbConnectionProvider connectionProvider) + public DeletePersonHandlerAsync(IAmARelationalDbConnectionProvider relationalDbConnectionProvider) { - _connectionProvider = connectionProvider; + _relationalDbConnectionProvider = relationalDbConnectionProvider; } [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) { - using (var connection = await _connectionProvider.GetConnectionAsync(cancellationToken)) + var connection = await _relationalDbConnectionProvider.GetConnectionAsync(cancellationToken); + await connection.OpenAsync(cancellationToken); + + //NOTE: we are using a transaction, but a connection provider will not manage one for us, so we need to do it ourselves + var tx = await connection.BeginTransactionAsync(cancellationToken); + try { - await connection.OpenAsync(cancellationToken); - - //NOTE: we are using a transaction, but a connection provider will not manage one for us, so we need to do it ourselves - var tx = await connection.BeginTransactionAsync(cancellationToken); - try - { - var searchbyName = Predicates.Field(p => p.Name, Operator.Eq, deletePerson.Name); - var people = await _connectionProvider.GetConnection() - .GetListAsync(searchbyName, transaction: tx); - var person = people.Single(); + var searchbyName = Predicates.Field(p => p.Name, Operator.Eq, deletePerson.Name); + var people = await _relationalDbConnectionProvider.GetConnection() + .GetListAsync(searchbyName, transaction: tx); + var person = people.Single(); - var deleteById = Predicates.Field(g => g.RecipientId, Operator.Eq, person.Id); - await _connectionProvider.GetConnection().DeleteAsync(deleteById, tx); + var deleteById = Predicates.Field(g => g.RecipientId, Operator.Eq, person.Id); + await _relationalDbConnectionProvider.GetConnection().DeleteAsync(deleteById, tx); - await tx.CommitAsync(cancellationToken); - } - catch (Exception) - { - //it went wrong, rollback the entity change and the downstream message - await tx.RollbackAsync(cancellationToken); - return await base.HandleAsync(deletePerson, cancellationToken); - } + await tx.CommitAsync(cancellationToken); + } + catch (Exception) + { + //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_Kafka/GreetingsPorts/Handlers/FIndGreetingsForPersonHandlerAsync.cs b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/FIndGreetingsForPersonHandlerAsync.cs index e51de8c714..79f165d20a 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/FIndGreetingsForPersonHandlerAsync.cs +++ b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/FIndGreetingsForPersonHandlerAsync.cs @@ -35,7 +35,7 @@ public FIndGreetingsForPersonHandlerAsync(IAmARelationalDbConnectionProvider re from Person p inner join Greeting g on g.Recipient_Id = p.Id"; - using (var connection = _relationalDbConnectionProvider.GetConnection()) + using (var connection = await _relationalDbConnectionProvider.GetConnectionAsync(cancellationToken)) { var people = await connection.QueryAsync(sql, (person, greeting) => { diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs index d580dad5e6..ff4ea79621 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs +++ b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs @@ -122,12 +122,6 @@ private void ConfigureMySql(IServiceCollection services) private void ConfigureDapper(IServiceCollection services) { - var configuration = new RelationalDatabaseConfiguration( - DbConnectionString(), - outBoxTableName:_outBoxTableName - ); - services.AddSingleton(configuration); - ConfigureDapperByHost(GetDatabaseType(), services); DapperExtensions.DapperExtensions.SetMappingAssemblies(new[] { typeof(PersonMapper).Assembly }); From b1f98330aa43b150b421030f847f8ee588567096 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Wed, 17 May 2023 17:07:01 +0100 Subject: [PATCH 47/89] Flow move back to box transaction provider out --- .../ServiceCollectionExtensions.cs | 4 +- .../DynamoDbOutbox.cs | 14 +++---- .../EventStoreOutboxSync.cs | 11 +++-- .../MsSqlOutbox.cs | 12 +++--- .../MySqlOutbox.cs | 12 +++--- .../ServiceCollectionExtensions.cs | 16 ++++++++ .../PostgreSqlOutbox.cs | 16 ++++---- .../ServiceCollectionExtensions.cs | 29 +++++++++----- .../ServiceCollectionExtensions.cs | 23 +++++++++-- .../SqliteOutbox.cs | 14 +++---- .../ControlBusReceiverBuilder.cs | 2 +- src/Paramore.Brighter/CommandProcessor.cs | 40 +++++++++---------- .../CommandProcessorBuilder.cs | 16 ++++---- src/Paramore.Brighter/ExternalBusServices.cs | 24 ++++++++--- src/Paramore.Brighter/IAmABulkOutboxAsync.cs | 8 +++- src/Paramore.Brighter/IAmABulkOutboxSync.cs | 4 +- src/Paramore.Brighter/IAmAnOutboxAsync.cs | 5 ++- src/Paramore.Brighter/IAmAnOutboxSync.cs | 4 +- src/Paramore.Brighter/InMemoryOutbox.cs | 26 ++++++------ .../RelationDatabaseOutbox.cs | 32 +++++++-------- .../TestDoubles/FakeOutboxSync.cs | 13 ++++-- 21 files changed, 192 insertions(+), 133 deletions(-) diff --git a/src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionExtensions.cs b/src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionExtensions.cs index 1e01519b96..4b34a42da1 100644 --- a/src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionExtensions.cs @@ -293,7 +293,7 @@ private static CommandProcessor BuildCommandProcessor(IServiceProvider provider) var outbox = provider.GetService>(); var asyncOutbox = provider.GetService>(); - var overridingConnectionProvider = provider.GetService(); + var overridingConnectionProvider = provider.GetService(); if (outbox == null) outbox = new InMemoryOutbox(); if (asyncOutbox == null) asyncOutbox = new InMemoryOutbox(); @@ -347,7 +347,7 @@ private static INeedARequestContext AddExternalBusMaybe( MessageMapperRegistry messageMapperRegistry, InboxConfiguration inboxConfiguration, IAmAnOutboxSync outbox, - IAmATransactionConnectionProvider overridingProvider, + IAmABoxTransactionProvider overridingProvider, IUseRpc useRequestResponse, int outboxBulkChunkSize, IAmAMessageTransformerFactory transformerFactory) diff --git a/src/Paramore.Brighter.Outbox.DynamoDB/DynamoDbOutbox.cs b/src/Paramore.Brighter.Outbox.DynamoDB/DynamoDbOutbox.cs index 518642d87c..8d8b4b6165 100644 --- a/src/Paramore.Brighter.Outbox.DynamoDB/DynamoDbOutbox.cs +++ b/src/Paramore.Brighter.Outbox.DynamoDB/DynamoDbOutbox.cs @@ -76,7 +76,7 @@ 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, IAmATransactionConnectionProvider transactionProvider = null) + public void Add(Message message, int outBoxTimeout = -1, IAmABoxTransactionProvider transactionProvider = null) { AddAsync(message, outBoxTimeout).ConfigureAwait(ContinueOnCapturedContext).GetAwaiter().GetResult(); } @@ -87,12 +87,12 @@ public void Add(Message message, int outBoxTimeout = -1, IAmATransactionConnecti /// /// 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, - IAmATransactionConnectionProvider transactionProvider = 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 messageToStore = new MessageItem(message); diff --git a/src/Paramore.Brighter.Outbox.EventStore/EventStoreOutboxSync.cs b/src/Paramore.Brighter.Outbox.EventStore/EventStoreOutboxSync.cs index f7b14167cd..91d69df89d 100644 --- a/src/Paramore.Brighter.Outbox.EventStore/EventStoreOutboxSync.cs +++ b/src/Paramore.Brighter.Outbox.EventStore/EventStoreOutboxSync.cs @@ -73,7 +73,7 @@ public EventStoreOutboxSync(IEventStoreConnection eventStore) /// The message. /// The outBoxTimeout. /// Task. - public void Add(Message message, int outBoxTimeout = -1, IAmATransactionConnectionProvider transactionProvider = 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)); @@ -93,13 +93,12 @@ public void Add(Message message, int outBoxTimeout = -1, IAmATransactionConnecti /// The message. /// The time allowed for the write in milliseconds; on a -1 default /// Allows the sender to cancel the request pipeline. Optional + /// /// . - public async Task AddAsync( - Message message, + public async Task AddAsync(Message message, int outBoxTimeout = -1, - CancellationToken cancellationToken = default, - IAmATransactionConnectionProvider transactionProvider = null - ) + CancellationToken cancellationToken = default, + IAmABoxTransactionProvider transactionProvider = null) { s_logger.LogDebug("Adding message to Event Store Outbox: {Request}", JsonSerializer.Serialize(message, JsonSerialisationOptions.Options)); diff --git a/src/Paramore.Brighter.Outbox.MsSql/MsSqlOutbox.cs b/src/Paramore.Brighter.Outbox.MsSql/MsSqlOutbox.cs index 39a4ce6202..12d9617004 100644 --- a/src/Paramore.Brighter.Outbox.MsSql/MsSqlOutbox.cs +++ b/src/Paramore.Brighter.Outbox.MsSql/MsSqlOutbox.cs @@ -70,13 +70,13 @@ public MsSqlOutbox(IAmARelationalDatabaseConfiguration configuration) : this(con } protected override void WriteToStore( - IAmATransactionConnectionProvider transactionProvider, + IAmABoxTransactionProvider transactionProvider, Func commandFunc, Action loggingAction) { var connectionProvider = _connectionProvider; - if (transactionProvider != null) - connectionProvider = transactionProvider; + if (transactionProvider is IAmARelationalDbConnectionProvider transConnectionProvider) + connectionProvider = transConnectionProvider; var connection = connectionProvider.GetConnection(); @@ -112,14 +112,14 @@ protected override void WriteToStore( } protected override async Task WriteToStoreAsync( - IAmATransactionConnectionProvider transactionProvider, + IAmABoxTransactionProvider transactionProvider, Func commandFunc, Action loggingAction, CancellationToken cancellationToken) { var connectionProvider = _connectionProvider; - if (transactionProvider != null) - connectionProvider = transactionProvider; + if (transactionProvider is IAmARelationalDbConnectionProvider transConnectionProvider) + connectionProvider = transConnectionProvider; var connection = await connectionProvider.GetConnectionAsync(cancellationToken) .ConfigureAwait(ContinueOnCapturedContext); diff --git a/src/Paramore.Brighter.Outbox.MySql/MySqlOutbox.cs b/src/Paramore.Brighter.Outbox.MySql/MySqlOutbox.cs index e398b17a86..bd62c58ca5 100644 --- a/src/Paramore.Brighter.Outbox.MySql/MySqlOutbox.cs +++ b/src/Paramore.Brighter.Outbox.MySql/MySqlOutbox.cs @@ -63,14 +63,14 @@ public MySqlOutbox(IAmARelationalDatabaseConfiguration configuration) } protected override void WriteToStore( - IAmATransactionConnectionProvider transactionProvider, + IAmABoxTransactionProvider transactionProvider, Func commandFunc, Action loggingAction ) { var connectionProvider = _connectionProvider; - if (transactionProvider != null) - connectionProvider = transactionProvider; + if (transactionProvider is IAmARelationalDbConnectionProvider transConnectionProvider) + connectionProvider = transConnectionProvider; var connection = connectionProvider.GetConnection(); @@ -106,15 +106,15 @@ Action loggingAction } protected override async Task WriteToStoreAsync( - IAmATransactionConnectionProvider transactionProvider, + IAmABoxTransactionProvider transactionProvider, Func commandFunc, Action loggingAction, CancellationToken cancellationToken ) { var connectionProvider = _connectionProvider; - if (transactionProvider != null) - connectionProvider = transactionProvider; + if (transactionProvider is IAmARelationalDbConnectionProvider transConnectionProvider) + connectionProvider = transConnectionProvider; var connection = await connectionProvider.GetConnectionAsync(cancellationToken) .ConfigureAwait(ContinueOnCapturedContext); diff --git a/src/Paramore.Brighter.Outbox.MySql/ServiceCollectionExtensions.cs b/src/Paramore.Brighter.Outbox.MySql/ServiceCollectionExtensions.cs index f87f5fcdea..50b106fe54 100644 --- a/src/Paramore.Brighter.Outbox.MySql/ServiceCollectionExtensions.cs +++ b/src/Paramore.Brighter.Outbox.MySql/ServiceCollectionExtensions.cs @@ -50,6 +50,22 @@ public static IBrighterBuilder UseMySqTransactionConnectionProvider( ServiceLifetime serviceLifetime = ServiceLifetime.Scoped ) { + if (brighterBuilder is null) + throw new ArgumentNullException($"{nameof(brighterBuilder)} cannot be null.", nameof(brighterBuilder)); + + if (transactionProvider is null) + throw new ArgumentNullException($"{nameof(transactionProvider)} cannot be null.", nameof(transactionProvider)); + + if (!typeof(IAmABoxTransactionProvider).IsAssignableFrom(transactionProvider)) + throw new Exception($"Unable to register provider of type {transactionProvider.GetType().Name}. Class does not implement interface {nameof(IAmABoxTransactionProvider)}."); + + if (!typeof(IAmATransactionConnectionProvider).IsAssignableFrom(transactionProvider)) + throw new Exception($"Unable to register provider of type {transactionProvider.GetType().Name}. Class does not implement interface {nameof(IAmATransactionConnectionProvider)}."); + + //register the specific interface + brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmABoxTransactionProvider), transactionProvider, serviceLifetime)); + + //register the combined interface just in case brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmATransactionConnectionProvider), transactionProvider, serviceLifetime)); return brighterBuilder; diff --git a/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutbox.cs b/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutbox.cs index 9997851709..208444318c 100644 --- a/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutbox.cs +++ b/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutbox.cs @@ -60,13 +60,13 @@ public PostgreSqlOutbox(IAmARelationalDatabaseConfiguration configuration) { } protected override void WriteToStore( - IAmATransactionConnectionProvider transactionConnectionProvider, + IAmABoxTransactionProvider transactionProvider, Func commandFunc, Action loggingAction) { var connectionProvider = _connectionProvider; - if (transactionConnectionProvider != null) - connectionProvider = transactionConnectionProvider; + if (transactionProvider is IAmARelationalDbConnectionProvider transConnectionProvider) + connectionProvider = transConnectionProvider; var connection = connectionProvider.GetConnection(); @@ -76,7 +76,7 @@ protected override void WriteToStore( { try { - if (transactionConnectionProvider != null && connectionProvider.HasOpenTransaction) + if (transactionProvider != null && connectionProvider.HasOpenTransaction) command.Transaction = connectionProvider.GetTransaction(); command.ExecuteNonQuery(); } @@ -101,14 +101,14 @@ protected override void WriteToStore( } protected override async Task WriteToStoreAsync( - IAmATransactionConnectionProvider transactionConnectionProvider, + IAmABoxTransactionProvider transactionProvider, Func commandFunc, Action loggingAction, CancellationToken cancellationToken) { var connectionProvider = _connectionProvider; - if (transactionConnectionProvider != null) - connectionProvider = transactionConnectionProvider; + if (transactionProvider is IAmARelationalDbConnectionProvider transConnectionProvider) + connectionProvider = transConnectionProvider; var connection = await connectionProvider.GetConnectionAsync(cancellationToken) .ConfigureAwait(ContinueOnCapturedContext); @@ -119,7 +119,7 @@ protected override async Task WriteToStoreAsync( { try { - if (transactionConnectionProvider != null && connectionProvider.HasOpenTransaction) + if (transactionProvider != null && connectionProvider.HasOpenTransaction) command.Transaction = connectionProvider.GetTransaction(); await command.ExecuteNonQueryAsync(cancellationToken); } diff --git a/src/Paramore.Brighter.Outbox.PostgreSql/ServiceCollectionExtensions.cs b/src/Paramore.Brighter.Outbox.PostgreSql/ServiceCollectionExtensions.cs index 4793240dd0..c6c57a6af4 100644 --- a/src/Paramore.Brighter.Outbox.PostgreSql/ServiceCollectionExtensions.cs +++ b/src/Paramore.Brighter.Outbox.PostgreSql/ServiceCollectionExtensions.cs @@ -35,28 +35,37 @@ public static IBrighterBuilder UsePostgreSqlOutbox( /// 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 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, + this IBrighterBuilder brighterBuilder, + Type transactionProvider, 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 (transactionProvider is null) + throw new ArgumentNullException($"{nameof(transactionProvider)} cannot be null.", nameof(transactionProvider)); - if (!typeof(IAmATransactionConnectionProvider).IsAssignableFrom(connectionProvider)) - throw new Exception($"Unable to register provider of type {connectionProvider.GetType().Name}. Class does not implement interface {nameof(IAmATransactionConnectionProvider)}."); - - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmABoxTransactionProvider), connectionProvider, serviceLifetime)); - - return brighterBuilder; + if (!typeof(IAmABoxTransactionProvider).IsAssignableFrom(transactionProvider)) + throw new Exception($"Unable to register provider of type {transactionProvider.GetType().Name}. Class does not implement interface {nameof(IAmABoxTransactionProvider)}."); + + if (!typeof(IAmATransactionConnectionProvider).IsAssignableFrom(transactionProvider)) + throw new Exception($"Unable to register provider of type {transactionProvider.GetType().Name}. Class does not implement interface {nameof(IAmATransactionConnectionProvider)}."); + + //register the specific interface + brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmABoxTransactionProvider), transactionProvider, serviceLifetime)); + + //register the combined interface just in case + brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmATransactionConnectionProvider), transactionProvider, serviceLifetime)); + + return brighterBuilder; + } private static PostgreSqlOutbox BuildPostgreSqlOutboxSync(IServiceProvider provider) diff --git a/src/Paramore.Brighter.Outbox.Sqlite/ServiceCollectionExtensions.cs b/src/Paramore.Brighter.Outbox.Sqlite/ServiceCollectionExtensions.cs index 7e30881a70..d77921bfb2 100644 --- a/src/Paramore.Brighter.Outbox.Sqlite/ServiceCollectionExtensions.cs +++ b/src/Paramore.Brighter.Outbox.Sqlite/ServiceCollectionExtensions.cs @@ -38,18 +38,35 @@ public static IBrighterBuilder UseSqliteOutbox( /// 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 type of the transaction 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, + this IBrighterBuilder brighterBuilder, + Type transactionProvider, ServiceLifetime serviceLifetime = ServiceLifetime.Scoped) { - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmATransactionConnectionProvider), connectionProvider, serviceLifetime)); + if (brighterBuilder is null) + throw new ArgumentNullException($"{nameof(brighterBuilder)} cannot be null.", nameof(brighterBuilder)); + if (transactionProvider is null) + throw new ArgumentNullException($"{nameof(transactionProvider)} cannot be null.", nameof(transactionProvider)); + + if (!typeof(IAmABoxTransactionProvider).IsAssignableFrom(transactionProvider)) + throw new Exception($"Unable to register provider of type {transactionProvider.GetType().Name}. Class does not implement interface {nameof(IAmABoxTransactionProvider)}."); + + if (!typeof(IAmATransactionConnectionProvider).IsAssignableFrom(transactionProvider)) + throw new Exception($"Unable to register provider of type {transactionProvider.GetType().Name}. Class does not implement interface {nameof(IAmATransactionConnectionProvider)}."); + + //register the specific interface + brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmABoxTransactionProvider), transactionProvider, serviceLifetime)); + + //register the combined interface just in case + brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmATransactionConnectionProvider), transactionProvider, serviceLifetime)); + return brighterBuilder; } diff --git a/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutbox.cs b/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutbox.cs index 98b43e1924..5f9ef57fb0 100644 --- a/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutbox.cs +++ b/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutbox.cs @@ -72,14 +72,14 @@ public SqliteOutbox(IAmARelationalDatabaseConfiguration configuration) } protected override void WriteToStore( - IAmATransactionConnectionProvider transactionProvider, + IAmABoxTransactionProvider transactionProvider, Func commandFunc, Action loggingAction ) { var connectionProvider = _connectionProvider; - if (transactionProvider != null) - connectionProvider = transactionProvider; + if (transactionProvider is IAmARelationalDbConnectionProvider transConnectionProvider) + connectionProvider = transConnectionProvider; var connection = connectionProvider.GetConnection(); @@ -111,14 +111,14 @@ Action loggingAction } protected override async Task WriteToStoreAsync( - IAmATransactionConnectionProvider transactionConnectionProvider, + IAmABoxTransactionProvider transactionProvider, Func commandFunc, Action loggingAction, CancellationToken cancellationToken) { var connectionProvider = _connectionProvider; - if (transactionConnectionProvider != null) - connectionProvider = transactionConnectionProvider; + if (transactionProvider is IAmARelationalDbConnectionProvider transConnectionProvider) + connectionProvider = transConnectionProvider; var connection = await connectionProvider.GetConnectionAsync(cancellationToken); @@ -128,7 +128,7 @@ protected override async Task WriteToStoreAsync( { try { - if (transactionConnectionProvider != null && connectionProvider.HasOpenTransaction) + if (transactionProvider != null && connectionProvider.HasOpenTransaction) command.Transaction = connectionProvider.GetTransaction(); await command.ExecuteNonQueryAsync(cancellationToken); } diff --git a/src/Paramore.Brighter.ServiceActivator/ControlBusReceiverBuilder.cs b/src/Paramore.Brighter.ServiceActivator/ControlBusReceiverBuilder.cs index 6f801273e3..2c3b0d0b66 100644 --- a/src/Paramore.Brighter.ServiceActivator/ControlBusReceiverBuilder.cs +++ b/src/Paramore.Brighter.ServiceActivator/ControlBusReceiverBuilder.cs @@ -185,7 +185,7 @@ public Dispatcher Build(string hostName) /// private class SinkOutboxSync : IAmAnOutboxSync { - public void Add(Message message, int outBoxTimeout = -1, IAmATransactionConnectionProvider transactionProvider = null) + public void Add(Message message, int outBoxTimeout = -1, IAmABoxTransactionProvider transactionProvider = null) { //discard message } diff --git a/src/Paramore.Brighter/CommandProcessor.cs b/src/Paramore.Brighter/CommandProcessor.cs index e43ee1baf3..64b3fcc40e 100644 --- a/src/Paramore.Brighter/CommandProcessor.cs +++ b/src/Paramore.Brighter/CommandProcessor.cs @@ -55,7 +55,7 @@ public class CommandProcessor : IAmACommandProcessor private readonly IAmARequestContextFactory _requestContextFactory; private readonly IPolicyRegistry _policyRegistry; private readonly InboxConfiguration _inboxConfiguration; - private readonly IAmATransactionConnectionProvider _transactionConnectionProvider; + private readonly IAmABoxTransactionProvider _transactionProvider; private readonly IAmAFeatureSwitchRegistry _featureSwitchRegistry; private readonly IEnumerable _replySubscriptions; private readonly TransformPipelineBuilder _transformPipelineBuilder; @@ -149,7 +149,7 @@ public CommandProcessor( /// 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 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, @@ -160,7 +160,7 @@ public CommandProcessor(IAmARequestContextFactory requestContextFactory, int outboxTimeout = 300, IAmAFeatureSwitchRegistry featureSwitchRegistry = null, InboxConfiguration inboxConfiguration = null, - IAmATransactionConnectionProvider transactionConnectionProvider = null, + IAmABoxTransactionProvider transactionProvider = null, int outboxBulkChunkSize = 100, IAmAMessageTransformerFactory messageTransformerFactory = null) { @@ -168,7 +168,7 @@ public CommandProcessor(IAmARequestContextFactory requestContextFactory, _policyRegistry = policyRegistry; _featureSwitchRegistry = featureSwitchRegistry; _inboxConfiguration = inboxConfiguration; - _transactionConnectionProvider = transactionConnectionProvider; + _transactionProvider = transactionProvider; _transformPipelineBuilder = new TransformPipelineBuilder(mapperRegistry, messageTransformerFactory); InitExtServiceBus(policyRegistry, outBox, outboxTimeout, producerRegistry, outboxBulkChunkSize); @@ -192,7 +192,7 @@ public CommandProcessor(IAmARequestContextFactory requestContextFactory, /// 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 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, @@ -207,7 +207,7 @@ public CommandProcessor(IAmASubscriberRegistry subscriberRegistry, IAmAFeatureSwitchRegistry featureSwitchRegistry = null, IAmAChannelFactory responseChannelFactory = null, InboxConfiguration inboxConfiguration = null, - IAmATransactionConnectionProvider transactionConnectionProvider = null, + IAmABoxTransactionProvider transactionProvider = null, int outboxBulkChunkSize = 100, IAmAMessageTransformerFactory messageTransformerFactory = null) : this(subscriberRegistry, handlerFactory, requestContextFactory, policyRegistry) @@ -215,7 +215,7 @@ public CommandProcessor(IAmASubscriberRegistry subscriberRegistry, _featureSwitchRegistry = featureSwitchRegistry; _responseChannelFactory = responseChannelFactory; _inboxConfiguration = inboxConfiguration; - _transactionConnectionProvider = transactionConnectionProvider; + _transactionProvider = transactionProvider; _replySubscriptions = replySubscriptions; _transformPipelineBuilder = new TransformPipelineBuilder(mapperRegistry, messageTransformerFactory); @@ -238,7 +238,7 @@ public CommandProcessor(IAmASubscriberRegistry subscriberRegistry, /// 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 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, @@ -251,13 +251,13 @@ public CommandProcessor(IAmASubscriberRegistry subscriberRegistry, int outboxTimeout = 300, IAmAFeatureSwitchRegistry featureSwitchRegistry = null, InboxConfiguration inboxConfiguration = null, - IAmATransactionConnectionProvider transactionConnectionProvider = null, + IAmABoxTransactionProvider transactionProvider = null, int outboxBulkChunkSize = 100, IAmAMessageTransformerFactory messageTransformerFactory = null) : this(subscriberRegistry, handlerFactory, requestContextFactory, policyRegistry, featureSwitchRegistry) { _inboxConfiguration = inboxConfiguration; - _transactionConnectionProvider = transactionConnectionProvider; + _transactionProvider = transactionProvider; _transformPipelineBuilder = new TransformPipelineBuilder(mapperRegistry, messageTransformerFactory); InitExtServiceBus(policyRegistry, outBox, outboxTimeout, producerRegistry, outboxBulkChunkSize); @@ -537,7 +537,7 @@ public async Task PostAsync(T request, bool continueOnCapturedContext = false /// The Id of the Message that has been deposited. public Guid DepositPost(T request) where T : class, IRequest { - return DepositPost(request, _transactionConnectionProvider); + return DepositPost(request, _transactionProvider); } /// @@ -552,11 +552,10 @@ public Guid DepositPost(T request) where T : class, IRequest /// The Id of the Message that has been deposited. public Guid[] DepositPost(IEnumerable requests) where T : class, IRequest { - return DepositPost(requests, _transactionConnectionProvider); + return DepositPost(requests, _transactionProvider); } - private Guid DepositPost(T request, IAmATransactionConnectionProvider provider) - where T : class, IRequest + private Guid DepositPost(T request, IAmABoxTransactionProvider provider) where T : class, IRequest { s_logger.LogInformation("Save request: {RequestType} {Id}", request.GetType(), request.Id); @@ -572,7 +571,7 @@ private Guid DepositPost(T request, IAmATransactionConnectionProvider provide return message.Id; } - private Guid[] DepositPost(IEnumerable requests, IAmATransactionConnectionProvider provider) + private Guid[] DepositPost(IEnumerable requests, IAmABoxTransactionProvider provider) where T : class, IRequest { if (!_bus.HasBulkOutbox()) @@ -609,7 +608,7 @@ private Guid[] DepositPost(IEnumerable requests, IAmATransactionConnection public async Task DepositPostAsync(T request, bool continueOnCapturedContext = false, CancellationToken cancellationToken = default) where T : class, IRequest { - return await DepositPostAsync(request, _transactionConnectionProvider, continueOnCapturedContext, + return await DepositPostAsync(request, _transactionProvider, continueOnCapturedContext, cancellationToken); } @@ -631,14 +630,15 @@ public Task DepositPostAsync( CancellationToken cancellationToken = default ) where T : class, IRequest { - return DepositPostAsync(requests, _transactionConnectionProvider, continueOnCapturedContext, cancellationToken); + return DepositPostAsync(requests, _transactionProvider, continueOnCapturedContext, cancellationToken); } private async Task DepositPostAsync( T request, - IAmATransactionConnectionProvider provider, + IAmABoxTransactionProvider provider, bool continueOnCapturedContext = false, - CancellationToken cancellationToken = default) where T : class, IRequest + CancellationToken cancellationToken = default + ) where T : class, IRequest { s_logger.LogInformation("Save request: {RequestType} {Id}", request.GetType(), request.Id); @@ -879,7 +879,7 @@ private static void InitExtServiceBus( } private async Task DepositPostAsync(IEnumerable requests, - IAmATransactionConnectionProvider provider, + IAmABoxTransactionProvider provider, bool continueOnCapturedContext = false, CancellationToken cancellationToken = default ) where T : class, IRequest diff --git a/src/Paramore.Brighter/CommandProcessorBuilder.cs b/src/Paramore.Brighter/CommandProcessorBuilder.cs index 642decf2bc..65e64436a3 100644 --- a/src/Paramore.Brighter/CommandProcessorBuilder.cs +++ b/src/Paramore.Brighter/CommandProcessorBuilder.cs @@ -84,7 +84,7 @@ public class CommandProcessorBuilder : INeedAHandlers, INeedPolicy, INeedMessagi private bool _useExternalBus = false; private bool _useRequestReplyQueues = false; private IEnumerable _replySubscriptions; - private IAmATransactionConnectionProvider _overridingBoxTransactionProvider = null; + private IAmABoxTransactionProvider _overridingBoxTransactionProvider = null; private int _outboxBulkChunkSize; private CommandProcessorBuilder() @@ -162,14 +162,14 @@ public INeedMessaging DefaultPolicy() /// /// The Task Queues configuration. /// The Outbox. - /// + /// /// INeedARequestContext. - public INeedARequestContext ExternalBus(ExternalBusConfiguration configuration, IAmAnOutbox outbox, IAmATransactionConnectionProvider transactionConnectionProvider = null) + public INeedARequestContext ExternalBus(ExternalBusConfiguration configuration, IAmAnOutbox outbox, IAmABoxTransactionProvider boxTransactionProvider = null) { _useExternalBus = true; _producers = configuration.ProducerRegistry; _outbox = outbox; - _overridingBoxTransactionProvider = transactionConnectionProvider; + _overridingBoxTransactionProvider = boxTransactionProvider; _messageMapperRegistry = configuration.MessageMapperRegistry; _outboxWriteTimeout = configuration.OutboxWriteTimeout; _outboxBulkChunkSize = configuration.OutboxBulkChunkSize; @@ -247,7 +247,7 @@ public CommandProcessor Build() producerRegistry: _producers, outboxTimeout: _outboxWriteTimeout, featureSwitchRegistry: _featureSwitchRegistry, - transactionConnectionProvider: _overridingBoxTransactionProvider, + transactionProvider: _overridingBoxTransactionProvider, outboxBulkChunkSize: _outboxBulkChunkSize, messageTransformerFactory: _transformerFactory ); @@ -263,7 +263,7 @@ public CommandProcessor Build() outBox: _outbox, producerRegistry: _producers, replySubscriptions: _replySubscriptions, - responseChannelFactory: _responseChannelFactory, transactionConnectionProvider: _overridingBoxTransactionProvider); + responseChannelFactory: _responseChannelFactory, transactionProvider: _overridingBoxTransactionProvider); } else { @@ -323,9 +323,9 @@ public interface INeedMessaging /// /// The configuration. /// The outbox. - /// The connection provider to use when adding messages to the bus + /// The transaction provider to use when adding messages to the bus /// INeedARequestContext. - INeedARequestContext ExternalBus(ExternalBusConfiguration configuration, IAmAnOutbox outbox, IAmATransactionConnectionProvider transactionConnectionProvider = null); + INeedARequestContext ExternalBus(ExternalBusConfiguration configuration, IAmAnOutbox outbox, IAmABoxTransactionProvider boxTransactionProvider = null); /// /// We don't send messages out of process /// diff --git a/src/Paramore.Brighter/ExternalBusServices.cs b/src/Paramore.Brighter/ExternalBusServices.cs index 31bb3bc071..7612a1c496 100644 --- a/src/Paramore.Brighter/ExternalBusServices.cs +++ b/src/Paramore.Brighter/ExternalBusServices.cs @@ -65,7 +65,13 @@ protected virtual void Dispose(bool disposing) } - internal async Task AddToOutboxAsync(T request, bool continueOnCapturedContext, CancellationToken cancellationToken, Message message, IAmATransactionConnectionProvider overridingAmATransactionProvider = null) + internal async Task AddToOutboxAsync( + T request, bool continueOnCapturedContext, + + CancellationToken cancellationToken, + Message message, + IAmABoxTransactionProvider overridingAmATransactionProvider = null + ) where T : class, IRequest { CheckOutboxOutstandingLimit(); @@ -79,7 +85,12 @@ internal async Task AddToOutboxAsync(T request, bool continueOnCapturedContex tags: new ActivityTagsCollection {{"MessageId", message.Id}})); } - internal async Task AddToOutboxAsync(IEnumerable messages, bool continueOnCapturedContext, CancellationToken cancellationToken, IAmATransactionConnectionProvider overridingAmATransactionProvider = null) + internal async Task AddToOutboxAsync( + IEnumerable messages, + bool continueOnCapturedContext, + CancellationToken cancellationToken, + IAmABoxTransactionProvider overridingTransactionProvider = null + ) { CheckOutboxOutstandingLimit(); @@ -92,7 +103,7 @@ internal async Task AddToOutboxAsync(IEnumerable messages, bool continu var written = await RetryAsync( async ct => { - await box.AddAsync(chunk, OutboxTimeout, ct, overridingAmATransactionProvider) + await box.AddAsync(chunk, OutboxTimeout, ct, overridingTransactionProvider) .ConfigureAwait(continueOnCapturedContext); }, continueOnCapturedContext, cancellationToken).ConfigureAwait(continueOnCapturedContext); @@ -107,7 +118,8 @@ await box.AddAsync(chunk, OutboxTimeout, ct, overridingAmATransactionProvider) } } - internal void AddToOutbox(T request, Message message, IAmATransactionConnectionProvider overridingAmATransactionProvider = null) where T : class, IRequest + internal void AddToOutbox(T request, Message message, IAmABoxTransactionProvider overridingAmATransactionProvider = null) + where T : class, IRequest { CheckOutboxOutstandingLimit(); @@ -119,7 +131,7 @@ internal void AddToOutbox(T request, Message message, IAmATransactionConnecti tags: new ActivityTagsCollection {{"MessageId", message.Id}})); } - internal void AddToOutbox(IEnumerable messages, IAmATransactionConnectionProvider overridingAmATransactionProvider = null) + internal void AddToOutbox(IEnumerable messages, IAmABoxTransactionProvider overridingTransactionProvider = null) { CheckOutboxOutstandingLimit(); @@ -130,7 +142,7 @@ internal void AddToOutbox(IEnumerable messages, IAmATransactionConnecti foreach (var chunk in ChunkMessages(messages)) { var written = - Retry(() => { box.Add(chunk, OutboxTimeout, overridingAmATransactionProvider); }); + Retry(() => { box.Add(chunk, OutboxTimeout, overridingTransactionProvider); }); if (!written) throw new ChannelFailureException($"Could not write {chunk.Count()} messages to the outbox"); diff --git a/src/Paramore.Brighter/IAmABulkOutboxAsync.cs b/src/Paramore.Brighter/IAmABulkOutboxAsync.cs index 382665f31b..9806856c00 100644 --- a/src/Paramore.Brighter/IAmABulkOutboxAsync.cs +++ b/src/Paramore.Brighter/IAmABulkOutboxAsync.cs @@ -46,8 +46,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, IAmATransactionConnectionProvider amATransactionProvider = 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 0c4555f12c..dc2087ce84 100644 --- a/src/Paramore.Brighter/IAmABulkOutboxSync.cs +++ b/src/Paramore.Brighter/IAmABulkOutboxSync.cs @@ -44,7 +44,7 @@ public interface IAmABulkOutboxSync : IAmAnOutboxSync where T : 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, IAmATransactionConnectionProvider amATransactionProvider = 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/IAmAnOutboxAsync.cs b/src/Paramore.Brighter/IAmAnOutboxAsync.cs index 91649f62cd..6bbdce04bf 100644 --- a/src/Paramore.Brighter/IAmAnOutboxAsync.cs +++ b/src/Paramore.Brighter/IAmAnOutboxAsync.cs @@ -53,9 +53,10 @@ 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, IAmATransactionConnectionProvider amATransactionProvider = 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 bdb44708b7..df3612e77a 100644 --- a/src/Paramore.Brighter/IAmAnOutboxSync.cs +++ b/src/Paramore.Brighter/IAmAnOutboxSync.cs @@ -41,8 +41,8 @@ public interface IAmAnOutboxSync : IAmAnOutbox where T : 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, IAmATransactionConnectionProvider amATransactionProvider = 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 29d3a666c4..7cd5da5a8d 100644 --- a/src/Paramore.Brighter/InMemoryOutbox.cs +++ b/src/Paramore.Brighter/InMemoryOutbox.cs @@ -98,8 +98,8 @@ public class InMemoryOutbox : InMemoryBox, IAmABulkOutboxSync /// /// - /// This is not used for the In Memory Outbox. - public void Add(Message message, int outBoxTimeout = -1, IAmATransactionConnectionProvider amATransactionProvider = 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 +119,15 @@ public void Add(Message message, int outBoxTimeout = -1, IAmATransactionConnecti /// /// /// - /// This is not used for the In Memory Outbox. - public void Add(IEnumerable messages, int outBoxTimeout = -1, IAmATransactionConnectionProvider amATransactionProvider = 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, amATransactionProvider); + Add(message, outBoxTimeout, transactionProvider); } } @@ -137,14 +137,12 @@ public void Add(IEnumerable messages, int outBoxTimeout = -1, IAmATrans /// /// /// - /// 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, - IAmATransactionConnectionProvider amATransactionProvider = null - ) + public Task AddAsync(Message message, + int outBoxTimeout = -1, + CancellationToken cancellationToken = default, + IAmABoxTransactionProvider transactionProvider = null) { var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); @@ -166,13 +164,13 @@ public Task AddAsync( /// /// /// - /// 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, - IAmATransactionConnectionProvider amATransactionProvider = null + IAmABoxTransactionProvider transactionProvider = null ) { var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); diff --git a/src/Paramore.Brighter/RelationDatabaseOutbox.cs b/src/Paramore.Brighter/RelationDatabaseOutbox.cs index 3f1769604a..f5a81035c2 100644 --- a/src/Paramore.Brighter/RelationDatabaseOutbox.cs +++ b/src/Paramore.Brighter/RelationDatabaseOutbox.cs @@ -38,15 +38,15 @@ protected RelationDatabaseOutbox(string outboxTableName, IRelationDatabaseOutbox /// /// 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, - IAmATransactionConnectionProvider amATransactionProvider = null) + IAmABoxTransactionProvider transactionProvider = null) { var parameters = InitAddDbParameters(message); - WriteToStore(amATransactionProvider, 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", @@ -59,15 +59,15 @@ public void Add( /// /// 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, - IAmATransactionConnectionProvider amATransactionProvider = null + IAmABoxTransactionProvider transactionProvider = null ) { - WriteToStore(amATransactionProvider, + WriteToStore(transactionProvider, connection => InitBulkAddDbCommand(messages.ToList(), connection), () => _logger.LogWarning("MsSqlOutbox: At least one message already exists in the outbox")); } @@ -88,17 +88,15 @@ 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, + public Task AddAsync(Message message, int outBoxTimeout = -1, CancellationToken cancellationToken = default, - IAmATransactionConnectionProvider amATransactionProvider = null - ) + IAmABoxTransactionProvider transactionProvider = null) { var parameters = InitAddDbParameters(message); - return WriteToStoreAsync(amATransactionProvider, + return WriteToStoreAsync(transactionProvider, connection => InitAddDbCommand(connection, parameters), () => { _logger.LogWarning( @@ -114,16 +112,16 @@ public Task AddAsync( /// 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, CancellationToken cancellationToken = default, - IAmATransactionConnectionProvider amATransactionProvider = null + IAmABoxTransactionProvider transactionProvider = null ) { - return WriteToStoreAsync(amATransactionProvider, + return WriteToStoreAsync(transactionProvider, connection => InitBulkAddDbCommand(messages.ToList(), connection), () => _logger.LogWarning("MsSqlOutbox: At least one message already exists in the outbox"), cancellationToken); @@ -343,13 +341,13 @@ public Task DeleteAsync(CancellationToken cancellationToken, params Guid[] messa #endregion protected abstract void WriteToStore( - IAmATransactionConnectionProvider provider, + IAmABoxTransactionProvider transactionProvider, Func commandFunc, Action loggingAction ); protected abstract Task WriteToStoreAsync( - IAmATransactionConnectionProvider transactionConnectionProvider, + IAmABoxTransactionProvider transactionProvider, Func commandFunc, Action loggingAction, CancellationToken cancellationToken diff --git a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/TestDoubles/FakeOutboxSync.cs b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/TestDoubles/FakeOutboxSync.cs index 2ceb3649bb..c59b171254 100644 --- a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/TestDoubles/FakeOutboxSync.cs +++ b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/TestDoubles/FakeOutboxSync.cs @@ -37,12 +37,17 @@ public class FakeOutboxSync : IAmABulkOutboxSync, IAmABulkOutboxAsync messages, int outBoxTimeout = -1, - IAmATransactionConnectionProvider transactionProvider = null) + IAmABoxTransactionProvider transactionProvider = null) { foreach (Message message in messages) { @@ -198,7 +203,7 @@ public void Add(IEnumerable messages, int outBoxTimeout = -1, public async Task AddAsync(IEnumerable messages, int outBoxTimeout = -1, CancellationToken cancellationToken = default, - IAmATransactionConnectionProvider transactionProvider = null) + IAmABoxTransactionProvider transactionProvider = null) { foreach (var message in messages) { From 3592d73d8a4042fdc79801f839e32edeb11745f5 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Sat, 20 May 2023 15:00:23 +0100 Subject: [PATCH 48/89] Move to a single abstraction for boxtransactionprovider --- samples/OpenTelemetry/Sweeper/Program.cs | 10 +- .../Handlers/AddGreetingHandlerAsync.cs | 6 +- .../Handlers/AddPersonHandlerAsync.cs | 3 +- .../Handlers/DeletePersonHandlerAsync.cs | 3 +- .../FIndGreetingsForPersonHandlerAsync.cs | 3 +- .../Handlers/FindPersonByNameHandlerAsync.cs | 3 +- .../SalutationAnalytics/Program.cs | 2 +- .../Handlers/GreetingMadeHandler.cs | 6 +- .../BrighterOutboxConnectionHealthCheck.cs | 13 +- .../DynamoDbUnitOfWork.cs | 89 ++- .../IDynamoDbClientProvider.cs | 43 +- .../IDynamoDbTransactionProvider.cs | 6 +- .../ServiceCollectionExtensions.cs | 265 ++++---- .../HostedServiceCollectionExtensions.cs | 4 +- .../TimedOutboxArchiver.cs | 12 +- .../DynamoDbOutbox.cs | 14 +- .../ServiceCollectionExtensions.cs | 22 +- .../EventStoreOutboxSync.cs | 14 +- .../ServiceCollectionExtensions.cs | 5 +- .../MsSqlOutbox.cs | 34 +- .../ServiceCollectionExtensions.cs | 29 +- .../MySqlOutbox.cs | 32 +- .../ServiceCollectionExtensions.cs | 16 +- .../PostgreSqlOutbox.cs | 33 +- .../ServiceCollectionExtensions.cs | 39 +- .../ServiceCollectionExtensions.cs | 19 +- .../SqliteOutbox.cs | 38 +- .../ServiceCollectionExtensions.cs | 29 +- .../ControlBusReceiverBuilder.cs | 5 +- src/Paramore.Brighter/CommandProcessor.cs | 510 ++++++++------- .../CommandProcessorBuilder.cs | 65 +- src/Paramore.Brighter/ControlBusSender.cs | 35 +- .../ControlBusSenderFactory.cs | 3 +- src/Paramore.Brighter/ExternalBusServices.cs | 603 +++++++++++------- .../IAmABoxTransactionProvider.cs | 55 +- src/Paramore.Brighter/IAmABulkOutboxAsync.cs | 8 +- src/Paramore.Brighter/IAmABulkOutboxSync.cs | 5 +- src/Paramore.Brighter/IAmACommandProcessor.cs | 171 ++++- src/Paramore.Brighter/IAmAControlBusSender.cs | 31 +- .../IAmAControlBusSenderFactory.cs | 5 +- src/Paramore.Brighter/IAmAMessageRecoverer.cs | 6 +- .../IAmARelationalDbConnectionProvider.cs | 45 +- .../IAmATransactionConnectionProvider.cs | 6 +- .../IAmAnExternalBusService.cs | 80 +++ src/Paramore.Brighter/IAmAnOutbox.cs | 5 +- src/Paramore.Brighter/IAmAnOutboxAsync.cs | 13 +- src/Paramore.Brighter/IAmAnOutboxSync.cs | 8 +- src/Paramore.Brighter/InMemoryOutbox.cs | 89 ++- src/Paramore.Brighter/MessageRecovery.cs | 18 +- .../Monitoring/Handlers/MonitorHandler.cs | 13 +- .../Handlers/MonitorHandlerAsync.cs | 9 +- src/Paramore.Brighter/OutboxArchiver.cs | 26 +- .../RelationDatabaseOutbox.cs | 18 +- .../TestDoubles/FakeMessageProducer.cs | 6 +- .../{FakeOutboxSync.cs => FakeOutbox.cs} | 50 +- ..._PostBox_On_The_Command_Processor_Async.cs | 26 +- ...ling_A_Server_Via_The_Command_Processor.cs | 24 +- ...The_Command_Processor_With_No_In_Mapper.cs | 31 +- ...he_Command_Processor_With_No_Out_Mapper.cs | 28 +- ...a_The_Command_Processor_With_No_Timeout.cs | 26 +- ...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 +- ...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 | 16 +- ...age_And_There_Is_No_Message_Store_Async.cs | 11 +- ...ting_A_Message_To_The_Command_Processor.cs | 17 +- ..._Message_To_The_Command_Processor_Async.cs | 17 +- .../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 | 10 +- ...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 | 5 +- .../TestDoubles/SpyCommandProcessor.cs | 123 +++- ..._Clearing_The_Outbox_A_Span_Is_Exported.cs | 17 +- ...ing_The_Outbox_async_A_Span_Is_Exported.cs | 17 +- ...a_transaction_between_outbox_and_entity.cs | 9 +- .../TestDoubles/FakeCommandProcessor.cs | 32 + 86 files changed, 2200 insertions(+), 1242 deletions(-) create mode 100644 src/Paramore.Brighter/IAmAnExternalBusService.cs rename tests/Paramore.Brighter.Core.Tests/CommandProcessors/TestDoubles/{FakeOutboxSync.cs => FakeOutbox.cs} (81%) diff --git a/samples/OpenTelemetry/Sweeper/Program.cs b/samples/OpenTelemetry/Sweeper/Program.cs index 60dcc1bfb6..57595bb943 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(); @@ -49,7 +43,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/WebAPI_Dynamo/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs b/samples/WebAPI_Dynamo/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs index 69b1f1b125..66a1215d07 100644 --- a/samples/WebAPI_Dynamo/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs +++ b/samples/WebAPI_Dynamo/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs @@ -21,7 +21,7 @@ public class AddGreetingHandlerAsync: RequestHandlerAsync private readonly ILogger _logger; - public AddGreetingHandlerAsync(IAmABoxTransactionProvider uow, IAmACommandProcessor postBox, ILogger logger) + public AddGreetingHandlerAsync(IAmABoxTransactionProvider uow, IAmACommandProcessor postBox, ILogger logger) { _unitOfWork = (DynamoDbUnitOfWork)uow; _postBox = postBox; @@ -37,7 +37,7 @@ 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 transaction = await _unitOfWork.GetTransactionAsync(cancellationToken); try { var person = await context.LoadAsync(addGreeting.Name, cancellationToken); @@ -61,7 +61,7 @@ public override async Task HandleAsync(AddGreeting addGreeting, Can { _logger.LogError(e, "Exception thrown handling Add Greeting request"); //it went wrong, rollback the entity change and the downstream message - _unitOfWork.Rollback(); + await _unitOfWork.RollbackAsync(cancellationToken); return await base.HandleAsync(addGreeting, cancellationToken); } diff --git a/samples/WebAPI_Dynamo/GreetingsPorts/Handlers/AddPersonHandlerAsync.cs b/samples/WebAPI_Dynamo/GreetingsPorts/Handlers/AddPersonHandlerAsync.cs index 918c774ff2..c6eee8a174 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; @@ -14,7 +15,7 @@ public class AddPersonHandlerAsync : RequestHandlerAsync { private readonly DynamoDbUnitOfWork _dynamoDbUnitOfWork; - public AddPersonHandlerAsync(IAmABoxTransactionProvider dynamoDbUnitOfWork) + public AddPersonHandlerAsync(IAmABoxTransactionProvider dynamoDbUnitOfWork) { _dynamoDbUnitOfWork = (DynamoDbUnitOfWork )dynamoDbUnitOfWork; } diff --git a/samples/WebAPI_Dynamo/GreetingsPorts/Handlers/DeletePersonHandlerAsync.cs b/samples/WebAPI_Dynamo/GreetingsPorts/Handlers/DeletePersonHandlerAsync.cs index 747f46d269..8a300aa5ff 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; @@ -13,7 +14,7 @@ public class DeletePersonHandlerAsync : RequestHandlerAsync { private readonly DynamoDbUnitOfWork _unitOfWork; - public DeletePersonHandlerAsync(IAmABoxTransactionProvider unitOfWork) + public DeletePersonHandlerAsync(IAmABoxTransactionProvider unitOfWork) { _unitOfWork = (DynamoDbUnitOfWork)unitOfWork; } diff --git a/samples/WebAPI_Dynamo/GreetingsPorts/Handlers/FIndGreetingsForPersonHandlerAsync.cs b/samples/WebAPI_Dynamo/GreetingsPorts/Handlers/FIndGreetingsForPersonHandlerAsync.cs index 06466e3bc2..67bded1dd0 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; @@ -18,7 +19,7 @@ public class FIndGreetingsForPersonHandlerAsync : QueryHandlerAsync unitOfWork) { _unitOfWork = (DynamoDbUnitOfWork ) unitOfWork; } diff --git a/samples/WebAPI_Dynamo/GreetingsPorts/Handlers/FindPersonByNameHandlerAsync.cs b/samples/WebAPI_Dynamo/GreetingsPorts/Handlers/FindPersonByNameHandlerAsync.cs index d8167603f9..b5d23ce38d 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; @@ -19,7 +20,7 @@ public class FindPersonByNameHandlerAsync : QueryHandlerAsync unitOfWork) { _unitOfWork = (DynamoDbUnitOfWork ) unitOfWork; } diff --git a/samples/WebAPI_Dynamo/SalutationAnalytics/Program.cs b/samples/WebAPI_Dynamo/SalutationAnalytics/Program.cs index 15cb7c7b57..fc2de6c002 100644 --- a/samples/WebAPI_Dynamo/SalutationAnalytics/Program.cs +++ b/samples/WebAPI_Dynamo/SalutationAnalytics/Program.cs @@ -240,7 +240,7 @@ private static IAmAnInbox ConfigureInbox(IAmazonDynamoDB dynamoDb) return new DynamoDbInbox(dynamoDb); } - 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 dc5585878d..03f86dcde9 100644 --- a/samples/WebAPI_Dynamo/SalutationPorts/Handlers/GreetingMadeHandler.cs +++ b/samples/WebAPI_Dynamo/SalutationPorts/Handlers/GreetingMadeHandler.cs @@ -20,7 +20,7 @@ public class GreetingMadeHandlerAsync : RequestHandlerAsync private readonly IAmACommandProcessor _postBox; private readonly ILogger _logger; - public GreetingMadeHandlerAsync(IAmABoxTransactionProvider uow, IAmACommandProcessor postBox, ILogger logger) + public GreetingMadeHandlerAsync(IAmABoxTransactionProvider uow, IAmACommandProcessor postBox, ILogger logger) { _uow = (DynamoDbUnitOfWork)uow; _postBox = postBox; @@ -34,7 +34,7 @@ public override async Task HandleAsync(GreetingMade @event, Cancel { var posts = new List(); var context = new DynamoDBContext(_uow.DynamoDb); - var tx = _uow.BeginOrGetTransaction(); + var tx = await _uow.GetTransactionAsync(cancellationToken); try { var salutation = new Salutation{ Greeting = @event.Greeting}; @@ -49,7 +49,7 @@ public override async Task HandleAsync(GreetingMade @event, Cancel catch (Exception e) { _logger.LogError(e, "Could not save salutation"); - _uow.Rollback(); + await _uow.RollbackAsync(cancellationToken); return await base.HandleAsync(@event, cancellationToken); } diff --git a/samples/WebApiWithWorkerAndSweeper/Orders.Sweeper/HealthChecks/BrighterOutboxConnectionHealthCheck.cs b/samples/WebApiWithWorkerAndSweeper/Orders.Sweeper/HealthChecks/BrighterOutboxConnectionHealthCheck.cs index 6c582a95f4..813d1fe8dc 100644 --- a/samples/WebApiWithWorkerAndSweeper/Orders.Sweeper/HealthChecks/BrighterOutboxConnectionHealthCheck.cs +++ b/samples/WebApiWithWorkerAndSweeper/Orders.Sweeper/HealthChecks/BrighterOutboxConnectionHealthCheck.cs @@ -7,31 +7,24 @@ namespace Orders.Sweeper.HealthChecks; public class BrighterOutboxConnectionHealthCheck : IHealthCheck { - private readonly IAmARelationalDbConnectionProvider _connectionProvider; + private readonly IAmARelationalDbConnectionProvider _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/src/Paramore.Brighter.DynamoDb/DynamoDbUnitOfWork.cs b/src/Paramore.Brighter.DynamoDb/DynamoDbUnitOfWork.cs index 17cd772623..96c2b3af01 100644 --- a/src/Paramore.Brighter.DynamoDb/DynamoDbUnitOfWork.cs +++ b/src/Paramore.Brighter.DynamoDb/DynamoDbUnitOfWork.cs @@ -15,66 +15,85 @@ 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; } - /// - /// 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 - /// just add your items to the list - /// i.e. tx.TransactItems.Add(new TransactWriteItem(... - /// - /// - public TransactWriteItemsRequest BeginOrGetTransaction() + public void Close() { - if (HasTransaction()) - { - return _tx; - } - else - { - _tx = new TransactWriteItemsRequest(); - _tx.TransactItems = new List(); - return _tx; - } + _tx = null; } /// /// Commit a transaction, performing all associated write actions /// - /// A response indicating the status of the transaction - public TransactWriteItemsResponse Commit() + public void Commit() { - if (!HasTransaction()) + if (!HasOpenTransaction) throw new InvalidOperationException("No transaction to commit"); - return DynamoDb.TransactWriteItemsAsync(_tx).GetAwaiter().GetResult(); - } + LastResponse = DynamoDb.TransactWriteItemsAsync(_tx).GetAwaiter().GetResult(); - /// + } + + /// /// Commit a transaction, performing all associated write actions /// /// A response indicating the status of the transaction public async Task CommitAsync(CancellationToken ct = default) { - if (!HasTransaction()) - throw new InvalidOperationException("No transaction to commit"); + if (!HasOpenTransaction) + throw new InvalidOperationException("No transaction to commit"); - return await DynamoDb.TransactWriteItemsAsync(_tx, ct); - } + LastResponse = await DynamoDb.TransactWriteItemsAsync(_tx, ct); + return LastResponse; + } + /// - /// Is there an existing transaction + /// 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 + /// just add your items to the list + /// i.e. tx.TransactItems.Add(new TransactWriteItem(... /// /// - public bool HasTransaction() + public TransactWriteItemsRequest GetTransaction() { - return _tx != null; + if (HasOpenTransaction) + { + return _tx; + } + + _tx = new TransactWriteItemsRequest(); + _tx.TransactItems = new List(); + return _tx; } + public Task GetTransactionAsync(CancellationToken cancellationToken = default) + { + var tcs = new TaskCompletionSource(); + tcs.SetResult(GetTransaction()); + return tcs.Task; + } + + /// + /// Is there an existing transaction? + /// + /// + public bool HasOpenTransaction => _tx != null; + + /// + /// Is there a shared connection, not true, but we do not manage the DynamoDb client + /// + public bool IsSharedConnection => false; + /// /// Clear any transaction /// @@ -83,12 +102,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/IDynamoDbClientProvider.cs b/src/Paramore.Brighter.DynamoDb/IDynamoDbClientProvider.cs index 86b0a887d6..02a09ea14b 100644 --- a/src/Paramore.Brighter.DynamoDb/IDynamoDbClientProvider.cs +++ b/src/Paramore.Brighter.DynamoDb/IDynamoDbClientProvider.cs @@ -7,40 +7,21 @@ namespace Paramore.Brighter.DynamoDb { public interface IDynamoDbClientProvider { + /// + /// Commits the transaction to Dynamo + /// + /// A cancellation token + /// + Task CommitAsync(CancellationToken ct = default); + /// /// 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(); + IAmazonDynamoDB DynamoDb { get; } - /// - /// Clear any transaction - /// - void Rollback(); + /// + /// The response for the last transaction commit + /// + TransactWriteItemsResponse LastResponse { get; set; } } } - diff --git a/src/Paramore.Brighter.DynamoDb/IDynamoDbTransactionProvider.cs b/src/Paramore.Brighter.DynamoDb/IDynamoDbTransactionProvider.cs index c837c6eede..a979d57246 100644 --- a/src/Paramore.Brighter.DynamoDb/IDynamoDbTransactionProvider.cs +++ b/src/Paramore.Brighter.DynamoDb/IDynamoDbTransactionProvider.cs @@ -1,6 +1,8 @@ -namespace Paramore.Brighter.DynamoDb +using Amazon.DynamoDBv2.Model; + +namespace Paramore.Brighter.DynamoDb { - public interface IDynamoDbClientTransactionProvider : IDynamoDbClientProvider, IAmABoxTransactionProvider + public interface IDynamoDbClientTransactionProvider : IDynamoDbClientProvider, IAmABoxTransactionProvider { } diff --git a/src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionExtensions.cs b/src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionExtensions.cs index 4b34a42da1..b8392afaf5 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,24 +20,28 @@ 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 using System; using System.Collections.Generic; +using System.Net.Http; 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; namespace Paramore.Brighter.Extensions.DependencyInjection { public static class ServiceCollectionExtensions { private static int _outboxBulkChunkSize = 100; + private static Type _outboxType; + private static Type _asyncOutboxType; /// /// Will add Brighter into the .NET IoC Container - ServiceCollection @@ -48,9 +53,20 @@ public static class ServiceCollectionExtensions /// /// 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 + /// 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 + ) + { + return AddBrighterWithTransactionalMessaging(services, configure); + } + + private static IBrighterBuilder AddBrighterWithTransactionalMessaging( + IServiceCollection services, + Action configure) { if (services == null) throw new ArgumentNullException(nameof(services)); @@ -59,19 +75,21 @@ public static IBrighterBuilder AddBrighter(this IServiceCollection services, Act configure?.Invoke(options); services.TryAddSingleton(options); - return BrighterHandlerBuilder(services, options); + return BrighterHandlerBuilder(services, options); } /// - /// Normally you want to call AddBrighter from client code, and not this method. Public only to support Service Activator extensions + /// 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 :- /// - 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 - public static IBrighterBuilder BrighterHandlerBuilder(IServiceCollection services, BrighterOptions options) + /// Allows you to configure how we build Brighter + /// A builder that can be used to populate the IoC container with handlers and mappers by inspection + /// - used by built in factory from CommandProcessor + public static IBrighterBuilder BrighterHandlerBuilder(IServiceCollection services, BrighterOptions options) where TMessage : Message { var subscriberRegistry = new ServiceCollectionSubscriberRegistry(services, options.HandlerLifetime); services.TryAddSingleton(subscriberRegistry); @@ -79,87 +97,103 @@ public static IBrighterBuilder BrighterHandlerBuilder(IServiceCollection service var transformRegistry = new ServiceCollectionTransformerRegistry(services, options.TransformerLifetime); services.TryAddSingleton(transformRegistry); - services.TryAdd(new ServiceDescriptor(typeof(IAmACommandProcessor), BuildCommandProcessor, options.CommandProcessorLifetime)); + services.TryAdd(new ServiceDescriptor(typeof(IAmACommandProcessor), + (serviceProvider) => (IAmACommandProcessor)BuildCommandProcessor(serviceProvider), + options.CommandProcessorLifetime)); var mapperRegistry = new ServiceCollectionMessageMapperRegistry(services, options.MapperLifetime); services.TryAddSingleton(mapperRegistry); - return new ServiceCollectionBrighterBuilder(services, subscriberRegistry, mapperRegistry, transformRegistry); + return new ServiceCollectionBrighterBuilder(services, subscriberRegistry, mapperRegistry, + transformRegistry); } /// /// 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 :- + /// 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) /// /// 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 + /// 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) + public static IBrighterBuilder UseExternalOutbox( + this IBrighterBuilder brighterBuilder, + IAmAnOutbox outbox = null, + int outboxBulkChunkSize = 100 + ) where TMessage : Message { - if (outbox is IAmAnOutboxSync) + + if (outbox is IAmAnOutboxSync) { - brighterBuilder.Services.TryAdd(new ServiceDescriptor(typeof(IAmAnOutboxSync), _ => outbox, ServiceLifetime.Singleton)); + _outboxType = typeof(IAmAnOutboxSync); + brighterBuilder.Services.TryAdd(new ServiceDescriptor(_outboxType, factory:_ => outbox, ServiceLifetime.Singleton)); } - if (outbox is IAmAnOutboxAsync) + if (outbox is IAmAnOutboxAsync) { - brighterBuilder.Services.TryAdd(new ServiceDescriptor(typeof(IAmAnOutboxAsync), _ => outbox, ServiceLifetime.Singleton)); + _asyncOutboxType = typeof(IAmAnOutboxAsync); + brighterBuilder.Services.TryAdd(new ServiceDescriptor(_asyncOutboxType, _ => outbox, ServiceLifetime.Singleton)); } _outboxBulkChunkSize = outboxBulkChunkSize; - + return brighterBuilder; - } - /// - /// 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; - } + /// + /// 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 + /// The lifetime for the inbox, defaults to singleton + /// + public static IBrighterBuilder UseExternalInbox( + this IBrighterBuilder brighterBuilder, + IAmAnInbox inbox, InboxConfiguration inboxConfiguration = null, + ServiceLifetime serviceLifetime = ServiceLifetime.Singleton) + { + 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 + /// 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) @@ -168,8 +202,10 @@ public static IBrighterBuilder UseExternalInbox( /// The Brighter builder to allow chaining of requests public static IBrighterBuilder UseInMemoryOutbox(this IBrighterBuilder brighterBuilder) { - brighterBuilder.Services.TryAdd(new ServiceDescriptor(typeof(IAmAnOutboxSync), _ => new InMemoryOutbox(), ServiceLifetime.Singleton)); - brighterBuilder.Services.TryAdd(new ServiceDescriptor(typeof(IAmAnOutboxAsync), _ => new InMemoryOutbox(), ServiceLifetime.Singleton)); + brighterBuilder.Services.TryAdd(new ServiceDescriptor(typeof(IAmAnOutboxSync), + _ => new InMemoryOutbox(), ServiceLifetime.Singleton)); + brighterBuilder.Services.TryAdd(new ServiceDescriptor(typeof(IAmAnOutboxAsync), + _ => new InMemoryOutbox(), ServiceLifetime.Singleton)); return brighterBuilder; } @@ -187,15 +223,18 @@ public static IBrighterBuilder UseInMemoryOutbox(this IBrighterBuilder brighterB /// public static IBrighterBuilder UseInMemoryInbox(this IBrighterBuilder brighterBuilder) { - brighterBuilder.Services.TryAdd(new ServiceDescriptor(typeof(IAmAnInboxSync), _ => new InMemoryInbox(), ServiceLifetime.Singleton)); - brighterBuilder.Services.TryAdd(new ServiceDescriptor(typeof(IAmAnInboxAsync), _ => new InMemoryInbox(), ServiceLifetime.Singleton)); + brighterBuilder.Services.TryAdd(new ServiceDescriptor(typeof(IAmAnInboxSync), _ => new InMemoryInbox(), + ServiceLifetime.Singleton)); + brighterBuilder.Services.TryAdd(new ServiceDescriptor(typeof(IAmAnInboxAsync), _ => new InMemoryInbox(), + ServiceLifetime.Singleton)); return brighterBuilder; } /// - /// 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 + /// 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 @@ -205,13 +244,17 @@ public static IBrighterBuilder UseInMemoryInbox(this IBrighterBuilder brighterBu /// 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) + public static IBrighterBuilder UseExternalBus( + this IBrighterBuilder brighterBuilder, + IAmAProducerRegistry producerRegistry, + bool useRequestResponseQueues = false, + IEnumerable replyQueueSubscriptions = null) { - brighterBuilder.Services.TryAddSingleton(producerRegistry); - - brighterBuilder.Services.TryAddSingleton(new UseRpc(useRequestResponseQueues, replyQueueSubscriptions)); - + + brighterBuilder.Services.TryAddSingleton(new UseRpc(useRequestResponseQueues, + replyQueueSubscriptions)); + return brighterBuilder; } @@ -221,29 +264,31 @@ public static IBrighterBuilder UseExternalBus(this IBrighterBuilder brighterBuil /// 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) + 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 /// /// 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 @@ -265,7 +310,8 @@ public static MessageMapperRegistry MessageMapperRegistry(IServiceProvider provi } /// - /// Creates transforms. Normally you don't need to call this, it is called by the builder for Brighter or the Service Activator + /// 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 @@ -274,8 +320,9 @@ public static ServiceProviderTransformerFactory TransformFactory(IServiceProvide { return new ServiceProviderTransformerFactory(provider); } - - private static CommandProcessor BuildCommandProcessor(IServiceProvider provider) + + private static object BuildCommandProcessor(IServiceProvider provider) + where TMessage : Message { var loggerFactory = provider.GetService(); ApplicationLogging.LoggerFactory = loggerFactory; @@ -291,15 +338,11 @@ private static CommandProcessor BuildCommandProcessor(IServiceProvider 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(); + IAmAnOutbox outbox = provider.GetService>(); + if (outbox == null) outbox = new InMemoryOutbox() as IAmAnOutbox; var inboxConfiguration = provider.GetService(); - + var producerRegistry = provider.GetService(); var needHandlers = CommandProcessorBuilder.With(); @@ -308,7 +351,7 @@ private static CommandProcessor BuildCommandProcessor(IServiceProvider provider) if (featureSwitchRegistry != null) needHandlers = needHandlers.ConfigureFeatureSwitches(featureSwitchRegistry); - + var policyBuilder = needHandlers.Handlers(handlerConfiguration); var messagingBuilder = options.PolicyRegistry == null @@ -316,13 +359,12 @@ private static CommandProcessor BuildCommandProcessor(IServiceProvider provider) : policyBuilder.Policies(options.PolicyRegistry); var commandProcessor = AddExternalBusMaybe( - options, - producerRegistry, - messagingBuilder, - messageMapperRegistry, - inboxConfiguration, - outbox, - overridingConnectionProvider, + options, + producerRegistry, + messagingBuilder, + messageMapperRegistry, + inboxConfiguration, + outbox, useRequestResponse, _outboxBulkChunkSize, transformFactory) @@ -339,18 +381,17 @@ private enum ExternalBusType FireAndForget = 1, RPC = 2 } - - private static INeedARequestContext AddExternalBusMaybe( - IBrighterOptions options, - IAmAProducerRegistry producerRegistry, + + private static INeedARequestContext AddExternalBusMaybe( + IBrighterOptions options, + IAmAProducerRegistry producerRegistry, INeedMessaging messagingBuilder, - MessageMapperRegistry messageMapperRegistry, - InboxConfiguration inboxConfiguration, - IAmAnOutboxSync outbox, - IAmABoxTransactionProvider overridingProvider, + MessageMapperRegistry messageMapperRegistry, + InboxConfiguration inboxConfiguration, + IAmAnOutbox outbox, IUseRpc useRequestResponse, int outboxBulkChunkSize, - IAmAMessageTransformerFactory transformerFactory) + IAmAMessageTransformerFactory transformerFactory) where TMessage : Message { ExternalBusType externalBusType = GetExternalBusType(producerRegistry, useRequestResponse); @@ -359,13 +400,13 @@ private static INeedARequestContext AddExternalBusMaybe( else if (externalBusType == ExternalBusType.FireAndForget) return messagingBuilder.ExternalBus( new ExternalBusConfiguration( - producerRegistry, - messageMapperRegistry, - outboxBulkChunkSize: outboxBulkChunkSize, + producerRegistry, + messageMapperRegistry, + outboxBulkChunkSize: outboxBulkChunkSize, useInbox: inboxConfiguration, transformerFactory: transformerFactory), - outbox, - overridingProvider); + outbox + ); else if (externalBusType == ExternalBusType.RPC) { return messagingBuilder.ExternalRPC( @@ -381,10 +422,12 @@ private static INeedARequestContext AddExternalBusMaybe( throw new ArgumentOutOfRangeException("The external bus type requested was not understood"); } - private static ExternalBusType GetExternalBusType(IAmAProducerRegistry producerRegistry, IUseRpc useRequestResponse) + private static ExternalBusType GetExternalBusType(IAmAProducerRegistry producerRegistry, + IUseRpc useRequestResponse) { var externalBusType = producerRegistry == null ? ExternalBusType.None : ExternalBusType.FireAndForget; - if (externalBusType == ExternalBusType.FireAndForget && useRequestResponse.RPC) externalBusType = ExternalBusType.RPC; + if (externalBusType == ExternalBusType.FireAndForget && useRequestResponse.RPC) + externalBusType = ExternalBusType.RPC; return externalBusType; } } 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 1dcc1f41a0..629c1f6a63 100644 --- a/src/Paramore.Brighter.Extensions.Hosting/TimedOutboxArchiver.cs +++ b/src/Paramore.Brighter.Extensions.Hosting/TimedOutboxArchiver.cs @@ -9,15 +9,17 @@ 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; - public TimedOutboxArchiver(IAmAnOutbox outbox, IAmAnArchiveProvider archiveProvider, + public TimedOutboxArchiver( + IAmAnOutbox outbox, + IAmAnArchiveProvider archiveProvider, TimedOutboxArchiverOptions options) { _outbox = outbox; @@ -54,7 +56,7 @@ private void Archive(object state) try { - var outBoxArchiver = new OutboxArchiver( + var outBoxArchiver = new OutboxArchiver( _outbox, _archiveProvider, _options.BatchSize); diff --git a/src/Paramore.Brighter.Outbox.DynamoDB/DynamoDbOutbox.cs b/src/Paramore.Brighter.Outbox.DynamoDB/DynamoDbOutbox.cs index 8d8b4b6165..da20284de8 100644 --- a/src/Paramore.Brighter.Outbox.DynamoDB/DynamoDbOutbox.cs +++ b/src/Paramore.Brighter.Outbox.DynamoDB/DynamoDbOutbox.cs @@ -37,8 +37,8 @@ THE SOFTWARE. */ namespace Paramore.Brighter.Outbox.DynamoDB { public class DynamoDbOutbox : - IAmAnOutboxSync, - IAmAnOutboxAsync + IAmAnOutboxSync, + IAmAnOutboxAsync { private readonly DynamoDbConfiguration _configuration; private readonly DynamoDBContext _context; @@ -76,7 +76,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, IAmABoxTransactionProvider transactionProvider = null) + public void Add( + Message message, + int outBoxTimeout = -1, + IAmABoxTransactionProvider transactionProvider = null + ) { AddAsync(message, outBoxTimeout).ConfigureAwait(ContinueOnCapturedContext).GetAwaiter().GetResult(); } @@ -92,7 +96,7 @@ public void Add(Message message, int outBoxTimeout = -1, IAmABoxTransactionProvi public async Task AddAsync(Message message, int outBoxTimeout = -1, CancellationToken cancellationToken = default, - IAmABoxTransactionProvider transactionProvider = null) + IAmABoxTransactionProvider transactionProvider = null) { var messageToStore = new MessageItem(message); @@ -397,7 +401,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/ServiceCollectionExtensions.cs b/src/Paramore.Brighter.Outbox.DynamoDB/ServiceCollectionExtensions.cs index 4f1edfa72b..33ec44b72d 100644 --- a/src/Paramore.Brighter.Outbox.DynamoDB/ServiceCollectionExtensions.cs +++ b/src/Paramore.Brighter.Outbox.DynamoDB/ServiceCollectionExtensions.cs @@ -1,5 +1,6 @@ using System; using Amazon.DynamoDBv2; +using Amazon.DynamoDBv2.Model; using Microsoft.Extensions.DependencyInjection; using Paramore.Brighter.Extensions.DependencyInjection; @@ -20,11 +21,10 @@ public static class ServiceCollectionExtensions /// /// The lifetime of the outbox connection /// - public static IBrighterBuilder UseDynamoDbOutbox( - this IBrighterBuilder brighterBuilder, ServiceLifetime serviceLifetime = ServiceLifetime.Singleton) + 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)); + brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmAnOutboxSync), BuildDynamoDbOutbox, serviceLifetime)); + brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmAnOutboxAsync), BuildDynamoDbOutbox, serviceLifetime)); return brighterBuilder; } @@ -33,17 +33,24 @@ public static IBrighterBuilder UseDynamoDbOutbox( /// 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 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, + this IBrighterBuilder brighterBuilder, + Type transactionProvider, ServiceLifetime serviceLifetime = ServiceLifetime.Scoped) { - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmABoxTransactionProvider), connectionProvider, serviceLifetime)); + if (transactionProvider is null) + throw new ArgumentNullException($"{nameof(transactionProvider)} cannot be null.", nameof(transactionProvider)); + + if (!typeof(IAmABoxTransactionProvider).IsAssignableFrom(transactionProvider)) + throw new Exception($"Unable to register provider of type {transactionProvider.Name}. Class does not implement interface {nameof(IAmABoxTransactionProvider)}."); + + brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmABoxTransactionProvider), transactionProvider, serviceLifetime)); return brighterBuilder; } @@ -57,7 +64,6 @@ private static DynamoDbOutbox BuildDynamoDbOutbox(IServiceProvider provider) 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 91d69df89d..3ca0375832 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,7 +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(); @@ -72,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, IAmABoxTransactionProvider transactionProvider = 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)); @@ -93,12 +99,12 @@ public void Add(Message message, int outBoxTimeout = -1, IAmABoxTransactionProvi /// 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, - IAmABoxTransactionProvider transactionProvider = null) + IAmABoxTransactionProvider transactionProvider = null) { s_logger.LogDebug("Adding message to Event Store Outbox: {Request}", JsonSerializer.Serialize(message, JsonSerialisationOptions.Options)); 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/MsSqlOutbox.cs b/src/Paramore.Brighter.Outbox.MsSql/MsSqlOutbox.cs index 12d9617004..6b3e4e92d0 100644 --- a/src/Paramore.Brighter.Outbox.MsSql/MsSqlOutbox.cs +++ b/src/Paramore.Brighter.Outbox.MsSql/MsSqlOutbox.cs @@ -70,7 +70,7 @@ public MsSqlOutbox(IAmARelationalDatabaseConfiguration configuration) : this(con } protected override void WriteToStore( - IAmABoxTransactionProvider transactionProvider, + IAmABoxTransactionProvider transactionProvider, Func commandFunc, Action loggingAction) { @@ -86,8 +86,8 @@ protected override void WriteToStore( { try { - if (transactionProvider != null && connectionProvider.HasOpenTransaction) - command.Transaction = connectionProvider.GetTransaction(); + if (transactionProvider != null && transactionProvider.HasOpenTransaction) + command.Transaction = transactionProvider.GetTransaction(); command.ExecuteNonQuery(); } catch (SqlException sqlException) @@ -103,16 +103,16 @@ protected override void WriteToStore( } finally { - if (!connectionProvider.IsSharedConnection) - connection.Dispose(); - else if (!connectionProvider.HasOpenTransaction) + if (transactionProvider != null) + transactionProvider.Close(); + else connection.Close(); } } } protected override async Task WriteToStoreAsync( - IAmABoxTransactionProvider transactionProvider, + IAmABoxTransactionProvider transactionProvider, Func commandFunc, Action loggingAction, CancellationToken cancellationToken) @@ -130,8 +130,8 @@ protected override async Task WriteToStoreAsync( { try { - if (transactionProvider != null && connectionProvider.HasOpenTransaction) - command.Transaction = connectionProvider.GetTransaction(); + if (transactionProvider != null && transactionProvider.HasOpenTransaction) + command.Transaction = transactionProvider.GetTransaction(); await command.ExecuteNonQueryAsync(cancellationToken); } catch (SqlException sqlException) @@ -147,9 +147,9 @@ protected override async Task WriteToStoreAsync( } finally { - if (!connectionProvider.IsSharedConnection) - connection.Dispose(); - else if (!connectionProvider.HasOpenTransaction) + if (transactionProvider != null) + transactionProvider.Close(); + else connection.Close(); } } @@ -172,10 +172,7 @@ Func resultFunc } finally { - if (!_connectionProvider.IsSharedConnection) - connection.Dispose(); - else if (!_connectionProvider.HasOpenTransaction) - connection.Close(); + connection.Close(); } } } @@ -198,10 +195,7 @@ CancellationToken cancellationToken } finally { - if (!_connectionProvider.IsSharedConnection) - connection.Dispose(); - else if (!_connectionProvider.HasOpenTransaction) - connection.Close(); + connection.Close(); } } } diff --git a/src/Paramore.Brighter.Outbox.MsSql/ServiceCollectionExtensions.cs b/src/Paramore.Brighter.Outbox.MsSql/ServiceCollectionExtensions.cs index ff51489f6a..5c6e7ee571 100644 --- a/src/Paramore.Brighter.Outbox.MsSql/ServiceCollectionExtensions.cs +++ b/src/Paramore.Brighter.Outbox.MsSql/ServiceCollectionExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Data.Common; using System.Runtime.CompilerServices; using Microsoft.Extensions.DependencyInjection; using Paramore.Brighter.Extensions.DependencyInjection; @@ -24,17 +25,17 @@ public static class ServiceCollectionExtensions /// -- 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, RelationalDatabaseConfiguration configuration, Type connectionProvider, ServiceLifetime serviceLifetime = ServiceLifetime.Singleton, int outboxBulkChunkSize = 100) + this IBrighterBuilder brighterBuilder, RelationalDatabaseConfiguration configuration, Type connectionProvider, ServiceLifetime serviceLifetime = ServiceLifetime.Singleton) { + if (!typeof(IAmARelationalDbConnectionProvider).IsAssignableFrom(connectionProvider)) + throw new Exception($"Unable to register provider of type {connectionProvider.Name}. Class does not implement interface {nameof(IAmARelationalDbConnectionProvider)}."); + brighterBuilder.Services.AddSingleton(configuration); brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmARelationalDbConnectionProvider), connectionProvider, serviceLifetime)); - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmAnOutboxSync), BuildMsSqlOutbox, serviceLifetime)); - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmAnOutboxAsync), BuildMsSqlOutbox, 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; } @@ -42,18 +43,28 @@ public static IBrighterBuilder UseMsSqlOutbox( /// 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 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, + this IBrighterBuilder brighterBuilder, Type transactionProvider, ServiceLifetime serviceLifetime = ServiceLifetime.Scoped) { - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmABoxTransactionProvider), connectionProvider, serviceLifetime)); + if (transactionProvider is null) + throw new ArgumentNullException($"{nameof(transactionProvider)} cannot be null.", nameof(transactionProvider)); + if (!typeof(IAmABoxTransactionProvider).IsAssignableFrom(transactionProvider)) + throw new Exception($"Unable to register provider of type {transactionProvider.Name}. Class does not implement interface {nameof(IAmABoxTransactionProvider)}."); + + if (!typeof(IAmATransactionConnectionProvider).IsAssignableFrom(transactionProvider)) + throw new Exception($"Unable to register provider of type {transactionProvider.Name}. Class does not implement interface {nameof(IAmATransactionConnectionProvider)}."); + + brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmABoxTransactionProvider), transactionProvider, serviceLifetime)); + brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmATransactionConnectionProvider), transactionProvider, serviceLifetime)); + return brighterBuilder; } diff --git a/src/Paramore.Brighter.Outbox.MySql/MySqlOutbox.cs b/src/Paramore.Brighter.Outbox.MySql/MySqlOutbox.cs index bd62c58ca5..cb54b9eec7 100644 --- a/src/Paramore.Brighter.Outbox.MySql/MySqlOutbox.cs +++ b/src/Paramore.Brighter.Outbox.MySql/MySqlOutbox.cs @@ -63,7 +63,7 @@ public MySqlOutbox(IAmARelationalDatabaseConfiguration configuration) } protected override void WriteToStore( - IAmABoxTransactionProvider transactionProvider, + IAmABoxTransactionProvider transactionProvider, Func commandFunc, Action loggingAction ) @@ -80,8 +80,8 @@ Action loggingAction { try { - if (transactionProvider != null && connectionProvider.HasOpenTransaction) - command.Transaction = connectionProvider.GetTransaction(); + if (transactionProvider != null && transactionProvider.HasOpenTransaction) + command.Transaction = transactionProvider.GetTransaction(); command.ExecuteNonQuery(); } catch (MySqlException sqlException) @@ -97,16 +97,13 @@ Action loggingAction } finally { - if (!connectionProvider.IsSharedConnection) - connection.Dispose(); - else if (!connectionProvider.HasOpenTransaction) - connection.Close(); + transactionProvider?.Close(); } } } protected override async Task WriteToStoreAsync( - IAmABoxTransactionProvider transactionProvider, + IAmABoxTransactionProvider transactionProvider, Func commandFunc, Action loggingAction, CancellationToken cancellationToken @@ -125,8 +122,8 @@ CancellationToken cancellationToken { try { - if (transactionProvider != 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) @@ -142,10 +139,7 @@ CancellationToken cancellationToken } finally { - if (!connectionProvider.IsSharedConnection) - connection.Dispose(); - else if (!connectionProvider.HasOpenTransaction) - connection.Close(); + transactionProvider?.Close(); } } } @@ -167,10 +161,7 @@ Func resultFunc } finally { - if (!_connectionProvider.IsSharedConnection) - connection.Dispose(); - else if (!_connectionProvider.HasOpenTransaction) - connection.Close(); + connection.Close(); } } } @@ -192,10 +183,7 @@ protected override async Task ReadFromStoreAsync( } finally { - if (!_connectionProvider.IsSharedConnection) - connection.Dispose(); - else if (!_connectionProvider.HasOpenTransaction) - connection.Close(); + connection.Close(); } } } diff --git a/src/Paramore.Brighter.Outbox.MySql/ServiceCollectionExtensions.cs b/src/Paramore.Brighter.Outbox.MySql/ServiceCollectionExtensions.cs index 50b106fe54..5ec0e70807 100644 --- a/src/Paramore.Brighter.Outbox.MySql/ServiceCollectionExtensions.cs +++ b/src/Paramore.Brighter.Outbox.MySql/ServiceCollectionExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Data.Common; using Microsoft.Extensions.DependencyInjection; using Paramore.Brighter.Extensions.DependencyInjection; using Paramore.Brighter.MySql; @@ -28,8 +29,8 @@ public static IBrighterBuilder UseMySqlOutbox( brighterBuilder.Services.AddSingleton(configuration); brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmARelationalDbConnectionProvider), connectionProvider, serviceLifetime)); - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmAnOutboxSync), BuildMySqlOutboxOutbox, serviceLifetime)); - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmAnOutboxAsync), BuildMySqlOutboxOutbox, serviceLifetime)); + brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmAnOutboxSync), BuildMySqlOutboxOutbox, serviceLifetime)); + brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmAnOutboxAsync), BuildMySqlOutboxOutbox, serviceLifetime)); return brighterBuilder; } @@ -50,20 +51,17 @@ public static IBrighterBuilder UseMySqTransactionConnectionProvider( ServiceLifetime serviceLifetime = ServiceLifetime.Scoped ) { - if (brighterBuilder is null) - throw new ArgumentNullException($"{nameof(brighterBuilder)} cannot be null.", nameof(brighterBuilder)); - if (transactionProvider is null) throw new ArgumentNullException($"{nameof(transactionProvider)} cannot be null.", nameof(transactionProvider)); - if (!typeof(IAmABoxTransactionProvider).IsAssignableFrom(transactionProvider)) - throw new Exception($"Unable to register provider of type {transactionProvider.GetType().Name}. Class does not implement interface {nameof(IAmABoxTransactionProvider)}."); + if (!typeof(IAmABoxTransactionProvider).IsAssignableFrom(transactionProvider)) + throw new Exception($"Unable to register provider of type {transactionProvider.Name}. Class does not implement interface {nameof(IAmABoxTransactionProvider)}."); if (!typeof(IAmATransactionConnectionProvider).IsAssignableFrom(transactionProvider)) - throw new Exception($"Unable to register provider of type {transactionProvider.GetType().Name}. Class does not implement interface {nameof(IAmATransactionConnectionProvider)}."); + throw new Exception($"Unable to register provider of type {transactionProvider.Name}. Class does not implement interface {nameof(IAmATransactionConnectionProvider)}."); //register the specific interface - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmABoxTransactionProvider), transactionProvider, serviceLifetime)); + brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmABoxTransactionProvider), transactionProvider, serviceLifetime)); //register the combined interface just in case brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmATransactionConnectionProvider), transactionProvider, serviceLifetime)); diff --git a/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutbox.cs b/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutbox.cs index 208444318c..82b334d259 100644 --- a/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutbox.cs +++ b/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutbox.cs @@ -60,7 +60,7 @@ public PostgreSqlOutbox(IAmARelationalDatabaseConfiguration configuration) { } protected override void WriteToStore( - IAmABoxTransactionProvider transactionProvider, + IAmABoxTransactionProvider transactionProvider, Func commandFunc, Action loggingAction) { @@ -76,8 +76,8 @@ protected override void WriteToStore( { try { - if (transactionProvider != null && connectionProvider.HasOpenTransaction) - command.Transaction = connectionProvider.GetTransaction(); + if (transactionProvider != null && transactionProvider.HasOpenTransaction) + command.Transaction = transactionProvider.GetTransaction(); command.ExecuteNonQuery(); } catch (PostgresException sqlException) @@ -92,16 +92,13 @@ protected override void WriteToStore( } finally { - if (!connectionProvider.IsSharedConnection) - connection.Dispose(); - else if (!connectionProvider.HasOpenTransaction) - connection.Close(); + transactionProvider?.Close(); } } } protected override async Task WriteToStoreAsync( - IAmABoxTransactionProvider transactionProvider, + IAmABoxTransactionProvider transactionProvider, Func commandFunc, Action loggingAction, CancellationToken cancellationToken) @@ -119,8 +116,8 @@ protected override async Task WriteToStoreAsync( { try { - if (transactionProvider != null && connectionProvider.HasOpenTransaction) - command.Transaction = connectionProvider.GetTransaction(); + if (transactionProvider != null && transactionProvider.HasOpenTransaction) + command.Transaction = transactionProvider.GetTransaction(); await command.ExecuteNonQueryAsync(cancellationToken); } catch (PostgresException sqlException) @@ -136,9 +133,9 @@ protected override async Task WriteToStoreAsync( } finally { - if (!connectionProvider.IsSharedConnection) - connection.Dispose(); - else if (!connectionProvider.HasOpenTransaction) + if (transactionProvider != null) + transactionProvider.Close(); + else connection.Close(); } } @@ -161,10 +158,7 @@ Func resultFunc } finally { - if (!_connectionProvider.IsSharedConnection) - connection.Dispose(); - else if (!_connectionProvider.HasOpenTransaction) - connection.Close(); + connection.Close(); } } } @@ -187,10 +181,7 @@ CancellationToken cancellationToken } finally { - if (!_connectionProvider.IsSharedConnection) - connection.Dispose(); - else if (!_connectionProvider.HasOpenTransaction) - connection.Close(); + connection.Close(); } } } diff --git a/src/Paramore.Brighter.Outbox.PostgreSql/ServiceCollectionExtensions.cs b/src/Paramore.Brighter.Outbox.PostgreSql/ServiceCollectionExtensions.cs index c6c57a6af4..fb5c07ab46 100644 --- a/src/Paramore.Brighter.Outbox.PostgreSql/ServiceCollectionExtensions.cs +++ b/src/Paramore.Brighter.Outbox.PostgreSql/ServiceCollectionExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Data.Common; using Microsoft.Extensions.DependencyInjection; using Paramore.Brighter.Extensions.DependencyInjection; using Paramore.Brighter.PostgreSql; @@ -8,25 +9,14 @@ namespace Paramore.Brighter.Outbox.PostgreSql public static class ServiceCollectionExtensions { public static IBrighterBuilder UsePostgreSqlOutbox( - this IBrighterBuilder brighterBuilder, RelationalDatabaseConfiguration configuration, Type connectionProvider = null, ServiceLifetime serviceLifetime = ServiceLifetime.Singleton) + this IBrighterBuilder brighterBuilder, RelationalDatabaseConfiguration configuration, Type connectionProvider, 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)); - + if (!typeof(IAmARelationalDbConnectionProvider).IsAssignableFrom(connectionProvider)) + throw new Exception($"Unable to register provider of type {connectionProvider.Name}. Class does not implement interface {nameof(IAmARelationalDbConnectionProvider)}."); + brighterBuilder.Services.AddSingleton(configuration); - - if (connectionProvider is object) - { - if (!typeof(IAmARelationalDbConnectionProvider).IsAssignableFrom(connectionProvider)) - throw new Exception($"Unable to register provider of type {connectionProvider.GetType().Name}. Class does not implement interface {nameof(IAmARelationalDbConnectionProvider)}."); - - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmARelationalDbConnectionProvider), connectionProvider, serviceLifetime)); - } - - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmAnOutboxSync), BuildPostgreSqlOutboxSync, serviceLifetime)); + brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmARelationalDbConnectionProvider), connectionProvider, serviceLifetime)); + brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmAnOutboxSync), BuildPostgreSqlOutboxSync, serviceLifetime)); return brighterBuilder; } @@ -46,22 +36,17 @@ public static IBrighterBuilder UsePostgreSqlTransactionConnectionProvider( Type transactionProvider, ServiceLifetime serviceLifetime = ServiceLifetime.Scoped) { - if (brighterBuilder is null) - throw new ArgumentNullException($"{nameof(brighterBuilder)} cannot be null.", nameof(brighterBuilder)); - if (transactionProvider is null) throw new ArgumentNullException($"{nameof(transactionProvider)} cannot be null.", nameof(transactionProvider)); - if (!typeof(IAmABoxTransactionProvider).IsAssignableFrom(transactionProvider)) - throw new Exception($"Unable to register provider of type {transactionProvider.GetType().Name}. Class does not implement interface {nameof(IAmABoxTransactionProvider)}."); + if (!typeof(IAmABoxTransactionProvider).IsAssignableFrom(transactionProvider)) + throw new Exception($"Unable to register provider of type {transactionProvider.Name}. Class does not implement interface {nameof(IAmABoxTransactionProvider)}."); if (!typeof(IAmATransactionConnectionProvider).IsAssignableFrom(transactionProvider)) - throw new Exception($"Unable to register provider of type {transactionProvider.GetType().Name}. Class does not implement interface {nameof(IAmATransactionConnectionProvider)}."); + throw new Exception($"Unable to register provider of type {transactionProvider.Name}. Class does not implement interface {nameof(IAmATransactionConnectionProvider)}."); + - //register the specific interface - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmABoxTransactionProvider), transactionProvider, serviceLifetime)); - - //register the combined interface just in case + brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmABoxTransactionProvider), transactionProvider, serviceLifetime)); brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmATransactionConnectionProvider), transactionProvider, serviceLifetime)); return brighterBuilder; diff --git a/src/Paramore.Brighter.Outbox.Sqlite/ServiceCollectionExtensions.cs b/src/Paramore.Brighter.Outbox.Sqlite/ServiceCollectionExtensions.cs index d77921bfb2..a6e15bc625 100644 --- a/src/Paramore.Brighter.Outbox.Sqlite/ServiceCollectionExtensions.cs +++ b/src/Paramore.Brighter.Outbox.Sqlite/ServiceCollectionExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Data.Common; using Microsoft.Extensions.DependencyInjection; using Paramore.Brighter.Extensions.DependencyInjection; using Paramore.Brighter.Sqlite; @@ -25,11 +26,14 @@ public static class ServiceCollectionExtensions public static IBrighterBuilder UseSqliteOutbox( this IBrighterBuilder brighterBuilder, RelationalDatabaseConfiguration configuration, Type connectionProvider, ServiceLifetime serviceLifetime = ServiceLifetime.Singleton) { + if (!typeof(IAmARelationalDbConnectionProvider).IsAssignableFrom(connectionProvider)) + throw new Exception($"Unable to register provider of type {connectionProvider.Name}. Class does not implement interface {nameof(IAmARelationalDbConnectionProvider)}."); + brighterBuilder.Services.AddSingleton(configuration); brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmARelationalDbConnectionProvider), connectionProvider, serviceLifetime)); - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmAnOutboxSync), BuildSqliteOutbox, serviceLifetime)); - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmAnOutboxAsync), BuildSqliteOutbox, serviceLifetime)); + brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmAnOutboxSync), BuildSqliteOutbox, serviceLifetime)); + brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmAnOutboxAsync), BuildSqliteOutbox, serviceLifetime)); return brighterBuilder; } @@ -49,20 +53,17 @@ public static IBrighterBuilder UseSqliteTransactionConnectionProvider( Type transactionProvider, ServiceLifetime serviceLifetime = ServiceLifetime.Scoped) { - if (brighterBuilder is null) - throw new ArgumentNullException($"{nameof(brighterBuilder)} cannot be null.", nameof(brighterBuilder)); - if (transactionProvider is null) throw new ArgumentNullException($"{nameof(transactionProvider)} cannot be null.", nameof(transactionProvider)); - if (!typeof(IAmABoxTransactionProvider).IsAssignableFrom(transactionProvider)) - throw new Exception($"Unable to register provider of type {transactionProvider.GetType().Name}. Class does not implement interface {nameof(IAmABoxTransactionProvider)}."); + if (!typeof(IAmABoxTransactionProvider).IsAssignableFrom(transactionProvider)) + throw new Exception($"Unable to register provider of type {transactionProvider.Name}. Class does not implement interface {nameof(IAmABoxTransactionProvider)}."); if (!typeof(IAmATransactionConnectionProvider).IsAssignableFrom(transactionProvider)) - throw new Exception($"Unable to register provider of type {transactionProvider.GetType().Name}. Class does not implement interface {nameof(IAmATransactionConnectionProvider)}."); + throw new Exception($"Unable to register provider of type {transactionProvider.Name}. Class does not implement interface {nameof(IAmATransactionConnectionProvider)}."); //register the specific interface - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmABoxTransactionProvider), transactionProvider, serviceLifetime)); + brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmABoxTransactionProvider), transactionProvider, serviceLifetime)); //register the combined interface just in case brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmATransactionConnectionProvider), transactionProvider, serviceLifetime)); diff --git a/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutbox.cs b/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutbox.cs index 5f9ef57fb0..1fe9c232d6 100644 --- a/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutbox.cs +++ b/src/Paramore.Brighter.Outbox.Sqlite/SqliteOutbox.cs @@ -72,7 +72,7 @@ public SqliteOutbox(IAmARelationalDatabaseConfiguration configuration) } protected override void WriteToStore( - IAmABoxTransactionProvider transactionProvider, + IAmABoxTransactionProvider transactionProvider, Func commandFunc, Action loggingAction ) @@ -89,8 +89,8 @@ Action loggingAction { try { - if (transactionProvider != null && connectionProvider.HasOpenTransaction) - command.Transaction = connectionProvider.GetTransaction(); + if (transactionProvider != null && transactionProvider.HasOpenTransaction) + command.Transaction = transactionProvider.GetTransaction(); command.ExecuteNonQuery(); } catch (SqliteException sqlException) @@ -105,13 +105,16 @@ Action loggingAction } finally { - connection.Close(); + if (transactionProvider != null) + transactionProvider.Close(); + else + connection.Close(); } } } protected override async Task WriteToStoreAsync( - IAmABoxTransactionProvider transactionProvider, + IAmABoxTransactionProvider transactionProvider, Func commandFunc, Action loggingAction, CancellationToken cancellationToken) @@ -128,8 +131,8 @@ protected override async Task WriteToStoreAsync( { try { - if (transactionProvider != 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) @@ -144,10 +147,10 @@ protected override async Task WriteToStoreAsync( } finally { - if (!connectionProvider.IsSharedConnection) - connection.Dispose(); - else if (!connectionProvider.HasOpenTransaction) - await connection.CloseAsync(); + if (transactionProvider != null) + transactionProvider.Close(); + else + connection.Close(); } } } @@ -169,17 +172,15 @@ Func resultFunc } 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) + Func> resultFunc, + CancellationToken cancellationToken) { var connection = await _connectionProvider.GetConnectionAsync(cancellationToken); @@ -193,10 +194,7 @@ protected override async Task ReadFromStoreAsync( } finally { - if (!_connectionProvider.IsSharedConnection) - connection.Dispose(); - else if (!_connectionProvider.HasOpenTransaction) - connection.Close(); + await connection.CloseAsync(); } } } diff --git a/src/Paramore.Brighter.ServiceActivator.Extensions.DependencyInjection/ServiceCollectionExtensions.cs b/src/Paramore.Brighter.ServiceActivator.Extensions.DependencyInjection/ServiceCollectionExtensions.cs index ab13d73e7e..51aedab7a7 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,21 +26,27 @@ public static class ServiceActivatorServiceCollectionExtensions public static IBrighterBuilder AddServiceActivator( this IServiceCollection services, Action configure = null) - { - if (services == null) - throw new ArgumentNullException(nameof(services)); + { + return AddServiceActivatorWithTransactionalMessaging(services, configure); + } - var options = new ServiceActivatorOptions(); - configure?.Invoke(options); - services.TryAddSingleton(options); - services.TryAddSingleton(options); + private static IBrighterBuilder AddServiceActivatorWithTransactionalMessaging(IServiceCollection services, + Action configure) + { + if (services == null) + throw new ArgumentNullException(nameof(services)); - services.TryAddSingleton(BuildDispatcher); + var options = new ServiceActivatorOptions(); + configure?.Invoke(options); + services.TryAddSingleton(options); + services.TryAddSingleton(options); - return ServiceCollectionExtensions.BrighterHandlerBuilder(services, options); - } + services.TryAddSingleton(BuildDispatcher); + + return ServiceCollectionExtensions.BrighterHandlerBuilder(services, options); + } - private static Dispatcher BuildDispatcher(IServiceProvider serviceProvider) + private static Dispatcher BuildDispatcher(IServiceProvider serviceProvider) { var loggerFactory = serviceProvider.GetService(); ApplicationLogging.LoggerFactory = loggerFactory; diff --git a/src/Paramore.Brighter.ServiceActivator/ControlBusReceiverBuilder.cs b/src/Paramore.Brighter.ServiceActivator/ControlBusReceiverBuilder.cs index 2c3b0d0b66..bfdb73ab3e 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; @@ -183,9 +184,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, IAmABoxTransactionProvider transactionProvider = null) + public void Add(Message message, int outBoxTimeout = -1, IAmABoxTransactionProvider transactionProvider = null) { //discard message } diff --git a/src/Paramore.Brighter/CommandProcessor.cs b/src/Paramore.Brighter/CommandProcessor.cs index 64b3fcc40e..d57587c07b 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 IAmABoxTransactionProvider _transactionProvider; private readonly IAmAFeatureSwitchRegistry _featureSwitchRegistry; private readonly IEnumerable _replySubscriptions; private readonly TransformPipelineBuilder _transformPipelineBuilder; @@ -97,8 +96,10 @@ 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(); /// @@ -144,36 +145,26 @@ public CommandProcessor( /// 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(IAmARequestContextFactory requestContextFactory, IPolicyRegistry policyRegistry, IAmAMessageMapperRegistry mapperRegistry, - IAmAnOutbox outBox, - IAmAProducerRegistry producerRegistry, - int outboxTimeout = 300, + IAmAnExternalBusService bus, IAmAFeatureSwitchRegistry featureSwitchRegistry = null, InboxConfiguration inboxConfiguration = null, - IAmABoxTransactionProvider transactionProvider = null, - int outboxBulkChunkSize = 100, IAmAMessageTransformerFactory messageTransformerFactory = null) { _requestContextFactory = requestContextFactory; _policyRegistry = policyRegistry; _featureSwitchRegistry = featureSwitchRegistry; _inboxConfiguration = inboxConfiguration; - _transactionProvider = transactionProvider; _transformPipelineBuilder = new TransformPipelineBuilder(mapperRegistry, messageTransformerFactory); - InitExtServiceBus(policyRegistry, outBox, outboxTimeout, producerRegistry, outboxBulkChunkSize); - - ConfigureCallbacks(producerRegistry); + InitExtServiceBus(bus); } /// @@ -185,43 +176,33 @@ public CommandProcessor(IAmARequestContextFactory requestContextFactory, /// 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 external service bus that we want to send messages over /// The Subscriptions for creating the reply queues - /// How long should we wait to write to the outbox /// 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, IAmAHandlerFactory handlerFactory, IAmARequestContextFactory requestContextFactory, IPolicyRegistry policyRegistry, IAmAMessageMapperRegistry mapperRegistry, - IAmAnOutbox outBox, - IAmAProducerRegistry producerRegistry, + IAmAnExternalBusService bus, IEnumerable replySubscriptions, - int outboxTimeout = 300, IAmAFeatureSwitchRegistry featureSwitchRegistry = null, IAmAChannelFactory responseChannelFactory = null, InboxConfiguration inboxConfiguration = null, - IAmABoxTransactionProvider transactionProvider = null, - int outboxBulkChunkSize = 100, IAmAMessageTransformerFactory messageTransformerFactory = null) : this(subscriberRegistry, handlerFactory, requestContextFactory, policyRegistry) { _featureSwitchRegistry = featureSwitchRegistry; _responseChannelFactory = responseChannelFactory; _inboxConfiguration = inboxConfiguration; - _transactionProvider = transactionProvider; _replySubscriptions = replySubscriptions; _transformPipelineBuilder = new TransformPipelineBuilder(mapperRegistry, messageTransformerFactory); - InitExtServiceBus(policyRegistry, outBox, outboxTimeout, producerRegistry, outboxBulkChunkSize); - - ConfigureCallbacks(producerRegistry); + InitExtServiceBus(bus); + } /// @@ -234,35 +215,25 @@ public CommandProcessor(IAmASubscriberRegistry subscriberRegistry, /// 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, IAmARequestContextFactory requestContextFactory, IPolicyRegistry policyRegistry, IAmAMessageMapperRegistry mapperRegistry, - IAmAnOutbox outBox, - IAmAProducerRegistry producerRegistry, - int outboxTimeout = 300, + IAmAnExternalBusService bus, IAmAFeatureSwitchRegistry featureSwitchRegistry = null, InboxConfiguration inboxConfiguration = null, - IAmABoxTransactionProvider transactionProvider = null, - int outboxBulkChunkSize = 100, IAmAMessageTransformerFactory messageTransformerFactory = null) : this(subscriberRegistry, handlerFactory, requestContextFactory, policyRegistry, featureSwitchRegistry) { _inboxConfiguration = inboxConfiguration; - _transactionProvider = transactionProvider; _transformPipelineBuilder = new TransformPipelineBuilder(mapperRegistry, messageTransformerFactory); - InitExtServiceBus(policyRegistry, outBox, outboxTimeout, producerRegistry, outboxBulkChunkSize); - - ConfigureCallbacks(producerRegistry); + InitExtServiceBus(bus); } /// @@ -482,6 +453,24 @@ 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 + /// Paramore.Brighter.ServiceActivator provides an endpoint for use in a windows service that reads from a queue + /// and then Sends or Publishes the message to a within that service. The decision to or is based on the + /// mapper. Your mapper can map to a with either a , which results in a or a + /// which results in a + /// 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(TRequest request) where TRequest : class, IRequest + { + Post(request, null); + } /// /// Posts the specified request. The message is placed on a task queue and into a outbox for reposting in the event of failure. @@ -493,13 +482,41 @@ 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 transaction provider + /// The type of request + /// The type of transaction used by the Outbox /// - public void Post(T request) where T : class, IRequest + public void Post(TRequest request, IAmABoxTransactionProvider transactionProvider) where TRequest : class, IRequest { - ClearOutbox(DepositPost(request, null)); + ClearOutbox(DepositPost(request, transactionProvider)); } + + /// + /// Posts the specified request with async/await support. 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 + /// Paramore.Brighter.ServiceActivator provides an endpoint for use in a windows service that reads from a queue + /// and then Sends or Publishes the message to a within that service. The decision to or is based on the + /// mapper. Your mapper can map to a with either a , which results in a or a + /// which results in a + /// 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( + TRequest request, + bool continueOnCapturedContext = false, + CancellationToken cancellationToken = default + ) + where TRequest : class, IRequest + { + await PostAsync(request, null, continueOnCapturedContext, cancellationToken); + } /// /// Posts the specified request with async/await support. The message is placed on a task queue and into a outbox for reposting in the event of failure. @@ -511,20 +528,26 @@ 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. + /// The transaction provider used to share a transaction with an Outbox /// 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 + /// The type of a transaction i.e. DbTransaction etc /// /// awaitable . - public async Task PostAsync(T request, bool continueOnCapturedContext = false, - CancellationToken cancellationToken = default) - where T : class, IRequest + public async Task PostAsync( + TRequest request, + IAmABoxTransactionProvider transactionProvider = null, + bool continueOnCapturedContext = false, + CancellationToken cancellationToken = default + ) + where TRequest : class, IRequest { - var messageId = await DepositPostAsync(request, null, continueOnCapturedContext, cancellationToken); + var messageId = await DepositPostAsync(request, transactionProvider, 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,48 +556,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, _transactionProvider); + 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, _transactionProvider); - } - - private Guid DepositPost(T request, IAmABoxTransactionProvider provider) 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, provider); + bus.AddToOutbox(request, message, transactionProvider); return message.Id; } + + /// + /// 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 + { + return DepositPost(requests, null); + } - private Guid[] DepositPost(IEnumerable requests, IAmABoxTransactionProvider provider) - 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 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 { - if (!_bus.HasBulkOutbox()) + 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(); @@ -585,14 +644,14 @@ private Guid[] DepositPost(IEnumerable requests, IAmABoxTransactionProvide s_logger.LogInformation("Save requests: {RequestType} {AmountOfMessages}", batch.Key, messages.Count()); - _bus.AddToOutbox(messages, provider); + 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 @@ -603,12 +662,18 @@ private Guid[] DepositPost(IEnumerable requests, IAmABoxTransactionProvide /// 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, _transactionProvider, continueOnCapturedContext, + return await DepositPostAsync( + request, + null, + continueOnCapturedContext, cancellationToken); } @@ -619,51 +684,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 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 transaction used by the Outbox + /// + public async Task DepositPostAsync( + TRequest request, + IAmABoxTransactionProvider transactionProvider, + bool continueOnCapturedContext = false, + 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()) + throw new InvalidOperationException("No async outbox defined."); + + var message = await _transformPipelineBuilder.BuildWrapPipeline().WrapAsync(request, cancellationToken); + + AddTelemetryToMessage(message); + + 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 + /// The type of the request /// - public Task DepositPostAsync( - IEnumerable requests, + public async Task DepositPostAsync( + IEnumerable requests, bool continueOnCapturedContext = false, CancellationToken cancellationToken = default - ) where T : class, IRequest + ) where TRequest : class, IRequest { - return DepositPostAsync(requests, _transactionProvider, continueOnCapturedContext, cancellationToken); + return await DepositPostAsync( + requests, + null, + continueOnCapturedContext, + cancellationToken); } - private async Task DepositPostAsync( - T request, - IAmABoxTransactionProvider provider, + /// + /// 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 T : class, IRequest + ) where TRequest : class, IRequest { - s_logger.LogInformation("Save request: {RequestType} {Id}", request.GetType(), request.Id); + var bus = ((ExternalBusServices)_bus); + + if (!bus.HasAsyncBulkOutbox()) + throw new InvalidOperationException("No bulk async outbox defined."); - if (!_bus.HasAsyncOutbox()) - throw new InvalidOperationException("No async outbox defined."); + var successfullySentMessage = new List(); + + foreach (var batch in SplitRequestBatchIntoTypes(requests)) + { + var messages = await MapMessagesAsync(batch.Key, batch.ToArray(), cancellationToken); - var message = await _transformPipelineBuilder.BuildWrapPipeline().WrapAsync(request, cancellationToken); + s_logger.LogInformation("Save requests: {RequestType} {AmountOfMessages}", batch.Key, messages.Count()); - AddTelemetryToMessage(message); + await bus.AddToOutboxAsync(messages, transactionProvider, continueOnCapturedContext, cancellationToken); - await _bus.AddToOutboxAsync(request, continueOnCapturedContext, cancellationToken, message, - provider); + successfullySentMessage.AddRange(messages.Select(m => m.Id)); + } - return message.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); } /// @@ -683,7 +809,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, @@ -783,7 +911,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 @@ -803,6 +931,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 { @@ -816,116 +955,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, - IAmABoxTransactionProvider provider, - 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, provider); - - 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 => @@ -951,19 +981,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 @@ -980,5 +1029,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 65e64436a3..e4155b1509 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; @@ -70,7 +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; @@ -80,12 +80,10 @@ public class CommandProcessorBuilder : INeedAHandlers, INeedPolicy, INeedMessagi private IPolicyRegistry _policyRegistry; private IAmAFeatureSwitchRegistry _featureSwitchRegistry; private IAmAChannelFactory _responseChannelFactory; - private int _outboxWriteTimeout; private bool _useExternalBus = false; private bool _useRequestReplyQueues = false; private IEnumerable _replySubscriptions; - private IAmABoxTransactionProvider _overridingBoxTransactionProvider = null; - private int _outboxBulkChunkSize; + private IAmAnExternalBusService _bus; private CommandProcessorBuilder() { @@ -164,16 +162,25 @@ public INeedMessaging DefaultPolicy() /// The Outbox. /// /// INeedARequestContext. - public INeedARequestContext ExternalBus(ExternalBusConfiguration configuration, IAmAnOutbox outbox, IAmABoxTransactionProvider boxTransactionProvider = null) + public INeedARequestContext ExternalBus( + ExternalBusConfiguration configuration, + IAmAnOutbox outbox + ) where TMessage : Message { _useExternalBus = true; _producers = configuration.ProducerRegistry; - _outbox = outbox; - _overridingBoxTransactionProvider = boxTransactionProvider; _messageMapperRegistry = configuration.MessageMapperRegistry; - _outboxWriteTimeout = configuration.OutboxWriteTimeout; - _outboxBulkChunkSize = configuration.OutboxBulkChunkSize; + _responseChannelFactory = configuration.ResponseChannelFactory; _transformerFactory = configuration.TransformerFactory; + + var bus = new ExternalBusServices( + configuration.ProducerRegistry, + _policyRegistry, + outbox, + configuration.OutboxBulkChunkSize, + configuration.OutboxWriteTimeout); + _bus = bus; + return this; } @@ -193,16 +200,26 @@ public INeedARequestContext NoExternalBus() /// The outbox /// Subscriptions for creating reply queues /// - public INeedARequestContext ExternalRPC(ExternalBusConfiguration configuration, IAmAnOutbox outbox, IEnumerable subscriptions) + public INeedARequestContext ExternalRPC( + ExternalBusConfiguration configuration, + IAmAnOutbox outbox, + IEnumerable subscriptions + ) where TMessage : Message { _useRequestReplyQueues = true; _replySubscriptions = subscriptions; _producers = configuration.ProducerRegistry; _messageMapperRegistry = configuration.MessageMapperRegistry; - _outboxWriteTimeout = configuration.OutboxWriteTimeout; _responseChannelFactory = configuration.ResponseChannelFactory; - _outbox = outbox; _transformerFactory = configuration.TransformerFactory; + + var bus = new ExternalBusServices( + configuration.ProducerRegistry, + _policyRegistry, + outbox, + configuration.OutboxBulkChunkSize, + configuration.OutboxWriteTimeout); + _bus = bus; return this; } @@ -243,12 +260,8 @@ public CommandProcessor Build() requestContextFactory: _requestContextFactory, policyRegistry: _policyRegistry, mapperRegistry: _messageMapperRegistry, - outBox: _outbox, - producerRegistry: _producers, - outboxTimeout: _outboxWriteTimeout, + bus: _bus, featureSwitchRegistry: _featureSwitchRegistry, - transactionProvider: _overridingBoxTransactionProvider, - outboxBulkChunkSize: _outboxBulkChunkSize, messageTransformerFactory: _transformerFactory ); } @@ -260,10 +273,9 @@ public CommandProcessor Build() requestContextFactory: _requestContextFactory, policyRegistry: _policyRegistry, mapperRegistry: _messageMapperRegistry, - outBox: _outbox, - producerRegistry: _producers, + bus: _bus, replySubscriptions: _replySubscriptions, - responseChannelFactory: _responseChannelFactory, transactionProvider: _overridingBoxTransactionProvider); + responseChannelFactory: _responseChannelFactory); } else { @@ -323,9 +335,12 @@ public interface INeedMessaging /// /// The configuration. /// The outbox. - /// The transaction provider to use when adding messages to the bus /// INeedARequestContext. - INeedARequestContext ExternalBus(ExternalBusConfiguration configuration, IAmAnOutbox outbox, IAmABoxTransactionProvider boxTransactionProvider = null); + INeedARequestContext ExternalBus( + ExternalBusConfiguration configuration, + IAmAnOutbox outbox + ) + where T : Message; /// /// We don't send messages out of process /// @@ -338,7 +353,11 @@ public interface INeedMessaging /// /// The outbox /// Subscriptions for creating Reply queues - INeedARequestContext ExternalRPC(ExternalBusConfiguration externalBusConfiguration, IAmAnOutbox outboxSync, IEnumerable subscriptions); + INeedARequestContext ExternalRPC( + ExternalBusConfiguration externalBusConfiguration, + IAmAnOutbox outboxSync, + IEnumerable subscriptions + ) where T: Message; } /// 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..225c7c99b1 100644 --- a/src/Paramore.Brighter/ControlBusSenderFactory.cs +++ b/src/Paramore.Brighter/ControlBusSenderFactory.cs @@ -40,7 +40,8 @@ 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(); diff --git a/src/Paramore.Brighter/ExternalBusServices.cs b/src/Paramore.Brighter/ExternalBusServices.cs index 7612a1c496..e38cb7ab9f 100644 --- a/src/Paramore.Brighter/ExternalBusServices.cs +++ b/src/Paramore.Brighter/ExternalBusServices.cs @@ -15,13 +15,14 @@ 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 IAmAnOutboxSync OutBox { get; set; } + internal IAmAnOutboxAsync AsyncOutbox { get; set; } internal int OutboxTimeout { get; set; } = 300; @@ -29,22 +30,51 @@ internal class ExternalBusServices : IDisposable internal IAmAProducerRegistry ProducerRegistry { get; set; } - private static readonly SemaphoreSlim _clearSemaphoreToken = new SemaphoreSlim(1, 1); - private static readonly SemaphoreSlim _backgroundClearSemaphoreToken = new SemaphoreSlim(1, 1); + 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 _checkOutstandingSemaphoreToken = new SemaphoreSlim(1, 1); - + 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; + } + /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// @@ -62,40 +92,60 @@ protected virtual void Dispose(bool disposing) if (disposing && ProducerRegistry != null) ProducerRegistry.CloseAll(); _disposed = true; - } - internal async Task AddToOutboxAsync( - T request, bool continueOnCapturedContext, - - CancellationToken cancellationToken, - Message message, - IAmABoxTransactionProvider overridingAmATransactionProvider = 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, overridingAmATransactionProvider).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, - IAmABoxTransactionProvider overridingTransactionProvider = 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)) @@ -116,27 +166,42 @@ await box.AddAsync(chunk, OutboxTimeout, ct, overridingTransactionProvider) { throw new InvalidOperationException($"{AsyncOutbox.GetType()} does not implement IAmABulkOutboxAsync"); } - } - - internal void AddToOutbox(T request, Message message, IAmABoxTransactionProvider overridingAmATransactionProvider = 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, overridingAmATransactionProvider); }); + + 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, IAmABoxTransactionProvider overridingTransactionProvider = 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)) @@ -154,57 +219,34 @@ internal void AddToOutbox(IEnumerable messages, IAmABoxTransactionProvi } } - 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) @@ -213,27 +255,34 @@ internal void ClearOutbox(params Guid[] posts) 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) @@ -242,62 +291,246 @@ internal async Task ClearOutboxAsync( 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); @@ -312,8 +545,8 @@ private async Task BackgroundDispatchUsingSync(int amountToClear, int minimumAge finally { span?.Dispose(); - _clearSemaphoreToken.Release(); - _backgroundClearSemaphoreToken.Release(); + s_clearSemaphoreToken.Release(); + s_backgroundClearSemaphoreToken.Release(); } CheckOutstandingMessages(); @@ -325,16 +558,16 @@ 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); span?.AddEvent(new ActivityEvent(GETMESSAGESFROMOUTBOX)); @@ -362,8 +595,8 @@ private async Task BackgroundDispatchUsingAsync(int amountToClear, int minimumAg finally { span?.Dispose(); - _clearSemaphoreToken.Release(); - _backgroundClearSemaphoreToken.Release(); + s_clearSemaphoreToken.Release(); + s_backgroundClearSemaphoreToken.Release(); } CheckOutstandingMessages(); @@ -381,8 +614,12 @@ 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); @@ -404,15 +641,20 @@ private void Dispatch(IEnumerable posts) 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()); - + 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) @@ -429,7 +671,6 @@ await producerAsync.SendAsync(message).ConfigureAwait(continueOnCapturedContext) } else { - var sent = await RetryAsync( async ct => await producerAsync.SendAsync(message).ConfigureAwait(continueOnCapturedContext), @@ -438,7 +679,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); } } @@ -446,7 +689,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; @@ -460,9 +703,13 @@ private async Task BulkDispatchAsync(IEnumerable posts, CancellationTok 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)) @@ -470,7 +717,8 @@ private async Task BulkDispatchAsync(IEnumerable posts, CancellationTok if (!(producer is ISupportPublishConfirmation)) { await RetryAsync(async ct => await AsyncOutbox.MarkDispatchedAsync(successfulMessage, - DateTime.UtcNow, cancellationToken: cancellationToken), cancellationToken: cancellationToken); + DateTime.UtcNow, cancellationToken: cancellationToken), + cancellationToken: cancellationToken); } } } @@ -481,71 +729,10 @@ 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 @@ -556,45 +743,28 @@ private void OutstandingMessagesCheck() _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(); + s_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; - } - - return true; - } - private async Task RetryAsync(Func send, bool continueOnCapturedContext = false, CancellationToken cancellationToken = default) { @@ -606,7 +776,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(); } @@ -615,14 +785,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/IAmABoxTransactionProvider.cs b/src/Paramore.Brighter/IAmABoxTransactionProvider.cs index 870dde2447..82405e2701 100644 --- a/src/Paramore.Brighter/IAmABoxTransactionProvider.cs +++ b/src/Paramore.Brighter/IAmABoxTransactionProvider.cs @@ -1,7 +1,58 @@ -namespace Paramore.Brighter +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 { } + public interface IAmABoxTransactionProvider + { + /// + /// Close any open connection or transaction + /// + void Close(); + + /// + /// Commit any transaction that we are managing + /// + void Commit(); + + /// + /// 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 9806856c00..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. @@ -51,7 +53,7 @@ public interface IAmABulkOutboxAsync : IAmAnOutboxAsync where T : Messa Task AddAsync( IEnumerable messages, int outBoxTimeout = -1, CancellationToken cancellationToken = default, - IAmABoxTransactionProvider transactionProvider = null + IAmABoxTransactionProvider transactionProvider = null ); } } diff --git a/src/Paramore.Brighter/IAmABulkOutboxSync.cs b/src/Paramore.Brighter/IAmABulkOutboxSync.cs index dc2087ce84..71da595793 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 @@ -37,7 +38,7 @@ namespace Paramore.Brighter /// /// [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. @@ -45,6 +46,6 @@ public interface IAmABulkOutboxSync : IAmAnOutboxSync where T : 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, IAmABoxTransactionProvider transactionProvider = null); + 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..58648bbc6a 100644 --- a/src/Paramore.Brighter/IAmACommandProcessor.cs +++ b/src/Paramore.Brighter/IAmACommandProcessor.cs @@ -42,53 +42,103 @@ 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. + /// + /// The type of the request + /// The type of any transaction used by the request + /// The request. + /// If you wish to use an outbox with this request, the transaction provider for that outbox + void Post( + TRequest request, + IAmABoxTransactionProvider transactionProvider + ) 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; + + /// + /// Posts the specified request with async/await support. + /// + /// The type of the request + /// GThe type of transaction used by any outbox + /// The request. + /// If you wish to use an outbox with this request, the transaction provider for that outbox + /// 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( + TRequest request, + IAmABoxTransactionProvider transactionProvider = null, + 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 +148,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 +177,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 +213,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, + 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(T request, bool continueOnCapturedContext = false, CancellationToken cancellationToken = default) where T : class, IRequest; + 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 +253,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 +286,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..50b0f1ad19 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/IAmARelationalDbConnectionProvider.cs b/src/Paramore.Brighter/IAmARelationalDbConnectionProvider.cs index 98889dfbc3..1a801dc8ee 100644 --- a/src/Paramore.Brighter/IAmARelationalDbConnectionProvider.cs +++ b/src/Paramore.Brighter/IAmARelationalDbConnectionProvider.cs @@ -10,16 +10,6 @@ namespace Paramore.Brighter /// public interface IAmARelationalDbConnectionProvider { - /// - /// Close any open connection or transaction - /// - void Close(); - - /// - /// Commit any transaction that we are managing - /// - void Commit(); - /// /// Commit any transaction that we are managing /// @@ -41,38 +31,9 @@ public interface IAmARelationalDbConnectionProvider /// A database connection Task GetConnectionAsync(CancellationToken cancellationToken); - /// - /// 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 - DbTransaction 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/IAmATransactionConnectionProvider.cs b/src/Paramore.Brighter/IAmATransactionConnectionProvider.cs index 9346b7f6fe..b0ac47934e 100644 --- a/src/Paramore.Brighter/IAmATransactionConnectionProvider.cs +++ b/src/Paramore.Brighter/IAmATransactionConnectionProvider.cs @@ -1,4 +1,6 @@ -namespace Paramore.Brighter +using System.Data.Common; + +namespace Paramore.Brighter { - public interface IAmATransactionConnectionProvider : IAmARelationalDbConnectionProvider, IAmABoxTransactionProvider { } + 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..9716554c71 100644 --- a/src/Paramore.Brighter/IAmAnOutbox.cs +++ b/src/Paramore.Brighter/IAmAnOutbox.cs @@ -6,8 +6,9 @@ /// 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 + /// The type of the message + /// The type of the database transaction + public interface IAmAnOutbox where TMessage : Message { } diff --git a/src/Paramore.Brighter/IAmAnOutboxAsync.cs b/src/Paramore.Brighter/IAmAnOutboxAsync.cs index 6bbdce04bf..624443e291 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 /// - /// - 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. @@ -55,8 +57,11 @@ public interface IAmAnOutboxAsync : IAmAnOutbox where T : Message /// Allows the sender to cancel the request pipeline. Optional /// The Connection Provider to use for this call /// . - Task AddAsync(T message, int outBoxTimeout = -1, CancellationToken cancellationToken = default, - IAmABoxTransactionProvider transactionProvider = 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 df3612e77a..6c80cad8ee 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,8 +34,9 @@ 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. @@ -42,7 +44,7 @@ public interface IAmAnOutboxSync : IAmAnOutbox where T : 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, IAmABoxTransactionProvider transactionProvider = null); + 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 7cd5da5a8d..9ea36f0891 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 { /// @@ -99,7 +100,7 @@ public class InMemoryOutbox : InMemoryBox, IAmABulkOutboxSync /// /// This is not used for the In Memory Outbox. - public void Add(Message message, int outBoxTimeout = -1, IAmABoxTransactionProvider transactionProvider = null) + public void Add(Message message, int outBoxTimeout = -1, IAmABoxTransactionProvider transactionProvider = null) { ClearExpiredMessages(); EnforceCapacityLimit(); @@ -120,7 +121,11 @@ public void Add(Message message, int outBoxTimeout = -1, IAmABoxTransactionProvi /// /// /// This is not used for the In Memory Outbox. - public void Add(IEnumerable messages, int outBoxTimeout = -1, IAmABoxTransactionProvider transactionProvider = null) + public void Add( + IEnumerable messages, + int outBoxTimeout = -1, + IAmABoxTransactionProvider transactionProvider = null + ) { ClearExpiredMessages(); EnforceCapacityLimit(); @@ -139,10 +144,11 @@ public void Add(IEnumerable messages, int outBoxTimeout = -1, IAmABoxTr /// /// This is not used for the In Memory Outbox. /// - public Task AddAsync(Message message, + public Task AddAsync( + Message message, int outBoxTimeout = -1, CancellationToken cancellationToken = default, - IAmABoxTransactionProvider transactionProvider = null) + IAmABoxTransactionProvider transactionProvider = null) { var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); @@ -170,7 +176,7 @@ public Task AddAsync( IEnumerable messages, int outBoxTimeout = -1, CancellationToken cancellationToken = default, - IAmABoxTransactionProvider transactionProvider = null + IAmABoxTransactionProvider transactionProvider = null ) { var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); @@ -289,10 +295,12 @@ public Task> GetAsync( } /// - /// Mark the message as dispatched - /// - /// The message to mark as dispatched - public Task MarkDispatchedAsync( + /// 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, @@ -315,7 +323,13 @@ public Task MarkDispatchedAsync( 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( @@ -332,10 +346,12 @@ public Task> DispatchedMessagesAsync( } /// - /// 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(); @@ -345,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) { @@ -363,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) @@ -371,6 +393,14 @@ public void Delete(params Guid[] messageIds) } } + /// + /// 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, @@ -384,6 +414,15 @@ public Task> GetAsync( return tcs.Task; } + /// + /// 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, @@ -398,6 +437,12 @@ public Task> OutstandingMessagesAsync( 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/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 6d4ac21855..99bfe9c08c 100644 --- a/src/Paramore.Brighter/OutboxArchiver.cs +++ b/src/Paramore.Brighter/OutboxArchiver.cs @@ -8,23 +8,23 @@ 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>(); - 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; @@ -36,7 +36,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); var age = TimeSpan.FromHours(minimumAge); try @@ -59,7 +59,7 @@ public void Archive(int minimumAge) } finally { - if(activity?.DisplayName == ARCHIVEOUTBOX) + if(activity?.DisplayName == ARCHIVE_OUTBOX) activity.Dispose(); } } @@ -71,7 +71,7 @@ public void Archive(int minimumAge) /// The Cancellation Token public async Task ArchiveAsync(int minimumAge, CancellationToken cancellationToken) { - var activity = ApplicationTelemetry.ActivitySource.StartActivity(ARCHIVEOUTBOX, ActivityKind.Server); + var activity = ApplicationTelemetry.ActivitySource.StartActivity(ARCHIVE_OUTBOX, ActivityKind.Server); var age = TimeSpan.FromHours(minimumAge); @@ -96,7 +96,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 f5a81035c2..dd3727d90d 100644 --- a/src/Paramore.Brighter/RelationDatabaseOutbox.cs +++ b/src/Paramore.Brighter/RelationDatabaseOutbox.cs @@ -9,7 +9,7 @@ namespace Paramore.Brighter { - public abstract class RelationDatabaseOutbox : IAmAnOutboxSync, IAmAnOutboxAsync, IAmABulkOutboxAsync + public abstract class RelationDatabaseOutbox : IAmAnOutboxSync, IAmAnOutboxAsync, IAmABulkOutboxAsync { private readonly IRelationDatabaseOutboxQueries _queries; private readonly ILogger _logger; @@ -43,7 +43,7 @@ protected RelationDatabaseOutbox(string outboxTableName, IRelationDatabaseOutbox public void Add( Message message, int outBoxTimeout = -1, - IAmABoxTransactionProvider transactionProvider = null) + IAmABoxTransactionProvider transactionProvider = null) { var parameters = InitAddDbParameters(message); WriteToStore(transactionProvider, connection => InitAddDbCommand(connection, parameters), () => @@ -64,7 +64,7 @@ public void Add( public void Add( IEnumerable messages, int outBoxTimeout = -1, - IAmABoxTransactionProvider transactionProvider = null + IAmABoxTransactionProvider transactionProvider = null ) { WriteToStore(transactionProvider, @@ -90,10 +90,12 @@ public void Delete(params Guid[] messageIds) /// Cancellation Token /// Connection Provider to use for this call /// Task<Message>. - public Task AddAsync(Message message, + public Task AddAsync( + Message message, int outBoxTimeout = -1, CancellationToken cancellationToken = default, - IAmABoxTransactionProvider transactionProvider = null) + IAmABoxTransactionProvider transactionProvider = null + ) { var parameters = InitAddDbParameters(message); return WriteToStoreAsync(transactionProvider, @@ -118,7 +120,7 @@ public Task AddAsync( IEnumerable messages, int outBoxTimeout = -1, CancellationToken cancellationToken = default, - IAmABoxTransactionProvider transactionProvider = null + IAmABoxTransactionProvider transactionProvider = null ) { return WriteToStoreAsync(transactionProvider, @@ -341,13 +343,13 @@ public Task DeleteAsync(CancellationToken cancellationToken, params Guid[] messa #endregion protected abstract void WriteToStore( - IAmABoxTransactionProvider transactionProvider, + IAmABoxTransactionProvider transactionProvider, Func commandFunc, Action loggingAction ); protected abstract Task WriteToStoreAsync( - IAmABoxTransactionProvider transactionProvider, + IAmABoxTransactionProvider transactionProvider, Func commandFunc, Action loggingAction, CancellationToken cancellationToken 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 81% rename from tests/Paramore.Brighter.Core.Tests/CommandProcessors/TestDoubles/FakeOutboxSync.cs rename to tests/Paramore.Brighter.Core.Tests/CommandProcessors/TestDoubles/FakeOutbox.cs index c59b171254..b84bc55bf8 100644 --- a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/TestDoubles/FakeOutboxSync.cs +++ b/tests/Paramore.Brighter.Core.Tests/CommandProcessors/TestDoubles/FakeOutbox.cs @@ -28,16 +28,19 @@ 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, IAmABoxTransactionProvider transactionProvider = null) + public void Add(Message message, int outBoxTimeout = -1, IAmABoxTransactionProvider transactionProvider = null) { _posts.Add(new OutboxEntry {Message = message, TimeDeposited = DateTime.UtcNow}); } @@ -46,7 +49,7 @@ public Task AddAsync( Message message, int outBoxTimeout = -1, CancellationToken cancellationToken = default, - IAmABoxTransactionProvider transactionProvider = null + IAmABoxTransactionProvider transactionProvider = null ) { if (cancellationToken.IsCancellationRequested) @@ -116,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); @@ -127,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) { @@ -136,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)); } @@ -192,8 +210,10 @@ class OutboxEntry public Message Message { get; set; } } - public void Add(IEnumerable messages, int outBoxTimeout = -1, - IAmABoxTransactionProvider transactionProvider = null) + public void Add( + IEnumerable messages, + int outBoxTimeout = -1, + IAmABoxTransactionProvider transactionProvider = null) { foreach (Message message in messages) { @@ -201,9 +221,11 @@ public void Add(IEnumerable messages, int outBoxTimeout = -1, } } - public async Task AddAsync(IEnumerable messages, int outBoxTimeout = -1, + public async Task AddAsync( + IEnumerable messages, + int outBoxTimeout = -1, CancellationToken cancellationToken = default, - IAmABoxTransactionProvider transactionProvider = null) + IAmABoxTransactionProvider transactionProvider = null) { foreach (var message in messages) { 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 2c73b2af82..384672f621 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] 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..c93e5c334f 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},}), + bus, 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..311c3c0ecf 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, + 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..1ffca10f6f 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()},}), + bus, 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..2da541c938 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()},}), + bus, 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_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 index 9f62659ba3..6822674f76 100644 --- 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 @@ -24,6 +24,7 @@ THE SOFTWARE. */ using System; using System.Collections.Generic; +using System.Transactions; using FluentAssertions; using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; using Paramore.Brighter.Core.Tests.TestHelpers; @@ -57,13 +58,18 @@ public CommandProcessorNoOutboxTests() 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 {{"MyCommand", _fakeMessageProducerWithPublishConfirmation},}); + IAmAnExternalBusService bus = new ExternalBusServices(producerRegistry, policyRegistry, null); + + CommandProcessor.ClearExtServiceBus(); _commandProcessor = new CommandProcessor( - new InMemoryRequestContextFactory(), - new PolicyRegistry { { CommandProcessor.RETRYPOLICY, retryPolicy }, { CommandProcessor.CIRCUITBREAKER, circuitBreakerPolicy } }, + new InMemoryRequestContextFactory(), + policyRegistry, messageMapperRegistry, - null, - new ProducerRegistry(new Dictionary {{"MyCommand", _fakeMessageProducerWithPublishConfirmation},})); + bus + ); } [Fact] 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 index c25385c1b4..fd173d01c4 100644 --- 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 @@ -25,6 +25,7 @@ THE SOFTWARE. */ using System; using System.Collections.Generic; using System.Threading.Tasks; +using System.Transactions; using FluentAssertions; using Paramore.Brighter.Core.Tests.CommandProcessors.TestDoubles; using Paramore.Brighter.Core.Tests.TestHelpers; @@ -58,13 +59,17 @@ public CommandProcessorNoOutboxAsyncTests() var circuitBreakerPolicy = Policy .Handle() .CircuitBreaker(1, TimeSpan.FromMilliseconds(1)); + + var policyRegistry = new PolicyRegistry { { CommandProcessor.RETRYPOLICYASYNC, retryPolicy }, { CommandProcessor.CIRCUITBREAKERASYNC, circuitBreakerPolicy } }; + var producerRegistry = new ProducerRegistry(new Dictionary {{"MyCommand", _fakeMessageProducerWithPublishConfirmation},}); + IAmAnExternalBusService bus = new ExternalBusServices(producerRegistry, policyRegistry, null); + CommandProcessor.ClearExtServiceBus(); _commandProcessor = new CommandProcessor( new InMemoryRequestContextFactory(), - new PolicyRegistry { { CommandProcessor.RETRYPOLICY, retryPolicy }, { CommandProcessor.CIRCUITBREAKER, circuitBreakerPolicy } }, + policyRegistry, messageMapperRegistry, - null, - new ProducerRegistry(new Dictionary {{"MyCommand", _fakeMessageProducerWithPublishConfirmation},})); + bus); } [Fact] 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_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..c6408c6e02 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 @@ -38,14 +38,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"; @@ -64,7 +64,7 @@ public PostCommandTests() .ExternalBus(new ExternalBusConfiguration( new ProducerRegistry(new Dictionary {{topic, _fakeMessageProducerWithPublishConfirmation},}), messageMapperRegistry), - _fakeOutboxSync) + _fakeOutbox) .RequestContextFactory(new InMemoryRequestContextFactory()) .Build(); } @@ -75,14 +75,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..03a8d26155 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; @@ -9,12 +10,12 @@ public class ControlBusSenderFactoryTests { private IAmAControlBusSender s_sender; private readonly IAmAControlBusSenderFactory s_senderFactory; - private readonly IAmAnOutboxSync _fakeOutboxSync; + private readonly IAmAnOutboxSync _fakeOutboxSync; private readonly IAmAMessageProducerSync s_fakeGateway; public ControlBusSenderFactoryTests() { - _fakeOutboxSync = A.Fake>(); + _fakeOutboxSync = A.Fake>(); s_fakeGateway = A.Fake(); s_senderFactory = new ControlBusSenderFactory(); diff --git a/tests/Paramore.Brighter.Core.Tests/MessageDispatch/TestDoubles/SpyCommandProcessor.cs b/tests/Paramore.Brighter.Core.Tests/MessageDispatch/TestDoubles/SpyCommandProcessor.cs index c361054f72..655d1e1173 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,26 +287,30 @@ 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); } - } } 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 86c6c8a44a..07152e2c1f 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..5297ed95ea 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(); @@ -55,16 +55,23 @@ public ImplicitClearingAsyncObservabilityTests() .Handle() .CircuitBreakerAsync(1, TimeSpan.FromMilliseconds(1)); - var policyRegistry = new PolicyRegistry {{CommandProcessor.RETRYPOLICY, retryPolicy}, {CommandProcessor.RETRYPOLICYASYNC, circuitBreakerPolicy}}; + var policyRegistry = new PolicyRegistry {{CommandProcessor.RETRYPOLICY, 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..6069008478 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,7 +72,7 @@ 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}}); @@ -77,7 +80,7 @@ public async void When_There_Is_A_Transaction_Between_Outbox_And_Entity() } catch (Exception e) { - Console.WriteLine(e); + _testOutputHelper.WriteLine(e.ToString()); throw; } 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) { From 9f0c9cb606540babdd0ca874a3356fff8c6f01b6 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Sat, 20 May 2023 15:10:51 +0100 Subject: [PATCH 49/89] Modify for generic AddBrighter - need shifting of generic parameters to Outbox --- .../ASBTaskQueue/GreetingsSender.Web/Program.cs | 5 +++-- samples/ASBTaskQueue/GreetingsSender/Program.cs | 4 +++- samples/AWSTaskQueue/GreetingsPumper/Program.cs | 3 ++- samples/AWSTaskQueue/GreetingsSender/Program.cs | 3 ++- .../ClaimCheck/GreetingsSender/Program.cs | 3 ++- .../Compression/GreetingsSender/Program.cs | 3 ++- samples/HelloAsyncListeners/Program.cs | 3 ++- samples/HelloWorld/Program.cs | 3 ++- samples/HelloWorldAsync/Program.cs | 3 ++- .../KafkaSchemaRegistry/GreetingsSender/Program.cs | 3 ++- samples/KafkaTaskQueue/GreetingsSender/Program.cs | 3 ++- .../CompetingSender/Program.cs | 2 +- .../GreetingsSender/Program.cs | 5 +++-- samples/OpenTelemetry/Producer/Program.cs | 3 ++- samples/OpenTelemetry/Sweeper/Program.cs | 2 +- samples/RMQRequestReply/GreetingsClient/Program.cs | 3 ++- samples/RMQTaskQueue/GreetingsSender/Program.cs | 3 ++- samples/RedisTaskQueue/GreetingsSender/Program.cs | 3 ++- samples/WebAPI_Dapper/GreetingsWeb/Startup.cs | 3 ++- .../WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs | 3 ++- samples/WebAPI_Dynamo/GreetingsWeb/Startup.cs | 2 +- samples/WebAPI_EFCore/GreetingsWeb/Startup.cs | 4 ++-- .../Orders.API/Program.cs | 3 ++- .../Extensions/BrighterExtensions.cs | 3 ++- .../ServiceCollectionExtensions.cs | 14 +++----------- .../TestDifferentSetups.cs | 9 +++++---- 26 files changed, 56 insertions(+), 42 deletions(-) diff --git a/samples/ASBTaskQueue/GreetingsSender.Web/Program.cs b/samples/ASBTaskQueue/GreetingsSender.Web/Program.cs index d626a014e2..6c88e936df 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; @@ -39,7 +40,7 @@ var outboxConfig = new RelationalDatabaseConfiguration(dbConnString, outBoxTableName: "BrighterOutbox"); builder.Services - .AddBrighter(opt => + .AddBrighter(opt => { opt.PolicyRegistry = new DefaultPolicy(); opt.CommandProcessorLifetime = ServiceLifetime.Scoped; diff --git a/samples/ASBTaskQueue/GreetingsSender/Program.cs b/samples/ASBTaskQueue/GreetingsSender/Program.cs index b6d8ff6b97..009e4fa80f 100644 --- a/samples/ASBTaskQueue/GreetingsSender/Program.cs +++ b/samples/ASBTaskQueue/GreetingsSender/Program.cs @@ -1,10 +1,12 @@ using System; +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,7 +21,7 @@ static void Main(string[] args) //TODO: add your ASB qualified name here var asbClientProvider = new ServiceBusVisualStudioCredentialClientProvider("fim-development-bus.servicebus.windows.net"); - serviceCollection.AddBrighter() + serviceCollection.AddBrighter() .UseInMemoryOutbox() .UseExternalBus(new AzureServiceBusProducerRegistryFactory( asbClientProvider, diff --git a/samples/AWSTaskQueue/GreetingsPumper/Program.cs b/samples/AWSTaskQueue/GreetingsPumper/Program.cs index bf634f2ed5..8d37ba4619 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,7 +32,7 @@ private static async Task Main(string[] args) { var awsConnection = new AWSMessagingGatewayConnection(credentials, RegionEndpoint.EUWest1); - services.AddBrighter() + services.AddBrighter() .UseInMemoryOutbox() .UseExternalBus(new SnsProducerRegistryFactory( awsConnection, diff --git a/samples/AWSTaskQueue/GreetingsSender/Program.cs b/samples/AWSTaskQueue/GreetingsSender/Program.cs index b6f1bac8e7..61d8d90da9 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,7 +53,7 @@ static void Main(string[] args) { var awsConnection = new AWSMessagingGatewayConnection(credentials, RegionEndpoint.EUWest1); - serviceCollection.AddBrighter() + serviceCollection.AddBrighter() .UseInMemoryOutbox() .UseExternalBus(new SnsProducerRegistryFactory( awsConnection, diff --git a/samples/AWSTransfomers/ClaimCheck/GreetingsSender/Program.cs b/samples/AWSTransfomers/ClaimCheck/GreetingsSender/Program.cs index b3cdfb116d..57b943344b 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; @@ -58,7 +59,7 @@ static void Main(string[] args) var topic = new RoutingKey(typeof(GreetingEvent).FullName.ToValidSNSTopicName()); - serviceCollection.AddBrighter() + serviceCollection.AddBrighter() .UseInMemoryOutbox() .UseExternalBus(new SnsProducerRegistryFactory( awsConnection, diff --git a/samples/AWSTransfomers/Compression/GreetingsSender/Program.cs b/samples/AWSTransfomers/Compression/GreetingsSender/Program.cs index 62b164eef7..85eacda2df 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; @@ -58,7 +59,7 @@ static void Main(string[] args) var topic = new RoutingKey(typeof(GreetingEvent).FullName.ToValidSNSTopicName()); - serviceCollection.AddBrighter() + serviceCollection.AddBrighter() .UseInMemoryOutbox() .UseExternalBus(new SnsProducerRegistryFactory( awsConnection, diff --git a/samples/HelloAsyncListeners/Program.cs b/samples/HelloAsyncListeners/Program.cs index 65228c51f6..d0693bac21 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; @@ -20,7 +21,7 @@ private static async Task Main(string[] args) .ConfigureServices((hostContext, services) => { - services.AddBrighter().AutoFromAssemblies(); + services.AddBrighter().AutoFromAssemblies(); services.AddHostedService(); } ) diff --git a/samples/HelloWorld/Program.cs b/samples/HelloWorld/Program.cs index 36de3a3172..ca14e6387b 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; @@ -39,7 +40,7 @@ private static void Main() var host = Host.CreateDefaultBuilder() .ConfigureServices((context, collection) => { - collection.AddBrighter().AutoFromAssemblies(); + collection.AddBrighter().AutoFromAssemblies(); }) .UseConsoleLifetime() .Build(); diff --git a/samples/HelloWorldAsync/Program.cs b/samples/HelloWorldAsync/Program.cs index 8ac8de44ce..8391911326 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; @@ -41,7 +42,7 @@ private static async Task Main(string[] args) .ConfigureServices((hostContext, services) => { - services.AddBrighter() + services.AddBrighter() .AutoFromAssemblies(); } ) diff --git a/samples/KafkaSchemaRegistry/GreetingsSender/Program.cs b/samples/KafkaSchemaRegistry/GreetingsSender/Program.cs index 4a1d522de5..a4f4eaf02a 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,7 +92,7 @@ static async Task Main(string[] args) var cachedSchemaRegistryClient = new CachedSchemaRegistryClient(schemaRegistryConfig); services.AddSingleton(cachedSchemaRegistryClient); - services.AddBrighter(options => + services.AddBrighter(options => { options.PolicyRegistry = policyRegistry; }) diff --git a/samples/KafkaTaskQueue/GreetingsSender/Program.cs b/samples/KafkaTaskQueue/GreetingsSender/Program.cs index b3f71c127e..19813e28d7 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,7 +86,7 @@ static async Task Main(string[] args) {CommandProcessor.CIRCUITBREAKERASYNC, circuitBreakerPolicyAsync} }; - services.AddBrighter(options => + services.AddBrighter(options => { options.PolicyRegistry = policyRegistry; }) diff --git a/samples/MsSqlMessagingGateway/CompetingSender/Program.cs b/samples/MsSqlMessagingGateway/CompetingSender/Program.cs index a197b6b026..89da4bfdd2 100644 --- a/samples/MsSqlMessagingGateway/CompetingSender/Program.cs +++ b/samples/MsSqlMessagingGateway/CompetingSender/Program.cs @@ -42,7 +42,7 @@ private static async Task Main(string[] args) //create the gateway var messagingConfiguration = new RelationalDatabaseConfiguration(@"Database=BrighterSqlQueue;Server=.\sqlexpress;Integrated Security=SSPI;", queueStoreTable: "QueueData"); - services.AddBrighter() + services.AddBrighter() .UseInMemoryOutbox() .UseExternalBus(new MsSqlProducerRegistryFactory( messagingConfiguration, diff --git a/samples/MsSqlMessagingGateway/GreetingsSender/Program.cs b/samples/MsSqlMessagingGateway/GreetingsSender/Program.cs index 11d2935386..89c930ab92 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; @@ -25,7 +26,7 @@ static void Main() var messagingConfiguration = new RelationalDatabaseConfiguration(@"Database=BrighterSqlQueue;Server=.\sqlexpress;Integrated Security=SSPI;", queueStoreTable: "QueueData"); - serviceCollection.AddBrighter() + serviceCollection.AddBrighter() .UseInMemoryOutbox() .UseExternalBus(new MsSqlProducerRegistryFactory( messagingConfiguration, diff --git a/samples/OpenTelemetry/Producer/Program.cs b/samples/OpenTelemetry/Producer/Program.cs index 248fbbaf97..59590bbe8e 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,7 +28,7 @@ Exchange = new Exchange("paramore.brighter.exchange"), }; -builder.Services.AddBrighter(options => +builder.Services.AddBrighter(options => { options.CommandProcessorLifetime = ServiceLifetime.Scoped; }) diff --git a/samples/OpenTelemetry/Sweeper/Program.cs b/samples/OpenTelemetry/Sweeper/Program.cs index 57595bb943..c3c81a9210 100644 --- a/samples/OpenTelemetry/Sweeper/Program.cs +++ b/samples/OpenTelemetry/Sweeper/Program.cs @@ -32,7 +32,7 @@ {"default", new FakeMessageProducer()} }); -builder.Services.AddBrighter() +builder.Services.AddBrighter() .UseExternalBus(producerRegistry) .UseInMemoryOutbox() .UseOutboxSweeper(options => diff --git a/samples/RMQRequestReply/GreetingsClient/Program.cs b/samples/RMQRequestReply/GreetingsClient/Program.cs index 8770bddfcc..d0f4ace46b 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; @@ -62,7 +63,7 @@ static void Main(string[] args) }; serviceCollection - .AddBrighter(options => + .AddBrighter(options => { options.ChannelFactory = new ChannelFactory(rmqMessageConsumerFactory); }) diff --git a/samples/RMQTaskQueue/GreetingsSender/Program.cs b/samples/RMQTaskQueue/GreetingsSender/Program.cs index 12bb05570f..3e8437320c 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; @@ -53,7 +54,7 @@ static void Main(string[] args) Exchange = new Exchange("paramore.brighter.exchange"), }; - serviceCollection.AddBrighter() + serviceCollection.AddBrighter() .UseInMemoryOutbox() .UseExternalBus(new RmqProducerRegistryFactory( rmqConnection, diff --git a/samples/RedisTaskQueue/GreetingsSender/Program.cs b/samples/RedisTaskQueue/GreetingsSender/Program.cs index 1a318dc712..4d07a272de 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; @@ -91,7 +92,7 @@ private static IHostBuilder CreateHostBuilder(string[] args) => MessageTimeToLive = TimeSpan.FromMinutes(10) }; - collection.AddBrighter() + collection.AddBrighter() .UseInMemoryOutbox() .UseExternalBus(new RedisProducerRegistryFactory( redisConnection, diff --git a/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs b/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs index c40db03711..82e43d1be3 100644 --- a/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs +++ b/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs @@ -1,4 +1,5 @@ using System; +using System.Data.Common; using DapperExtensions; using DapperExtensions.Sql; using FluentMigrator.Runner; @@ -160,7 +161,7 @@ private void ConfigureBrighter(IServiceCollection services) ); services.AddSingleton(outboxConfiguration); - services.AddBrighter(options => + services.AddBrighter(options => { //we want to use scoped, so make sure everything understands that which needs to options.HandlerLifetime = ServiceLifetime.Scoped; diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs index ff4ea79621..7c8c9526e3 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs +++ b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs @@ -1,4 +1,5 @@ using System; +using System.Data.Common; using Confluent.SchemaRegistry; using DapperExtensions; using DapperExtensions.Sql; @@ -174,7 +175,7 @@ private void ConfigureBrighter(IServiceCollection services) Name = "paramore.brighter.greetingsender", BootStrapServers = new[] { "localhost:9092" } }; - services.AddBrighter(options => + services.AddBrighter(options => { //we want to use scoped, so make sure everything understands that which needs to options.HandlerLifetime = ServiceLifetime.Scoped; diff --git a/samples/WebAPI_Dynamo/GreetingsWeb/Startup.cs b/samples/WebAPI_Dynamo/GreetingsWeb/Startup.cs index dbd582526a..f4337faae6 100644 --- a/samples/WebAPI_Dynamo/GreetingsWeb/Startup.cs +++ b/samples/WebAPI_Dynamo/GreetingsWeb/Startup.cs @@ -163,7 +163,7 @@ private void CreateOutbox(IAmazonDynamoDB client, IServiceCollection services) private void ConfigureBrighter(IServiceCollection services) { - services.AddBrighter(options => + services.AddBrighter(options => { //we want to use scoped, so make sure everything understands that which needs to options.HandlerLifetime = ServiceLifetime.Scoped; diff --git a/samples/WebAPI_EFCore/GreetingsWeb/Startup.cs b/samples/WebAPI_EFCore/GreetingsWeb/Startup.cs index f75796edcb..7cca8bec3f 100644 --- a/samples/WebAPI_EFCore/GreetingsWeb/Startup.cs +++ b/samples/WebAPI_EFCore/GreetingsWeb/Startup.cs @@ -111,7 +111,7 @@ private void ConfigureBrighter(IServiceCollection services) { if (_env.IsDevelopment()) { - services.AddBrighter(options => + services.AddBrighter(options => { //we want to use scoped, so make sure everything understands that which needs to options.HandlerLifetime = ServiceLifetime.Scoped; @@ -147,7 +147,7 @@ private void ConfigureBrighter(IServiceCollection services) } else { - services.AddBrighter(options => + services.AddBrighter(options => { options.HandlerLifetime = ServiceLifetime.Scoped; options.MapperLifetime = ServiceLifetime.Singleton; diff --git a/samples/WebApiWithWorkerAndSweeper/Orders.API/Program.cs b/samples/WebApiWithWorkerAndSweeper/Orders.API/Program.cs index 833740c277..70237b4b1c 100644 --- a/samples/WebApiWithWorkerAndSweeper/Orders.API/Program.cs +++ b/samples/WebApiWithWorkerAndSweeper/Orders.API/Program.cs @@ -2,6 +2,7 @@ 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; @@ -34,7 +35,7 @@ var outboxConfig = new RelationalDatabaseConfiguration(dbConnString, outBoxTableName: "BrighterOutbox"); builder.Services - .AddBrighter(opt => + .AddBrighter(opt => { opt.PolicyRegistry = new DefaultPolicy(); opt.CommandProcessorLifetime = ServiceLifetime.Scoped; diff --git a/samples/WebApiWithWorkerAndSweeper/Orders.Sweeper/Extensions/BrighterExtensions.cs b/samples/WebApiWithWorkerAndSweeper/Orders.Sweeper/Extensions/BrighterExtensions.cs index cd9de28917..522a8f340c 100644 --- a/samples/WebApiWithWorkerAndSweeper/Orders.Sweeper/Extensions/BrighterExtensions.cs +++ b/samples/WebApiWithWorkerAndSweeper/Orders.Sweeper/Extensions/BrighterExtensions.cs @@ -1,3 +1,4 @@ +using System.Transactions; using Azure.Identity; using Orders.Sweeper.Settings; using Paramore.Brighter; @@ -51,7 +52,7 @@ public static WebApplicationBuilder AddBrighter(this WebApplicationBuilder build outboxType = typeof(MsSqlSqlAuthConnectionProvider); } - builder.Services.AddBrighter() + builder.Services.AddBrighter() .UseExternalBus(producerRegistry) .UseMsSqlOutbox(outboxSettings, outboxType) .UseOutboxSweeper(options => diff --git a/src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionExtensions.cs b/src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionExtensions.cs index b8392afaf5..0c35ddfd73 100644 --- a/src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionExtensions.cs @@ -56,17 +56,9 @@ public static class ServiceCollectionExtensions /// 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 - ) - { - return AddBrighterWithTransactionalMessaging(services, configure); - } - - private static IBrighterBuilder AddBrighterWithTransactionalMessaging( - IServiceCollection services, - Action configure) + public static IBrighterBuilder AddBrighter( + this IServiceCollection services, + Action configure = null) { if (services == null) throw new ArgumentNullException(nameof(services)); diff --git a/tests/Paramore.Brighter.Extensions.Tests/TestDifferentSetups.cs b/tests/Paramore.Brighter.Extensions.Tests/TestDifferentSetups.cs index 79aa81c260..daef9fde30 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; @@ -19,7 +20,7 @@ public void BasicSetup() { var serviceCollection = new ServiceCollection(); - serviceCollection.AddBrighter().AutoFromAssemblies(); + serviceCollection.AddBrighter().AutoFromAssemblies(); var serviceProvider = serviceCollection.BuildServiceProvider(); @@ -37,7 +38,7 @@ public void WithProducerRegistry() serviceCollection.AddSingleton(); serviceCollection - .AddBrighter() + .AddBrighter() .UseInMemoryOutbox() .UseExternalBus(producer, false) .AutoFromAssemblies(); @@ -69,7 +70,7 @@ public void WithCustomPolicy() serviceCollection - .AddBrighter(options => options.PolicyRegistry = policyRegistry) + .AddBrighter(options => options.PolicyRegistry = policyRegistry) .AutoFromAssemblies(); var serviceProvider = serviceCollection.BuildServiceProvider(); @@ -84,7 +85,7 @@ public void WithScopedLifetime() { var serviceCollection = new ServiceCollection(); - serviceCollection.AddBrighter(options => options.CommandProcessorLifetime = ServiceLifetime.Scoped + serviceCollection.AddBrighter(options => options.CommandProcessorLifetime = ServiceLifetime.Scoped ).AutoFromAssemblies(); Assert.Equal( ServiceLifetime.Scoped, serviceCollection.SingleOrDefault(x => x.ServiceType == typeof(IAmACommandProcessor))?.Lifetime); From be86eab60c643f3b402c3a175ccfc071d7a18710 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Sat, 20 May 2023 15:20:31 +0100 Subject: [PATCH 50/89] Tests don't apply any more, default to an in memory outbox --- ...A_Message_And_There_Is_No_Message_Store.cs | 89 ------------------- ...age_And_There_Is_No_Message_Store_Async.cs | 89 ------------------- 2 files changed, 178 deletions(-) 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 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 6822674f76..0000000000 --- a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Posting_A_Message_And_There_Is_No_Message_Store.cs +++ /dev/null @@ -1,89 +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 System.Transactions; -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)); - - var policyRegistry = new PolicyRegistry { { CommandProcessor.RETRYPOLICY, retryPolicy }, { CommandProcessor.CIRCUITBREAKER, circuitBreakerPolicy } }; - var producerRegistry = new ProducerRegistry(new Dictionary {{"MyCommand", _fakeMessageProducerWithPublishConfirmation},}); - IAmAnExternalBusService bus = new ExternalBusServices(producerRegistry, policyRegistry, null); - - CommandProcessor.ClearExtServiceBus(); - _commandProcessor = new CommandProcessor( - new InMemoryRequestContextFactory(), - policyRegistry, - messageMapperRegistry, - bus - ); - } - - [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 fd173d01c4..0000000000 --- a/tests/Paramore.Brighter.Core.Tests/CommandProcessors/When_Posting_A_Message_And_There_Is_No_Message_Store_Async.cs +++ /dev/null @@ -1,89 +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 System.Transactions; -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)); - - var policyRegistry = new PolicyRegistry { { CommandProcessor.RETRYPOLICYASYNC, retryPolicy }, { CommandProcessor.CIRCUITBREAKERASYNC, circuitBreakerPolicy } }; - var producerRegistry = new ProducerRegistry(new Dictionary {{"MyCommand", _fakeMessageProducerWithPublishConfirmation},}); - IAmAnExternalBusService bus = new ExternalBusServices(producerRegistry, policyRegistry, null); - - CommandProcessor.ClearExtServiceBus(); - _commandProcessor = new CommandProcessor( - new InMemoryRequestContextFactory(), - policyRegistry, - messageMapperRegistry, - bus); - } - - [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(); - } - } -} From fee00bf1e94de23153a9719562448888b59d9bae Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Sat, 20 May 2023 15:33:33 +0100 Subject: [PATCH 51/89] Fix failing test --- ...Implicitly_Clearing_The_Outbox_async_A_Span_Is_Exported.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 5297ed95ea..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 @@ -49,13 +49,13 @@ 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.CIRCUITBREAKERASYNC, circuitBreakerPolicy}}; + var policyRegistry = new PolicyRegistry {{CommandProcessor.RETRYPOLICYASYNC, retryPolicy}, {CommandProcessor.CIRCUITBREAKERASYNC, circuitBreakerPolicy}}; var producerRegistry = new ProducerRegistry(new Dictionary { {MyEvent.Topic, new FakeMessageProducer()} From 07cf08d46f2d1a63f8a6a7e34ffa9497cb826a55 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Sat, 20 May 2023 16:02:35 +0100 Subject: [PATCH 52/89] Fix failing mssql tests that open the connection --- src/Paramore.Brighter.Inbox.MsSql/MsSqlInbox.cs | 4 ---- .../SqlQueues/MsSqlMessageQueue.cs | 5 ----- tests/Paramore.Brighter.MSSQL.Tests/MsSqlTestHelper.cs | 5 ----- 3 files changed, 14 deletions(-) diff --git a/src/Paramore.Brighter.Inbox.MsSql/MsSqlInbox.cs b/src/Paramore.Brighter.Inbox.MsSql/MsSqlInbox.cs index b9f3836713..5f3c9a8ee5 100644 --- a/src/Paramore.Brighter.Inbox.MsSql/MsSqlInbox.cs +++ b/src/Paramore.Brighter.Inbox.MsSql/MsSqlInbox.cs @@ -84,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 { @@ -161,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 { @@ -271,7 +269,6 @@ params IDbDataParameter[] parameters command.CommandText = sql; command.Parameters.AddRange(parameters); - connection.Open(); var item = execute(command); return item; } @@ -291,7 +288,6 @@ 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; } diff --git a/src/Paramore.Brighter.MessagingGateway.MsSql/SqlQueues/MsSqlMessageQueue.cs b/src/Paramore.Brighter.MessagingGateway.MsSql/SqlQueues/MsSqlMessageQueue.cs index 6edd80c923..830397468e 100644 --- a/src/Paramore.Brighter.MessagingGateway.MsSql/SqlQueues/MsSqlMessageQueue.cs +++ b/src/Paramore.Brighter.MessagingGateway.MsSql/SqlQueues/MsSqlMessageQueue.cs @@ -59,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(); } @@ -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,7 +185,6 @@ public void Purge() using (var connection = _connectionProvider.GetConnection()) { - connection.Open(); var sqlCmd = InitPurgeDbCommand(connection); sqlCmd.ExecuteNonQuery(); } diff --git a/tests/Paramore.Brighter.MSSQL.Tests/MsSqlTestHelper.cs b/tests/Paramore.Brighter.MSSQL.Tests/MsSqlTestHelper.cs index 11f6171a3e..eec22b2050 100644 --- a/tests/Paramore.Brighter.MSSQL.Tests/MsSqlTestHelper.cs +++ b/tests/Paramore.Brighter.MSSQL.Tests/MsSqlTestHelper.cs @@ -68,7 +68,6 @@ public void CreateDatabase() { using (var connection = _masterConnectionProvider.GetConnection()) { - connection.Open(); using (var command = connection.CreateCommand()) { command.CommandText = @" @@ -106,7 +105,6 @@ private void CreateQueueTable() var ddl = _binaryMessagePayload ? _binaryQueueDDL : _textQueueDDL; var createTableSql = string.Format(ddl, _tableName, Guid.NewGuid().ToString()); - connection.Open(); using (var command = connection.CreateCommand()) { command.CommandText = createTableSql; @@ -120,7 +118,6 @@ public void CleanUpDb() { using (var connection = _connectionProvider.GetConnection()) { - connection.Open(); using (var command = connection.CreateCommand()) { command.CommandText = $@" @@ -140,7 +137,6 @@ public void CreateOutboxTable() _tableName = $"[message_{_tableName}]"; var createTableSql = SqlOutboxBuilder.GetDDL(_tableName, _binaryMessagePayload); - connection.Open(); using (var command = connection.CreateCommand()) { command.CommandText = createTableSql; @@ -156,7 +152,6 @@ public void CreateInboxTable() _tableName = $"[command_{_tableName}]"; var createTableSql = SqlInboxBuilder.GetDDL(_tableName); - connection.Open(); using (var command = connection.CreateCommand()) { command.CommandText = createTableSql; From ab3ad37479ecaf05dcfe0fd7d80b4d1b2b5084b3 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Sun, 21 May 2023 17:55:41 +0200 Subject: [PATCH 53/89] Make archiver non-generic --- .../GreetingsSender.Web/Program.cs | 2 +- .../ASBTaskQueue/GreetingsSender/Program.cs | 2 +- .../AWSTaskQueue/GreetingsPumper/Program.cs | 2 +- .../AWSTaskQueue/GreetingsSender/Program.cs | 2 +- .../ClaimCheck/GreetingsSender/Program.cs | 2 +- .../Compression/GreetingsSender/Program.cs | 2 +- samples/HelloAsyncListeners/Program.cs | 2 +- samples/HelloWorld/Program.cs | 2 +- samples/HelloWorldAsync/Program.cs | 2 +- .../GreetingsSender/Program.cs | 2 +- .../KafkaTaskQueue/GreetingsSender/Program.cs | 2 +- .../CompetingSender/Program.cs | 2 +- .../GreetingsSender/Program.cs | 2 +- samples/OpenTelemetry/Producer/Program.cs | 2 +- samples/OpenTelemetry/Sweeper/Program.cs | 2 +- .../GreetingsClient/Program.cs | 2 +- .../RMQTaskQueue/GreetingsSender/Program.cs | 2 +- .../RedisTaskQueue/GreetingsSender/Program.cs | 2 +- .../GreetingsWeb/Database/OutboxExtensions.cs | 8 +- samples/WebAPI_Dapper/GreetingsWeb/Startup.cs | 2 +- .../GreetingsWeb/Database/OutboxExtensions.cs | 8 +- .../GreetingsWeb/Startup.cs | 2 +- samples/WebAPI_Dynamo/GreetingsWeb/Startup.cs | 2 +- .../SalutationAnalytics/Program.cs | 4 +- samples/WebAPI_EFCore/GreetingsWeb/Startup.cs | 10 +- .../Orders.API/Program.cs | 2 +- .../Extensions/BrighterExtensions.cs | 2 +- .../ServiceCollectionExtensions.cs | 181 ++++++++---------- .../TimedOutboxArchiver.cs | 4 +- .../ServiceCollectionExtensions.cs | 77 +++----- .../ServiceCollectionExtensions.cs | 2 +- .../ControlBusReceiverBuilder.cs | 7 +- .../CommandProcessorBuilder.cs | 137 +++++++++++-- .../ControlBusSenderFactory.cs | 10 +- .../ExternalBusConfiguration.cs | 40 +++- src/Paramore.Brighter/ExternalBusServices.cs | 4 +- src/Paramore.Brighter/ExternalBusType.cs | 35 ++++ src/Paramore.Brighter/IAmABulkOutboxSync.cs | 2 +- .../IAmAControlBusSenderFactory.cs | 2 +- src/Paramore.Brighter/IAmAnOutbox.cs | 4 +- src/Paramore.Brighter/IAmAnOutboxAsync.cs | 2 +- src/Paramore.Brighter/IAmAnOutboxSync.cs | 2 +- src/Paramore.Brighter/OutboxArchiver.cs | 2 +- ..._Limit_Total_Writes_To_OutBox_In_Window.cs | 16 +- .../When_Posting_With_A_Default_Policy.cs | 14 +- .../When_creating_a_control_bus_sender.cs | 22 +-- .../TestDifferentSetups.cs | 8 +- 47 files changed, 385 insertions(+), 262 deletions(-) create mode 100644 src/Paramore.Brighter/ExternalBusType.cs diff --git a/samples/ASBTaskQueue/GreetingsSender.Web/Program.cs b/samples/ASBTaskQueue/GreetingsSender.Web/Program.cs index 6c88e936df..d3d6e15bd2 100644 --- a/samples/ASBTaskQueue/GreetingsSender.Web/Program.cs +++ b/samples/ASBTaskQueue/GreetingsSender.Web/Program.cs @@ -40,7 +40,7 @@ var outboxConfig = new RelationalDatabaseConfiguration(dbConnString, outBoxTableName: "BrighterOutbox"); builder.Services - .AddBrighter(opt => + .AddBrighter(opt => { opt.PolicyRegistry = new DefaultPolicy(); opt.CommandProcessorLifetime = ServiceLifetime.Scoped; diff --git a/samples/ASBTaskQueue/GreetingsSender/Program.cs b/samples/ASBTaskQueue/GreetingsSender/Program.cs index 009e4fa80f..87e8346d1d 100644 --- a/samples/ASBTaskQueue/GreetingsSender/Program.cs +++ b/samples/ASBTaskQueue/GreetingsSender/Program.cs @@ -21,7 +21,7 @@ static void Main(string[] args) //TODO: add your ASB qualified name here var asbClientProvider = new ServiceBusVisualStudioCredentialClientProvider("fim-development-bus.servicebus.windows.net"); - serviceCollection.AddBrighter() + serviceCollection.AddBrighter() .UseInMemoryOutbox() .UseExternalBus(new AzureServiceBusProducerRegistryFactory( asbClientProvider, diff --git a/samples/AWSTaskQueue/GreetingsPumper/Program.cs b/samples/AWSTaskQueue/GreetingsPumper/Program.cs index 8d37ba4619..4bda09325b 100644 --- a/samples/AWSTaskQueue/GreetingsPumper/Program.cs +++ b/samples/AWSTaskQueue/GreetingsPumper/Program.cs @@ -32,7 +32,7 @@ private static async Task Main(string[] args) { var awsConnection = new AWSMessagingGatewayConnection(credentials, RegionEndpoint.EUWest1); - services.AddBrighter() + services.AddBrighter() .UseInMemoryOutbox() .UseExternalBus(new SnsProducerRegistryFactory( awsConnection, diff --git a/samples/AWSTaskQueue/GreetingsSender/Program.cs b/samples/AWSTaskQueue/GreetingsSender/Program.cs index 61d8d90da9..6960327480 100644 --- a/samples/AWSTaskQueue/GreetingsSender/Program.cs +++ b/samples/AWSTaskQueue/GreetingsSender/Program.cs @@ -53,7 +53,7 @@ static void Main(string[] args) { var awsConnection = new AWSMessagingGatewayConnection(credentials, RegionEndpoint.EUWest1); - serviceCollection.AddBrighter() + serviceCollection.AddBrighter() .UseInMemoryOutbox() .UseExternalBus(new SnsProducerRegistryFactory( awsConnection, diff --git a/samples/AWSTransfomers/ClaimCheck/GreetingsSender/Program.cs b/samples/AWSTransfomers/ClaimCheck/GreetingsSender/Program.cs index 57b943344b..dc7e8d5ac5 100644 --- a/samples/AWSTransfomers/ClaimCheck/GreetingsSender/Program.cs +++ b/samples/AWSTransfomers/ClaimCheck/GreetingsSender/Program.cs @@ -59,7 +59,7 @@ static void Main(string[] args) var topic = new RoutingKey(typeof(GreetingEvent).FullName.ToValidSNSTopicName()); - serviceCollection.AddBrighter() + serviceCollection.AddBrighter() .UseInMemoryOutbox() .UseExternalBus(new SnsProducerRegistryFactory( awsConnection, diff --git a/samples/AWSTransfomers/Compression/GreetingsSender/Program.cs b/samples/AWSTransfomers/Compression/GreetingsSender/Program.cs index 85eacda2df..5f5aa2c62a 100644 --- a/samples/AWSTransfomers/Compression/GreetingsSender/Program.cs +++ b/samples/AWSTransfomers/Compression/GreetingsSender/Program.cs @@ -59,7 +59,7 @@ static void Main(string[] args) var topic = new RoutingKey(typeof(GreetingEvent).FullName.ToValidSNSTopicName()); - serviceCollection.AddBrighter() + serviceCollection.AddBrighter() .UseInMemoryOutbox() .UseExternalBus(new SnsProducerRegistryFactory( awsConnection, diff --git a/samples/HelloAsyncListeners/Program.cs b/samples/HelloAsyncListeners/Program.cs index d0693bac21..29e70c5889 100644 --- a/samples/HelloAsyncListeners/Program.cs +++ b/samples/HelloAsyncListeners/Program.cs @@ -21,7 +21,7 @@ private static async Task Main(string[] args) .ConfigureServices((hostContext, services) => { - services.AddBrighter().AutoFromAssemblies(); + services.AddBrighter().AutoFromAssemblies(); services.AddHostedService(); } ) diff --git a/samples/HelloWorld/Program.cs b/samples/HelloWorld/Program.cs index ca14e6387b..3fadbd17c7 100644 --- a/samples/HelloWorld/Program.cs +++ b/samples/HelloWorld/Program.cs @@ -40,7 +40,7 @@ private static void Main() var host = Host.CreateDefaultBuilder() .ConfigureServices((context, collection) => { - collection.AddBrighter().AutoFromAssemblies(); + collection.AddBrighter().AutoFromAssemblies(); }) .UseConsoleLifetime() .Build(); diff --git a/samples/HelloWorldAsync/Program.cs b/samples/HelloWorldAsync/Program.cs index 8391911326..42ecc8fce9 100644 --- a/samples/HelloWorldAsync/Program.cs +++ b/samples/HelloWorldAsync/Program.cs @@ -42,7 +42,7 @@ private static async Task Main(string[] args) .ConfigureServices((hostContext, services) => { - services.AddBrighter() + services.AddBrighter() .AutoFromAssemblies(); } ) diff --git a/samples/KafkaSchemaRegistry/GreetingsSender/Program.cs b/samples/KafkaSchemaRegistry/GreetingsSender/Program.cs index a4f4eaf02a..3a63e0ddac 100644 --- a/samples/KafkaSchemaRegistry/GreetingsSender/Program.cs +++ b/samples/KafkaSchemaRegistry/GreetingsSender/Program.cs @@ -92,7 +92,7 @@ static async Task Main(string[] args) var cachedSchemaRegistryClient = new CachedSchemaRegistryClient(schemaRegistryConfig); services.AddSingleton(cachedSchemaRegistryClient); - services.AddBrighter(options => + services.AddBrighter(options => { options.PolicyRegistry = policyRegistry; }) diff --git a/samples/KafkaTaskQueue/GreetingsSender/Program.cs b/samples/KafkaTaskQueue/GreetingsSender/Program.cs index 19813e28d7..349a3de3c3 100644 --- a/samples/KafkaTaskQueue/GreetingsSender/Program.cs +++ b/samples/KafkaTaskQueue/GreetingsSender/Program.cs @@ -86,7 +86,7 @@ static async Task Main(string[] args) {CommandProcessor.CIRCUITBREAKERASYNC, circuitBreakerPolicyAsync} }; - services.AddBrighter(options => + services.AddBrighter(options => { options.PolicyRegistry = policyRegistry; }) diff --git a/samples/MsSqlMessagingGateway/CompetingSender/Program.cs b/samples/MsSqlMessagingGateway/CompetingSender/Program.cs index 89da4bfdd2..a197b6b026 100644 --- a/samples/MsSqlMessagingGateway/CompetingSender/Program.cs +++ b/samples/MsSqlMessagingGateway/CompetingSender/Program.cs @@ -42,7 +42,7 @@ private static async Task Main(string[] args) //create the gateway var messagingConfiguration = new RelationalDatabaseConfiguration(@"Database=BrighterSqlQueue;Server=.\sqlexpress;Integrated Security=SSPI;", queueStoreTable: "QueueData"); - services.AddBrighter() + services.AddBrighter() .UseInMemoryOutbox() .UseExternalBus(new MsSqlProducerRegistryFactory( messagingConfiguration, diff --git a/samples/MsSqlMessagingGateway/GreetingsSender/Program.cs b/samples/MsSqlMessagingGateway/GreetingsSender/Program.cs index 89c930ab92..6f78ae6bbc 100644 --- a/samples/MsSqlMessagingGateway/GreetingsSender/Program.cs +++ b/samples/MsSqlMessagingGateway/GreetingsSender/Program.cs @@ -26,7 +26,7 @@ static void Main() var messagingConfiguration = new RelationalDatabaseConfiguration(@"Database=BrighterSqlQueue;Server=.\sqlexpress;Integrated Security=SSPI;", queueStoreTable: "QueueData"); - serviceCollection.AddBrighter() + serviceCollection.AddBrighter() .UseInMemoryOutbox() .UseExternalBus(new MsSqlProducerRegistryFactory( messagingConfiguration, diff --git a/samples/OpenTelemetry/Producer/Program.cs b/samples/OpenTelemetry/Producer/Program.cs index 59590bbe8e..b12f779fb8 100644 --- a/samples/OpenTelemetry/Producer/Program.cs +++ b/samples/OpenTelemetry/Producer/Program.cs @@ -28,7 +28,7 @@ Exchange = new Exchange("paramore.brighter.exchange"), }; -builder.Services.AddBrighter(options => +builder.Services.AddBrighter(options => { options.CommandProcessorLifetime = ServiceLifetime.Scoped; }) diff --git a/samples/OpenTelemetry/Sweeper/Program.cs b/samples/OpenTelemetry/Sweeper/Program.cs index c3c81a9210..57595bb943 100644 --- a/samples/OpenTelemetry/Sweeper/Program.cs +++ b/samples/OpenTelemetry/Sweeper/Program.cs @@ -32,7 +32,7 @@ {"default", new FakeMessageProducer()} }); -builder.Services.AddBrighter() +builder.Services.AddBrighter() .UseExternalBus(producerRegistry) .UseInMemoryOutbox() .UseOutboxSweeper(options => diff --git a/samples/RMQRequestReply/GreetingsClient/Program.cs b/samples/RMQRequestReply/GreetingsClient/Program.cs index d0f4ace46b..e9ad16815a 100644 --- a/samples/RMQRequestReply/GreetingsClient/Program.cs +++ b/samples/RMQRequestReply/GreetingsClient/Program.cs @@ -63,7 +63,7 @@ static void Main(string[] args) }; serviceCollection - .AddBrighter(options => + .AddBrighter(options => { options.ChannelFactory = new ChannelFactory(rmqMessageConsumerFactory); }) diff --git a/samples/RMQTaskQueue/GreetingsSender/Program.cs b/samples/RMQTaskQueue/GreetingsSender/Program.cs index 3e8437320c..fd041dce5b 100644 --- a/samples/RMQTaskQueue/GreetingsSender/Program.cs +++ b/samples/RMQTaskQueue/GreetingsSender/Program.cs @@ -54,7 +54,7 @@ static void Main(string[] args) Exchange = new Exchange("paramore.brighter.exchange"), }; - serviceCollection.AddBrighter() + serviceCollection.AddBrighter() .UseInMemoryOutbox() .UseExternalBus(new RmqProducerRegistryFactory( rmqConnection, diff --git a/samples/RedisTaskQueue/GreetingsSender/Program.cs b/samples/RedisTaskQueue/GreetingsSender/Program.cs index 4d07a272de..276d22292f 100644 --- a/samples/RedisTaskQueue/GreetingsSender/Program.cs +++ b/samples/RedisTaskQueue/GreetingsSender/Program.cs @@ -92,7 +92,7 @@ private static IHostBuilder CreateHostBuilder(string[] args) => MessageTimeToLive = TimeSpan.FromMinutes(10) }; - collection.AddBrighter() + collection.AddBrighter() .UseInMemoryOutbox() .UseExternalBus(new RedisProducerRegistryFactory( redisConnection, diff --git a/samples/WebAPI_Dapper/GreetingsWeb/Database/OutboxExtensions.cs b/samples/WebAPI_Dapper/GreetingsWeb/Database/OutboxExtensions.cs index 7793a3e271..1e2603ae39 100644 --- a/samples/WebAPI_Dapper/GreetingsWeb/Database/OutboxExtensions.cs +++ b/samples/WebAPI_Dapper/GreetingsWeb/Database/OutboxExtensions.cs @@ -43,13 +43,7 @@ public static IBrighterBuilder AddOutbox( private static void AddMySqlOutbox(IBrighterBuilder brighterBuilder, RelationalDatabaseConfiguration configuration) { - brighterBuilder.UseMySqlOutbox( - configuration, - typeof(MySqlConnectionProvider), - ServiceLifetime.Singleton) - .UseMySqTransactionConnectionProvider( - typeof(MySqlUnitOfWork), ServiceLifetime.Scoped) - .UseOutboxSweeper(); + brighterBuilder.UseMySqlOutbox(configuration, typeof(MySqlUnitOfWork)).UseOutboxSweeper(); } private static void AddSqliteOutBox(IBrighterBuilder brighterBuilder, diff --git a/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs b/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs index 82e43d1be3..cc2c93ccf1 100644 --- a/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs +++ b/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs @@ -161,7 +161,7 @@ private void ConfigureBrighter(IServiceCollection services) ); services.AddSingleton(outboxConfiguration); - services.AddBrighter(options => + services.AddBrighter(options => { //we want to use scoped, so make sure everything understands that which needs to options.HandlerLifetime = ServiceLifetime.Scoped; diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/OutboxExtensions.cs b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/OutboxExtensions.cs index 7793a3e271..1e2603ae39 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/OutboxExtensions.cs +++ b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/OutboxExtensions.cs @@ -43,13 +43,7 @@ public static IBrighterBuilder AddOutbox( private static void AddMySqlOutbox(IBrighterBuilder brighterBuilder, RelationalDatabaseConfiguration configuration) { - brighterBuilder.UseMySqlOutbox( - configuration, - typeof(MySqlConnectionProvider), - ServiceLifetime.Singleton) - .UseMySqTransactionConnectionProvider( - typeof(MySqlUnitOfWork), ServiceLifetime.Scoped) - .UseOutboxSweeper(); + brighterBuilder.UseMySqlOutbox(configuration, typeof(MySqlUnitOfWork)).UseOutboxSweeper(); } private static void AddSqliteOutBox(IBrighterBuilder brighterBuilder, diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs index 7c8c9526e3..eb540d99dc 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs +++ b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs @@ -175,7 +175,7 @@ private void ConfigureBrighter(IServiceCollection services) Name = "paramore.brighter.greetingsender", BootStrapServers = new[] { "localhost:9092" } }; - services.AddBrighter(options => + services.AddBrighter(options => { //we want to use scoped, so make sure everything understands that which needs to options.HandlerLifetime = ServiceLifetime.Scoped; diff --git a/samples/WebAPI_Dynamo/GreetingsWeb/Startup.cs b/samples/WebAPI_Dynamo/GreetingsWeb/Startup.cs index f4337faae6..dbd582526a 100644 --- a/samples/WebAPI_Dynamo/GreetingsWeb/Startup.cs +++ b/samples/WebAPI_Dynamo/GreetingsWeb/Startup.cs @@ -163,7 +163,7 @@ private void CreateOutbox(IAmazonDynamoDB client, IServiceCollection services) private void ConfigureBrighter(IServiceCollection services) { - services.AddBrighter(options => + services.AddBrighter(options => { //we want to use scoped, so make sure everything understands that which needs to options.HandlerLifetime = ServiceLifetime.Scoped; diff --git a/samples/WebAPI_Dynamo/SalutationAnalytics/Program.cs b/samples/WebAPI_Dynamo/SalutationAnalytics/Program.cs index fc2de6c002..a8c5520bdd 100644 --- a/samples/WebAPI_Dynamo/SalutationAnalytics/Program.cs +++ b/samples/WebAPI_Dynamo/SalutationAnalytics/Program.cs @@ -123,7 +123,7 @@ private static void ConfigureBrighter( actionOnExists: OnceOnlyAction.Throw ) ) - .UseExternalOutbox(ConfigureOutbox(awsCredentials, dynamoDb)) + .UseExternalOutbox(ConfigureOutbox(awsCredentials, dynamoDb)) .UseDynamoDbTransactionConnectionProvider(typeof(DynamoDbUnitOfWork), ServiceLifetime.Scoped); services.AddHostedService(); @@ -240,7 +240,7 @@ private static IAmAnInbox ConfigureInbox(IAmazonDynamoDB dynamoDb) return new DynamoDbInbox(dynamoDb); } - 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_EFCore/GreetingsWeb/Startup.cs b/samples/WebAPI_EFCore/GreetingsWeb/Startup.cs index 7cca8bec3f..5715daa615 100644 --- a/samples/WebAPI_EFCore/GreetingsWeb/Startup.cs +++ b/samples/WebAPI_EFCore/GreetingsWeb/Startup.cs @@ -111,7 +111,7 @@ private void ConfigureBrighter(IServiceCollection services) { if (_env.IsDevelopment()) { - services.AddBrighter(options => + services.AddBrighter(options => { //we want to use scoped, so make sure everything understands that which needs to options.HandlerLifetime = ServiceLifetime.Scoped; @@ -147,7 +147,7 @@ private void ConfigureBrighter(IServiceCollection services) } else { - services.AddBrighter(options => + services.AddBrighter(options => { options.HandlerLifetime = ServiceLifetime.Scoped; options.MapperLifetime = ServiceLifetime.Singleton; @@ -170,8 +170,10 @@ private void ConfigureBrighter(IServiceCollection services) }} ).Create() ) - .UseMySqlOutbox(new RelationalDatabaseConfiguration(DbConnectionString(), _outBoxTableName), typeof(MySqlConnectionProvider), ServiceLifetime.Singleton) - .UseMySqTransactionConnectionProvider(typeof(MySqlEntityFrameworkConnectionProvider), ServiceLifetime.Scoped) + .UseMySqlOutbox( + new RelationalDatabaseConfiguration(DbConnectionString(), _outBoxTableName), + typeof(MySqlEntityFrameworkConnectionProvider) + ) .UseOutboxSweeper() .AutoFromAssemblies(); } diff --git a/samples/WebApiWithWorkerAndSweeper/Orders.API/Program.cs b/samples/WebApiWithWorkerAndSweeper/Orders.API/Program.cs index 70237b4b1c..a40821b4c9 100644 --- a/samples/WebApiWithWorkerAndSweeper/Orders.API/Program.cs +++ b/samples/WebApiWithWorkerAndSweeper/Orders.API/Program.cs @@ -35,7 +35,7 @@ var outboxConfig = new RelationalDatabaseConfiguration(dbConnString, outBoxTableName: "BrighterOutbox"); builder.Services - .AddBrighter(opt => + .AddBrighter(opt => { opt.PolicyRegistry = new DefaultPolicy(); opt.CommandProcessorLifetime = ServiceLifetime.Scoped; diff --git a/samples/WebApiWithWorkerAndSweeper/Orders.Sweeper/Extensions/BrighterExtensions.cs b/samples/WebApiWithWorkerAndSweeper/Orders.Sweeper/Extensions/BrighterExtensions.cs index 522a8f340c..0a9a8b82eb 100644 --- a/samples/WebApiWithWorkerAndSweeper/Orders.Sweeper/Extensions/BrighterExtensions.cs +++ b/samples/WebApiWithWorkerAndSweeper/Orders.Sweeper/Extensions/BrighterExtensions.cs @@ -52,7 +52,7 @@ public static WebApplicationBuilder AddBrighter(this WebApplicationBuilder build outboxType = typeof(MsSqlSqlAuthConnectionProvider); } - builder.Services.AddBrighter() + builder.Services.AddBrighter() .UseExternalBus(producerRegistry) .UseMsSqlOutbox(outboxSettings, outboxType) .UseOutboxSweeper(options => diff --git a/src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionExtensions.cs b/src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionExtensions.cs index 0c35ddfd73..6824902273 100644 --- a/src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionExtensions.cs @@ -39,9 +39,11 @@ namespace Paramore.Brighter.Extensions.DependencyInjection { public static class ServiceCollectionExtensions { - private static int _outboxBulkChunkSize = 100; - private static Type _outboxType; - private static Type _asyncOutboxType; + private static IAmAnExternalBusService s_inMemoryBusService; + private static IAmAnExternalBusService s_dbBusService; + private static IAmAnExternalBusService s_rpcBusService; + private static BrighterOptions s_options; + private static ExternalBusConfiguration s_externalBusConfiguration = new ExternalBusConfiguration(); /// /// Will add Brighter into the .NET IoC Container - ServiceCollection @@ -56,18 +58,18 @@ public static class ServiceCollectionExtensions /// 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( + public static IBrighterBuilder AddBrighter( this IServiceCollection services, Action configure = null) { if (services == null) throw new ArgumentNullException(nameof(services)); - var options = new BrighterOptions(); - configure?.Invoke(options); - services.TryAddSingleton(options); + s_options = new BrighterOptions(); + configure?.Invoke(s_options); + services.TryAddSingleton(s_options); - return BrighterHandlerBuilder(services, options); + return BrighterHandlerBuilder(services, s_options); } /// @@ -81,7 +83,7 @@ public static IBrighterBuilder AddBrighter( /// Allows you to configure how we build Brighter /// A builder that can be used to populate the IoC container with handlers and mappers by inspection /// - used by built in factory from CommandProcessor - public static IBrighterBuilder BrighterHandlerBuilder(IServiceCollection services, BrighterOptions options) where TMessage : Message + public static IBrighterBuilder BrighterHandlerBuilder(IServiceCollection services, BrighterOptions options) { var subscriberRegistry = new ServiceCollectionSubscriberRegistry(services, options.HandlerLifetime); services.TryAddSingleton(subscriberRegistry); @@ -90,7 +92,7 @@ public static IBrighterBuilder BrighterHandlerBuilder(IS services.TryAddSingleton(transformRegistry); services.TryAdd(new ServiceDescriptor(typeof(IAmACommandProcessor), - (serviceProvider) => (IAmACommandProcessor)BuildCommandProcessor(serviceProvider), + (serviceProvider) => (IAmACommandProcessor)BuildCommandProcessor(serviceProvider), options.CommandProcessorLifetime)); var mapperRegistry = new ServiceCollectionMessageMapperRegistry(services, options.MapperLifetime); @@ -116,28 +118,40 @@ public static IBrighterBuilder BrighterHandlerBuilder(IS /// 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 - /// + /// Whe using a bulk outbox how should we chunk + /// When using an outbox how to long to timeout in ms /// public static IBrighterBuilder UseExternalOutbox( this IBrighterBuilder brighterBuilder, - IAmAnOutbox outbox = null, - int outboxBulkChunkSize = 100 - ) where TMessage : Message + IAmAnOutbox outbox, + int outboxBulkChunkSize = 100, + int outboxTimeout = 300) + where TMessage : Message { + s_externalBusConfiguration.UseInMemoryOutbox = false; + + s_externalBusConfiguration.Outbox = outbox; + s_externalBusConfiguration.OutboxBulkChunkSize = outboxBulkChunkSize; + s_externalBusConfiguration.OutboxWriteTimeout = outboxTimeout; if (outbox is IAmAnOutboxSync) { - _outboxType = typeof(IAmAnOutboxSync); - brighterBuilder.Services.TryAdd(new ServiceDescriptor(_outboxType, factory:_ => outbox, ServiceLifetime.Singleton)); + brighterBuilder.Services.TryAddSingleton>((IAmAnOutboxSync) outbox); } if (outbox is IAmAnOutboxAsync) { - _asyncOutboxType = typeof(IAmAnOutboxAsync); - brighterBuilder.Services.TryAdd(new ServiceDescriptor(_asyncOutboxType, _ => outbox, ServiceLifetime.Singleton)); + brighterBuilder.Services.TryAddSingleton>((IAmAnOutboxAsync) outbox); } - - _outboxBulkChunkSize = outboxBulkChunkSize; + + var bus = new ExternalBusServices( + s_externalBusConfiguration.ProducerRegistry, + s_options.PolicyRegistry, + s_externalBusConfiguration.Outbox, + s_externalBusConfiguration.OutboxBulkChunkSize, + s_externalBusConfiguration.OutboxWriteTimeout); + + brighterBuilder.Services.TryAddSingleton(bus); return brighterBuilder; } @@ -158,7 +172,8 @@ public static IBrighterBuilder UseExternalOutbox( /// public static IBrighterBuilder UseExternalInbox( this IBrighterBuilder brighterBuilder, - IAmAnInbox inbox, InboxConfiguration inboxConfiguration = null, + IAmAnInbox inbox, + InboxConfiguration inboxConfiguration = null, ServiceLifetime serviceLifetime = ServiceLifetime.Singleton) { if (inbox is IAmAnInboxSync) @@ -194,10 +209,25 @@ public static IBrighterBuilder UseExternalInbox( /// The Brighter builder to allow chaining of requests public static IBrighterBuilder UseInMemoryOutbox(this IBrighterBuilder brighterBuilder) { - brighterBuilder.Services.TryAdd(new ServiceDescriptor(typeof(IAmAnOutboxSync), - _ => new InMemoryOutbox(), ServiceLifetime.Singleton)); - brighterBuilder.Services.TryAdd(new ServiceDescriptor(typeof(IAmAnOutboxAsync), - _ => new InMemoryOutbox(), ServiceLifetime.Singleton)); + s_externalBusConfiguration.UseInMemoryOutbox = true; + s_externalBusConfiguration.Outbox = new InMemoryOutbox(); + s_externalBusConfiguration.OutboxBulkChunkSize = 100; + s_externalBusConfiguration.OutboxWriteTimeout = 300; + + brighterBuilder.Services.TryAdd(new ServiceDescriptor(typeof(IAmAnOutboxSync), + _ => s_externalBusConfiguration.Outbox, ServiceLifetime.Singleton)); + brighterBuilder.Services.TryAdd(new ServiceDescriptor(typeof(IAmAnOutboxAsync), + _ => s_externalBusConfiguration.Outbox, ServiceLifetime.Singleton)); + + var bus = new ExternalBusServices( + s_externalBusConfiguration.ProducerRegistry, + s_options.PolicyRegistry, + s_externalBusConfiguration.Outbox, + s_externalBusConfiguration.OutboxBulkChunkSize, + s_externalBusConfiguration.OutboxWriteTimeout + ); + + brighterBuilder.Services.TryAddSingleton(bus); return brighterBuilder; } @@ -215,7 +245,7 @@ public static IBrighterBuilder UseInMemoryOutbox(this IBrighterBuilder brighterB /// public static IBrighterBuilder UseInMemoryInbox(this IBrighterBuilder brighterBuilder) { - brighterBuilder.Services.TryAdd(new ServiceDescriptor(typeof(IAmAnInboxSync), _ => new InMemoryInbox(), + brighterBuilder.Services.TryAdd(new ServiceDescriptor(typeof(IAmAnInboxSync), _ => new InMemoryInbox(), ServiceLifetime.Singleton)); brighterBuilder.Services.TryAdd(new ServiceDescriptor(typeof(IAmAnInboxAsync), _ => new InMemoryInbox(), ServiceLifetime.Singleton)); @@ -240,12 +270,17 @@ public static IBrighterBuilder UseExternalBus( this IBrighterBuilder brighterBuilder, IAmAProducerRegistry producerRegistry, bool useRequestResponseQueues = false, - IEnumerable replyQueueSubscriptions = null) + IEnumerable replyQueueSubscriptions = null + ) { + s_externalBusConfiguration.ProducerRegistry = producerRegistry; + brighterBuilder.Services.TryAddSingleton(producerRegistry); - brighterBuilder.Services.TryAddSingleton(new UseRpc(useRequestResponseQueues, - replyQueueSubscriptions)); + if (useRequestResponseQueues) + { + brighterBuilder.Services.TryAddSingleton(new UseRpc(useRequestResponseQueues, replyQueueSubscriptions)); + } return brighterBuilder; } @@ -313,8 +348,7 @@ public static ServiceProviderTransformerFactory TransformFactory(IServiceProvide return new ServiceProviderTransformerFactory(provider); } - private static object BuildCommandProcessor(IServiceProvider provider) - where TMessage : Message + private static object BuildCommandProcessor(IServiceProvider provider) { var loggerFactory = provider.GetService(); ApplicationLogging.LoggerFactory = loggerFactory; @@ -326,15 +360,6 @@ private static object BuildCommandProcessor(IServiceProv var handlerFactory = new ServiceProviderHandlerFactory(provider); var handlerConfiguration = new HandlerConfiguration(subscriberRegistry, handlerFactory); - var messageMapperRegistry = MessageMapperRegistry(provider); - - var transformFactory = TransformFactory(provider); - - IAmAnOutbox outbox = provider.GetService>(); - if (outbox == null) outbox = new InMemoryOutbox() as IAmAnOutbox; - - var inboxConfiguration = provider.GetService(); - var producerRegistry = provider.GetService(); var needHandlers = CommandProcessorBuilder.With(); @@ -350,77 +375,31 @@ private static object BuildCommandProcessor(IServiceProv ? policyBuilder.DefaultPolicy() : policyBuilder.Policies(options.PolicyRegistry); - var commandProcessor = AddExternalBusMaybe( - options, - producerRegistry, - messagingBuilder, - messageMapperRegistry, - inboxConfiguration, - outbox, - useRequestResponse, - _outboxBulkChunkSize, - transformFactory) + INeedARequestContext ret = CreateEventBusMaybe(messagingBuilder, producerRegistry, useRequestResponse); + + var commandProcessor = ret .RequestContextFactory(options.RequestContextFactory) .Build(); return commandProcessor; } - - private enum ExternalBusType - { - None = 0, - FireAndForget = 1, - RPC = 2 - } - - private static INeedARequestContext AddExternalBusMaybe( - IBrighterOptions options, - IAmAProducerRegistry producerRegistry, + private static INeedARequestContext CreateEventBusMaybe( INeedMessaging messagingBuilder, - MessageMapperRegistry messageMapperRegistry, - InboxConfiguration inboxConfiguration, - IAmAnOutbox outbox, - IUseRpc useRequestResponse, - int outboxBulkChunkSize, - IAmAMessageTransformerFactory transformerFactory) where TMessage : Message + IAmAProducerRegistry producerRegistry, + IUseRpc useRequestResponse + ) { - ExternalBusType externalBusType = GetExternalBusType(producerRegistry, useRequestResponse); - - if (externalBusType == ExternalBusType.None) + if (producerRegistry == null) return messagingBuilder.NoExternalBus(); - else if (externalBusType == ExternalBusType.FireAndForget) - return messagingBuilder.ExternalBus( - new ExternalBusConfiguration( - producerRegistry, - messageMapperRegistry, - outboxBulkChunkSize: outboxBulkChunkSize, - useInbox: inboxConfiguration, - transformerFactory: transformerFactory), - outbox - ); - else if (externalBusType == ExternalBusType.RPC) - { - return messagingBuilder.ExternalRPC( - new ExternalBusConfiguration( - producerRegistry, - messageMapperRegistry, - responseChannelFactory: options.ChannelFactory, - useInbox: inboxConfiguration), - outbox, - useRequestResponse.ReplyQueueSubscriptions); - } - - throw new ArgumentOutOfRangeException("The external bus type requested was not understood"); - } + + IAmAnExternalBusService bus; + var busType = s_externalBusConfiguration.UseInMemoryOutbox ? ExternalBusType.InMemory : ExternalBusType.Db; + bus = busType ==ExternalBusType.InMemory ? s_inMemoryBusService : s_dbBusService; + if (useRequestResponse.RPC) busType = ExternalBusType.RPC; + + return messagingBuilder.ExternalBusNoCreate(busType, bus); - private static ExternalBusType GetExternalBusType(IAmAProducerRegistry producerRegistry, - IUseRpc useRequestResponse) - { - var externalBusType = producerRegistry == null ? ExternalBusType.None : ExternalBusType.FireAndForget; - if (externalBusType == ExternalBusType.FireAndForget && useRequestResponse.RPC) - externalBusType = ExternalBusType.RPC; - return externalBusType; } } } diff --git a/src/Paramore.Brighter.Extensions.Hosting/TimedOutboxArchiver.cs b/src/Paramore.Brighter.Extensions.Hosting/TimedOutboxArchiver.cs index 629c1f6a63..b6277c24a8 100644 --- a/src/Paramore.Brighter.Extensions.Hosting/TimedOutboxArchiver.cs +++ b/src/Paramore.Brighter.Extensions.Hosting/TimedOutboxArchiver.cs @@ -13,12 +13,12 @@ public class TimedOutboxArchiver : IHostedService, IDisp { private readonly TimedOutboxArchiverOptions _options; private static readonly ILogger s_logger = ApplicationLogging.CreateLogger(); - private readonly IAmAnOutbox _outbox; + private readonly IAmAnOutbox _outbox; private readonly IAmAnArchiveProvider _archiveProvider; private Timer _timer; public TimedOutboxArchiver( - IAmAnOutbox outbox, + IAmAnOutbox outbox, IAmAnArchiveProvider archiveProvider, TimedOutboxArchiverOptions options) { diff --git a/src/Paramore.Brighter.Outbox.MySql/ServiceCollectionExtensions.cs b/src/Paramore.Brighter.Outbox.MySql/ServiceCollectionExtensions.cs index 5ec0e70807..839d2626ce 100644 --- a/src/Paramore.Brighter.Outbox.MySql/ServiceCollectionExtensions.cs +++ b/src/Paramore.Brighter.Outbox.MySql/ServiceCollectionExtensions.cs @@ -13,68 +13,51 @@ public static class ServiceCollectionExtensions /// /// 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 + /// The provider of transactions for the outbox to participate in + /// What size should we chunk bulk work in + /// What is the timeout in ms for the Outbox + /// What is the lifetime of the services that we add (outbox always singleton) /// Allows fluent syntax /// Registers the following /// -- MySqlOutboxConfiguration: connection string and outbox name /// -- IAmARelationalDbConnectionProvider: 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 + /// -- IAmAnOutbox: an outbox to store messages we want to send + /// -- IAmAnOutboxSync<Message, DbTransaction>>: an outbox to store messages we want to send + /// -- IAmAnOutboxAsync<Message, DbTransaction>: an outbox to store messages we want to send + /// -- IAmABoxTransactionConnectionProvider: the provider of a connection for any existing transaction public static IBrighterBuilder UseMySqlOutbox( - this IBrighterBuilder brighterBuilder, RelationalDatabaseConfiguration configuration, Type connectionProvider, ServiceLifetime serviceLifetime = ServiceLifetime.Singleton) + this IBrighterBuilder brighterBuilder, + RelationalDatabaseConfiguration configuration, + Type transactionProvider, + int outboxBulkChunkSize = 100, + int outboxTimeout = 300, + ServiceLifetime serviceLifetime = ServiceLifetime.Scoped + ) { - brighterBuilder.Services.AddSingleton(configuration); - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmARelationalDbConnectionProvider), connectionProvider, serviceLifetime)); + if (brighterBuilder is null) + throw new ArgumentNullException($"{nameof(brighterBuilder)} cannot be null.", nameof(brighterBuilder)); - 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 transaction 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 transactionProvider, - ServiceLifetime serviceLifetime = ServiceLifetime.Scoped - ) - { if (transactionProvider is null) throw new ArgumentNullException($"{nameof(transactionProvider)} cannot be null.", nameof(transactionProvider)); - if (!typeof(IAmABoxTransactionProvider).IsAssignableFrom(transactionProvider)) - throw new Exception($"Unable to register provider of type {transactionProvider.Name}. Class does not implement interface {nameof(IAmABoxTransactionProvider)}."); - if (!typeof(IAmATransactionConnectionProvider).IsAssignableFrom(transactionProvider)) throw new Exception($"Unable to register provider of type {transactionProvider.Name}. Class does not implement interface {nameof(IAmATransactionConnectionProvider)}."); - //register the specific interface - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmABoxTransactionProvider), transactionProvider, serviceLifetime)); + + brighterBuilder.Services.AddSingleton(configuration); + brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmARelationalDbConnectionProvider), typeof(MySqlConnectionProvider), serviceLifetime)); + + brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmABoxTransactionProvider), transactionProvider, serviceLifetime)); - //register the combined interface just in case - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmATransactionConnectionProvider), transactionProvider, serviceLifetime)); - - return brighterBuilder; - } - - private static MySqlOutbox BuildMySqlOutboxOutbox(IServiceProvider provider) - { - var config = provider.GetService(); - var connectionProvider = provider.GetService(); + //register the combined interface just in case + brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmATransactionConnectionProvider), transactionProvider, serviceLifetime)); - return new MySqlOutbox(config, connectionProvider); + var outbox = new MySqlOutbox(configuration, new MySqlConnectionProvider(configuration)); + + brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmAnOutboxSync), outbox)); + brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmAnOutboxAsync), outbox)); + + return brighterBuilder.UseExternalOutbox(outbox, outboxBulkChunkSize, outboxTimeout); } } } diff --git a/src/Paramore.Brighter.ServiceActivator.Extensions.DependencyInjection/ServiceCollectionExtensions.cs b/src/Paramore.Brighter.ServiceActivator.Extensions.DependencyInjection/ServiceCollectionExtensions.cs index 51aedab7a7..2484edd50f 100644 --- a/src/Paramore.Brighter.ServiceActivator.Extensions.DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Paramore.Brighter.ServiceActivator.Extensions.DependencyInjection/ServiceCollectionExtensions.cs @@ -43,7 +43,7 @@ private static IBrighterBuilder AddServiceActivatorWithTransactionalMessaging(BuildDispatcher); - return ServiceCollectionExtensions.BrighterHandlerBuilder(services, options); + return ServiceCollectionExtensions.BrighterHandlerBuilder(services, options); } private static Dispatcher BuildDispatcher(IServiceProvider serviceProvider) diff --git a/src/Paramore.Brighter.ServiceActivator/ControlBusReceiverBuilder.cs b/src/Paramore.Brighter.ServiceActivator/ControlBusReceiverBuilder.cs index bfdb73ab3e..c9fb571f14 100644 --- a/src/Paramore.Brighter.ServiceActivator/ControlBusReceiverBuilder.cs +++ b/src/Paramore.Brighter.ServiceActivator/ControlBusReceiverBuilder.cs @@ -151,10 +151,13 @@ 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) + .Policies(policyRegistry).ExternalBusWithOutbox(externalBusConfiguration, outbox) .RequestContextFactory(new InMemoryRequestContextFactory()) .Build(); diff --git a/src/Paramore.Brighter/CommandProcessorBuilder.cs b/src/Paramore.Brighter/CommandProcessorBuilder.cs index e4155b1509..5e7ef6ec83 100644 --- a/src/Paramore.Brighter/CommandProcessorBuilder.cs +++ b/src/Paramore.Brighter/CommandProcessorBuilder.cs @@ -51,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 @@ -71,7 +71,6 @@ namespace Paramore.Brighter /// public class CommandProcessorBuilder : INeedAHandlers, INeedPolicy, INeedMessaging, INeedARequestContext, IAmACommandProcessorBuilder { - private IAmAProducerRegistry _producers; private IAmAMessageMapperRegistry _messageMapperRegistry; private IAmAMessageTransformerFactory _transformerFactory; private IAmARequestContextFactory _requestContextFactory; @@ -83,7 +82,9 @@ public class CommandProcessorBuilder : INeedAHandlers, INeedPolicy, INeedMessagi private bool _useExternalBus = false; private bool _useRequestReplyQueues = false; private IEnumerable _replySubscriptions; - private IAmAnExternalBusService _bus; + private IAmAnExternalBusService _inMemoryBus; + private IAmAnExternalBusService _dbBus; + private IAmAnExternalBusService _rpcBus; private CommandProcessorBuilder() { @@ -152,6 +153,47 @@ 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 + /// + /// If we use a request reply queue how do we subscribe to replies + /// + public INeedARequestContext ExternalBusNoCreate( + ExternalBusType busType, + IAmAnExternalBusService bus, + IEnumerable subscriptions = null + ) + { + switch (busType) + { + case ExternalBusType.None: + _useExternalBus = false; + break; + case ExternalBusType.InMemory: + _useExternalBus = true; + _inMemoryBus = bus; + break; + case ExternalBusType.Db: + _useExternalBus = true; + _dbBus = bus; + break; + case ExternalBusType.RPC: + _useExternalBus = true; + _rpcBus = bus; + _useRequestReplyQueues = true; + _replySubscriptions = subscriptions; + 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,13 +204,12 @@ public INeedMessaging DefaultPolicy() /// The Outbox. /// /// INeedARequestContext. - public INeedARequestContext ExternalBus( + public INeedARequestContext ExternalBusWithOutbox( ExternalBusConfiguration configuration, - IAmAnOutbox outbox + IAmAnOutbox outbox ) where TMessage : Message { _useExternalBus = true; - _producers = configuration.ProducerRegistry; _messageMapperRegistry = configuration.MessageMapperRegistry; _responseChannelFactory = configuration.ResponseChannelFactory; _transformerFactory = configuration.TransformerFactory; @@ -179,7 +220,37 @@ IAmAnOutbox outbox outbox, configuration.OutboxBulkChunkSize, configuration.OutboxWriteTimeout); - _bus = bus; + _dbBus = bus; + + 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 Task Queues configuration. + /// The Outbox. + /// + /// INeedARequestContext. + public INeedARequestContext ExternalBusInMemoryOutbox( + ExternalBusConfiguration configuration + ) + { + _useExternalBus = true; + _messageMapperRegistry = configuration.MessageMapperRegistry; + _responseChannelFactory = configuration.ResponseChannelFactory; + _transformerFactory = configuration.TransformerFactory; + + var bus = new ExternalBusServices( + configuration.ProducerRegistry, + _policyRegistry, + new InMemoryOutbox(), + configuration.OutboxBulkChunkSize, + configuration.OutboxWriteTimeout); + _inMemoryBus = bus; return this; } @@ -202,13 +273,12 @@ public INeedARequestContext NoExternalBus() /// public INeedARequestContext ExternalRPC( ExternalBusConfiguration configuration, - IAmAnOutbox outbox, + IAmAnOutbox outbox, IEnumerable subscriptions ) where TMessage : Message { _useRequestReplyQueues = true; _replySubscriptions = subscriptions; - _producers = configuration.ProducerRegistry; _messageMapperRegistry = configuration.MessageMapperRegistry; _responseChannelFactory = configuration.ResponseChannelFactory; _transformerFactory = configuration.TransformerFactory; @@ -219,7 +289,7 @@ IEnumerable subscriptions outbox, configuration.OutboxBulkChunkSize, configuration.OutboxWriteTimeout); - _bus = bus; + _rpcBus = bus; return this; } @@ -260,7 +330,7 @@ public CommandProcessor Build() requestContextFactory: _requestContextFactory, policyRegistry: _policyRegistry, mapperRegistry: _messageMapperRegistry, - bus: _bus, + bus: _dbBus ?? _inMemoryBus, featureSwitchRegistry: _featureSwitchRegistry, messageTransformerFactory: _transformerFactory ); @@ -273,7 +343,7 @@ public CommandProcessor Build() requestContextFactory: _requestContextFactory, policyRegistry: _policyRegistry, mapperRegistry: _messageMapperRegistry, - bus: _bus, + bus: _rpcBus, replySubscriptions: _replySubscriptions, responseChannelFactory: _responseChannelFactory); } @@ -336,17 +406,37 @@ public interface INeedMessaging /// The configuration. /// The outbox. /// INeedARequestContext. - INeedARequestContext ExternalBus( + INeedARequestContext ExternalBusWithOutbox( ExternalBusConfiguration configuration, - IAmAnOutbox outbox + IAmAnOutbox outbox ) where T : Message; + /// - /// We don't 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. /// - /// INeedARequestContext. - INeedARequestContext NoExternalBus(); + /// The type of Bus: In-memory, Db, or RPC + /// The bus that we wish to use + /// If we are RPC, any reply subscriptons + /// + INeedARequestContext ExternalBusNoCreate(ExternalBusType busType, IAmAnExternalBusService bus, IEnumerable subscriptions = null); + /// + /// 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 Task Queues configuration. + /// The Outbox. + /// + /// INeedARequestContext. + INeedARequestContext ExternalBusInMemoryOutbox( + ExternalBusConfiguration configuration + ); + /// /// We want to use RPC to send messages to another process /// @@ -355,9 +445,18 @@ IAmAnOutbox outbox /// Subscriptions for creating Reply queues INeedARequestContext ExternalRPC( ExternalBusConfiguration externalBusConfiguration, - IAmAnOutbox outboxSync, + IAmAnOutbox outboxSync, IEnumerable subscriptions - ) where T: Message; + ) where T: Message; + + /// + /// We don't send messages out of process + /// + /// INeedARequestContext. + INeedARequestContext NoExternalBus(); + + + } /// diff --git a/src/Paramore.Brighter/ControlBusSenderFactory.cs b/src/Paramore.Brighter/ControlBusSenderFactory.cs index 225c7c99b1..e78fe8439a 100644 --- a/src/Paramore.Brighter/ControlBusSenderFactory.cs +++ b/src/Paramore.Brighter/ControlBusSenderFactory.cs @@ -40,16 +40,18 @@ 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(); + var busConfiguration = new ExternalBusConfiguration(); + busConfiguration.ProducerRegistry = producerRegistry; + busConfiguration.MessageMapperRegistry = mapper; return new ControlBusSender(CommandProcessorBuilder.With() - .Handlers(new HandlerConfiguration()) - .DefaultPolicy() - .ExternalBus(new ExternalBusConfiguration(producerRegistry, mapper),outbox) + .Handlers(new HandlerConfiguration()) + .DefaultPolicy().ExternalBusWithOutbox(busConfiguration,outbox) .RequestContextFactory(new InMemoryRequestContextFactory()) .Build() ); diff --git a/src/Paramore.Brighter/ExternalBusConfiguration.cs b/src/Paramore.Brighter/ExternalBusConfiguration.cs index f089c1cca3..488ef0bbc6 100644 --- a/src/Paramore.Brighter/ExternalBusConfiguration.cs +++ b/src/Paramore.Brighter/ExternalBusConfiguration.cs @@ -36,41 +36,59 @@ public class ExternalBusConfiguration /// 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 OutboxWriteTimeout { 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 configuration of our inbox /// - public InboxConfiguration UseInbox { get;} + public InboxConfiguration UseInbox { get; set; } + + /// + /// Should we use an in-memory outbox + /// + public bool UseInMemoryOutbox { get; set; } + + /// + /// Initializes a new instance of the class. + /// + public ExternalBusConfiguration() + { + /*allows setting of properties one-by-one*/ + } /// @@ -78,13 +96,16 @@ public class ExternalBusConfiguration /// /// Clients for the external bus by topic they send to. The client details are specialised by transport /// The message mapper registry. + /// The outbox we wish to use for messaging /// 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, + public ExternalBusConfiguration( + IAmAProducerRegistry producerRegistry, IAmAMessageMapperRegistry messageMapperRegistry, + IAmAnOutbox outbox, int outboxBulkChunkSize = 100, int outboxWriteTimeout = 300, IAmAChannelFactory responseChannelFactory = null, @@ -93,6 +114,7 @@ public ExternalBusConfiguration(IAmAProducerRegistry producerRegistry, { ProducerRegistry = producerRegistry; MessageMapperRegistry = messageMapperRegistry; + Outbox = outbox; OutboxWriteTimeout = outboxWriteTimeout; ResponseChannelFactory = responseChannelFactory; UseInbox = useInbox; diff --git a/src/Paramore.Brighter/ExternalBusServices.cs b/src/Paramore.Brighter/ExternalBusServices.cs index e38cb7ab9f..c395397284 100644 --- a/src/Paramore.Brighter/ExternalBusServices.cs +++ b/src/Paramore.Brighter/ExternalBusServices.cs @@ -59,7 +59,7 @@ public class ExternalBusServices : IAmAnExternalBusServi public ExternalBusServices( IAmAProducerRegistry producerRegistry, IPolicyRegistry policyRegistry, - IAmAnOutbox outbox = null, + IAmAnOutbox outbox = null, int outboxBulkChunkSize = 100, int outboxTimeout = 300 ) @@ -68,7 +68,7 @@ public ExternalBusServices( 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 == null) outbox = new InMemoryOutbox() as IAmAnOutbox; if (outbox is IAmAnOutboxSync syncOutbox) OutBox = syncOutbox; if (outbox is IAmAnOutboxAsync asyncOutbox) AsyncOutbox = asyncOutbox; OutboxBulkChunkSize = outboxBulkChunkSize; diff --git a/src/Paramore.Brighter/ExternalBusType.cs b/src/Paramore.Brighter/ExternalBusType.cs new file mode 100644 index 0000000000..bd99f8f750 --- /dev/null +++ b/src/Paramore.Brighter/ExternalBusType.cs @@ -0,0 +1,35 @@ +#region Licence + +/* The MIT License (MIT) +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 +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 +{ + public enum ExternalBusType + { + None = 0, + InMemory = 1, + Db = 2, + RPC = 3 + } +} diff --git a/src/Paramore.Brighter/IAmABulkOutboxSync.cs b/src/Paramore.Brighter/IAmABulkOutboxSync.cs index 71da595793..ffa08f7a6c 100644 --- a/src/Paramore.Brighter/IAmABulkOutboxSync.cs +++ b/src/Paramore.Brighter/IAmABulkOutboxSync.cs @@ -33,7 +33,7 @@ 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 /// /// diff --git a/src/Paramore.Brighter/IAmAControlBusSenderFactory.cs b/src/Paramore.Brighter/IAmAControlBusSenderFactory.cs index 50b0f1ad19..0e96a8066b 100644 --- a/src/Paramore.Brighter/IAmAControlBusSenderFactory.cs +++ b/src/Paramore.Brighter/IAmAControlBusSenderFactory.cs @@ -39,7 +39,7 @@ public interface IAmAControlBusSenderFactory { /// 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/IAmAnOutbox.cs b/src/Paramore.Brighter/IAmAnOutbox.cs index 9716554c71..7333ced091 100644 --- a/src/Paramore.Brighter/IAmAnOutbox.cs +++ b/src/Paramore.Brighter/IAmAnOutbox.cs @@ -6,9 +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 /// - /// The type of the message - /// The type of the database transaction - public interface IAmAnOutbox where TMessage : Message + public interface IAmAnOutbox { } diff --git a/src/Paramore.Brighter/IAmAnOutboxAsync.cs b/src/Paramore.Brighter/IAmAnOutboxAsync.cs index 624443e291..82484d40ed 100644 --- a/src/Paramore.Brighter/IAmAnOutboxAsync.cs +++ b/src/Paramore.Brighter/IAmAnOutboxAsync.cs @@ -39,7 +39,7 @@ namespace Paramore.Brighter /// /// The type of message /// The type of transaction supported by the Outbox - public interface IAmAnOutboxAsync : IAmAnOutbox where T : Message + 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. diff --git a/src/Paramore.Brighter/IAmAnOutboxSync.cs b/src/Paramore.Brighter/IAmAnOutboxSync.cs index 6c80cad8ee..32251b2444 100644 --- a/src/Paramore.Brighter/IAmAnOutboxSync.cs +++ b/src/Paramore.Brighter/IAmAnOutboxSync.cs @@ -36,7 +36,7 @@ namespace Paramore.Brighter /// /// The message type /// The transaction type of the underlying Db - public interface IAmAnOutboxSync : IAmAnOutbox where T : Message + public interface IAmAnOutboxSync : IAmAnOutbox where T : Message { /// /// Adds the specified message. diff --git a/src/Paramore.Brighter/OutboxArchiver.cs b/src/Paramore.Brighter/OutboxArchiver.cs index 99bfe9c08c..800587cb56 100644 --- a/src/Paramore.Brighter/OutboxArchiver.cs +++ b/src/Paramore.Brighter/OutboxArchiver.cs @@ -18,7 +18,7 @@ public class OutboxArchiver where TMessage: Message private readonly IAmAnArchiveProvider _archiveProvider; private readonly ILogger _logger = ApplicationLogging.CreateLogger>(); - 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) 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 71bd7199dc..9777617909 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,19 @@ 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 - ) + .ExternalBusWithOutbox(busConfiguration, _outbox) .RequestContextFactory(new InMemoryRequestContextFactory()) .Build(); } 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 c6408c6e02..8f29690543 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; @@ -58,13 +59,18 @@ 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), - _fakeOutbox) + .ExternalBusWithOutbox(busConfiguration, _fakeOutbox) .RequestContextFactory(new InMemoryRequestContextFactory()) .Build(); } 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 03a8d26155..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 @@ -8,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.Extensions.Tests/TestDifferentSetups.cs b/tests/Paramore.Brighter.Extensions.Tests/TestDifferentSetups.cs index daef9fde30..917710b1d5 100644 --- a/tests/Paramore.Brighter.Extensions.Tests/TestDifferentSetups.cs +++ b/tests/Paramore.Brighter.Extensions.Tests/TestDifferentSetups.cs @@ -20,7 +20,7 @@ public void BasicSetup() { var serviceCollection = new ServiceCollection(); - serviceCollection.AddBrighter().AutoFromAssemblies(); + serviceCollection.AddBrighter().AutoFromAssemblies(); var serviceProvider = serviceCollection.BuildServiceProvider(); @@ -38,7 +38,7 @@ public void WithProducerRegistry() serviceCollection.AddSingleton(); serviceCollection - .AddBrighter() + .AddBrighter() .UseInMemoryOutbox() .UseExternalBus(producer, false) .AutoFromAssemblies(); @@ -70,7 +70,7 @@ public void WithCustomPolicy() serviceCollection - .AddBrighter(options => options.PolicyRegistry = policyRegistry) + .AddBrighter(options => options.PolicyRegistry = policyRegistry) .AutoFromAssemblies(); var serviceProvider = serviceCollection.BuildServiceProvider(); @@ -85,7 +85,7 @@ public void WithScopedLifetime() { var serviceCollection = new ServiceCollection(); - serviceCollection.AddBrighter(options => options.CommandProcessorLifetime = ServiceLifetime.Scoped + serviceCollection.AddBrighter(options => options.CommandProcessorLifetime = ServiceLifetime.Scoped ).AutoFromAssemblies(); Assert.Equal( ServiceLifetime.Scoped, serviceCollection.SingleOrDefault(x => x.ServiceType == typeof(IAmACommandProcessor))?.Lifetime); From a3395900fee4400ed6932a9686660264dad05c50 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Sun, 21 May 2023 18:13:37 +0200 Subject: [PATCH 54/89] Make derived outboxes call useexternaloutbox --- .../GreetingsSender.Web/Program.cs | 3 +- .../ASBTaskQueue/GreetingsWorker/Program.cs | 3 +- .../GreetingsWeb/Database/OutboxExtensions.cs | 7 +- .../GreetingsWeb/Database/OutboxExtensions.cs | 7 +- samples/WebAPI_EFCore/GreetingsWeb/Startup.cs | 5 +- .../Orders.API/Program.cs | 3 +- .../Extensions/BrighterExtensions.cs | 2 +- .../Orders.Worker/Program.cs | 3 +- .../MsSqlInbox.cs | 2 +- .../MsSqlMessageConsumer.cs | 2 +- .../MsSqlMessageProducer.cs | 2 +- ...ider.cs => MsSqlAuthConnectionProvider.cs} | 4 +- .../MsSqlOutbox.cs | 2 +- .../ServiceCollectionExtensions.cs | 58 +++++---------- .../ServiceCollectionExtensions.cs | 1 - .../ServiceCollectionExtensions.cs | 69 +++++++++-------- .../ServiceCollectionExtensions.cs | 74 +++++++------------ .../MsSqlTestHelper.cs | 4 +- 18 files changed, 99 insertions(+), 152 deletions(-) rename src/Paramore.Brighter.MsSql/{MsSqlSqlAuthConnectionProvider.cs => MsSqlAuthConnectionProvider.cs} (91%) diff --git a/samples/ASBTaskQueue/GreetingsSender.Web/Program.cs b/samples/ASBTaskQueue/GreetingsSender.Web/Program.cs index d3d6e15bd2..1297017e30 100644 --- a/samples/ASBTaskQueue/GreetingsSender.Web/Program.cs +++ b/samples/ASBTaskQueue/GreetingsSender.Web/Program.cs @@ -57,8 +57,7 @@ ) .Create() ) - .UseMsSqlOutbox(outboxConfig, typeof(MsSqlSqlAuthConnectionProvider)) - .UseMsSqlTransactionConnectionProvider(typeof(MsSqlEntityFrameworkCoreConnectionProvider)) + .UseMsSqlOutbox(outboxConfig, typeof(MsSqlEntityFrameworkCoreConnectionProvider)) .MapperRegistry(r => { r.Add(typeof(GreetingEvent), typeof(GreetingEventMessageMapper)); diff --git a/samples/ASBTaskQueue/GreetingsWorker/Program.cs b/samples/ASBTaskQueue/GreetingsWorker/Program.cs index f93a1d9e14..4d90deeb01 100644 --- a/samples/ASBTaskQueue/GreetingsWorker/Program.cs +++ b/samples/ASBTaskQueue/GreetingsWorker/Program.cs @@ -81,8 +81,7 @@ options.ChannelFactory = new AzureServiceBusChannelFactory(asbConsumerFactory); options.UseScoped = true; - }).UseMsSqlOutbox(outboxConfig, typeof(MsSqlSqlAuthConnectionProvider)) - .UseMsSqlTransactionConnectionProvider(typeof(MsSqlEntityFrameworkCoreConnectionProvider)) + }).UseMsSqlOutbox(outboxConfig, typeof(MsSqlEntityFrameworkCoreConnectionProvider)) .AutoFromAssemblies(); builder.Services.AddHostedService(); diff --git a/samples/WebAPI_Dapper/GreetingsWeb/Database/OutboxExtensions.cs b/samples/WebAPI_Dapper/GreetingsWeb/Database/OutboxExtensions.cs index 1e2603ae39..8a0b349693 100644 --- a/samples/WebAPI_Dapper/GreetingsWeb/Database/OutboxExtensions.cs +++ b/samples/WebAPI_Dapper/GreetingsWeb/Database/OutboxExtensions.cs @@ -49,12 +49,7 @@ private static void AddMySqlOutbox(IBrighterBuilder brighterBuilder, private static void AddSqliteOutBox(IBrighterBuilder brighterBuilder, RelationalDatabaseConfiguration configuration) { - brighterBuilder.UseSqliteOutbox( - configuration, - typeof(SqliteConnectionProvider), - ServiceLifetime.Singleton) - .UseSqliteTransactionConnectionProvider( - typeof(SqliteUnitOfWork), ServiceLifetime.Scoped) + brighterBuilder.UseSqliteOutbox(configuration, typeof(SqliteUnitOfWork)) .UseOutboxSweeper(options => { options.TimerInterval = 5; diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/OutboxExtensions.cs b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/OutboxExtensions.cs index 1e2603ae39..8a0b349693 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/OutboxExtensions.cs +++ b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/OutboxExtensions.cs @@ -49,12 +49,7 @@ private static void AddMySqlOutbox(IBrighterBuilder brighterBuilder, private static void AddSqliteOutBox(IBrighterBuilder brighterBuilder, RelationalDatabaseConfiguration configuration) { - brighterBuilder.UseSqliteOutbox( - configuration, - typeof(SqliteConnectionProvider), - ServiceLifetime.Singleton) - .UseSqliteTransactionConnectionProvider( - typeof(SqliteUnitOfWork), ServiceLifetime.Scoped) + brighterBuilder.UseSqliteOutbox(configuration, typeof(SqliteUnitOfWork)) .UseOutboxSweeper(options => { options.TimerInterval = 5; diff --git a/samples/WebAPI_EFCore/GreetingsWeb/Startup.cs b/samples/WebAPI_EFCore/GreetingsWeb/Startup.cs index 5715daa615..feb79507d5 100644 --- a/samples/WebAPI_EFCore/GreetingsWeb/Startup.cs +++ b/samples/WebAPI_EFCore/GreetingsWeb/Startup.cs @@ -136,8 +136,9 @@ private void ConfigureBrighter(IServiceCollection services) }} ).Create() ) - .UseSqliteOutbox(new RelationalDatabaseConfiguration(DbConnectionString(), _outBoxTableName), typeof(SqliteConnectionProvider), ServiceLifetime.Singleton) - .UseSqliteTransactionConnectionProvider(typeof(SqliteEntityFrameworkConnectionProvider), ServiceLifetime.Scoped) + .UseSqliteOutbox( + new RelationalDatabaseConfiguration(DbConnectionString(), _outBoxTableName), + typeof(SqliteEntityFrameworkConnectionProvider)) .UseOutboxSweeper(options => { options.TimerInterval = 5; diff --git a/samples/WebApiWithWorkerAndSweeper/Orders.API/Program.cs b/samples/WebApiWithWorkerAndSweeper/Orders.API/Program.cs index a40821b4c9..b6f7f605d9 100644 --- a/samples/WebApiWithWorkerAndSweeper/Orders.API/Program.cs +++ b/samples/WebApiWithWorkerAndSweeper/Orders.API/Program.cs @@ -47,8 +47,7 @@ ) .Create() ) - .UseMsSqlOutbox(outboxConfig, typeof(MsSqlSqlAuthConnectionProvider)) - .UseMsSqlTransactionConnectionProvider(typeof(SqlConnectionProvider)) + .UseMsSqlOutbox(outboxConfig, typeof(MsSqlSqlAuthUnitOfWork)) .AutoFromAssemblies(Assembly.GetAssembly(typeof(NewOrderVersionEvent))); diff --git a/samples/WebApiWithWorkerAndSweeper/Orders.Sweeper/Extensions/BrighterExtensions.cs b/samples/WebApiWithWorkerAndSweeper/Orders.Sweeper/Extensions/BrighterExtensions.cs index 0a9a8b82eb..e3b3ba65d1 100644 --- a/samples/WebApiWithWorkerAndSweeper/Orders.Sweeper/Extensions/BrighterExtensions.cs +++ b/samples/WebApiWithWorkerAndSweeper/Orders.Sweeper/Extensions/BrighterExtensions.cs @@ -49,7 +49,7 @@ public static WebApplicationBuilder AddBrighter(this WebApplicationBuilder build } else { - outboxType = typeof(MsSqlSqlAuthConnectionProvider); + outboxType = typeof(MsSqlAuthConnectionProvider); } builder.Services.AddBrighter() diff --git a/samples/WebApiWithWorkerAndSweeper/Orders.Worker/Program.cs b/samples/WebApiWithWorkerAndSweeper/Orders.Worker/Program.cs index 1e9868c4dd..22cdcef3ff 100644 --- a/samples/WebApiWithWorkerAndSweeper/Orders.Worker/Program.cs +++ b/samples/WebApiWithWorkerAndSweeper/Orders.Worker/Program.cs @@ -62,8 +62,7 @@ options.ChannelFactory = new AzureServiceBusChannelFactory(asbConsumerFactory); options.UseScoped = true; - }).UseMsSqlOutbox(outboxConfig, typeof(MsSqlSqlAuthConnectionProvider)) - .UseMsSqlTransactionConnectionProvider(typeof(SqlConnectionProvider)) + }).UseMsSqlOutbox(outboxConfig, typeof(MsSqlSqlAuthUnitOfWork)) .AutoFromAssemblies(Assembly.GetAssembly(typeof(CreateOrderCommand))); builder.Services.AddHostedService(); diff --git a/src/Paramore.Brighter.Inbox.MsSql/MsSqlInbox.cs b/src/Paramore.Brighter.Inbox.MsSql/MsSqlInbox.cs index 5f3c9a8ee5..5f583704ff 100644 --- a/src/Paramore.Brighter.Inbox.MsSql/MsSqlInbox.cs +++ b/src/Paramore.Brighter.Inbox.MsSql/MsSqlInbox.cs @@ -66,7 +66,7 @@ public MsSqlInbox(IAmARelationalDatabaseConfiguration configuration, IAmARelatio /// /// The configuration. public MsSqlInbox(IAmARelationalDatabaseConfiguration configuration) : this(configuration, - new MsSqlSqlAuthConnectionProvider(configuration)) + new MsSqlAuthConnectionProvider(configuration)) { } diff --git a/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlMessageConsumer.cs b/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlMessageConsumer.cs index 5a52c4302f..c1be691e0a 100644 --- a/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlMessageConsumer.cs +++ b/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlMessageConsumer.cs @@ -25,7 +25,7 @@ RelationalDbConnectionProvider connectionProvider public MsSqlMessageConsumer( RelationalDatabaseConfiguration msSqlConfiguration, - string topic) :this(msSqlConfiguration, topic, new MsSqlSqlAuthConnectionProvider(msSqlConfiguration)) + string topic) :this(msSqlConfiguration, topic, new MsSqlAuthConnectionProvider(msSqlConfiguration)) { } diff --git a/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlMessageProducer.cs b/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlMessageProducer.cs index 7bc54c6088..4abe9f2588 100644 --- a/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlMessageProducer.cs +++ b/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlMessageProducer.cs @@ -71,7 +71,7 @@ public MsSqlMessageProducer( public MsSqlMessageProducer( RelationalDatabaseConfiguration msSqlConfiguration, - Publication publication = null) : this(msSqlConfiguration, new MsSqlSqlAuthConnectionProvider(msSqlConfiguration), publication) + Publication publication = null) : this(msSqlConfiguration, new MsSqlAuthConnectionProvider(msSqlConfiguration), publication) { } diff --git a/src/Paramore.Brighter.MsSql/MsSqlSqlAuthConnectionProvider.cs b/src/Paramore.Brighter.MsSql/MsSqlAuthConnectionProvider.cs similarity index 91% rename from src/Paramore.Brighter.MsSql/MsSqlSqlAuthConnectionProvider.cs rename to src/Paramore.Brighter.MsSql/MsSqlAuthConnectionProvider.cs index 282a99a876..f7c5027fb7 100644 --- a/src/Paramore.Brighter.MsSql/MsSqlSqlAuthConnectionProvider.cs +++ b/src/Paramore.Brighter.MsSql/MsSqlAuthConnectionProvider.cs @@ -9,7 +9,7 @@ namespace Paramore.Brighter.MsSql /// /// A connection provider for Sqlite /// - public class MsSqlSqlAuthConnectionProvider : RelationalDbConnectionProvider + public class MsSqlAuthConnectionProvider : RelationalDbConnectionProvider { private readonly string _connectionString; @@ -17,7 +17,7 @@ public class MsSqlSqlAuthConnectionProvider : RelationalDbConnectionProvider /// Create a connection provider for MSSQL using a connection string for Db access /// /// The configuration for this database - public MsSqlSqlAuthConnectionProvider(IAmARelationalDatabaseConfiguration configuration) + public MsSqlAuthConnectionProvider(IAmARelationalDatabaseConfiguration configuration) { if (string.IsNullOrWhiteSpace(configuration?.ConnectionString)) throw new ArgumentNullException(nameof(configuration.ConnectionString)); diff --git a/src/Paramore.Brighter.Outbox.MsSql/MsSqlOutbox.cs b/src/Paramore.Brighter.Outbox.MsSql/MsSqlOutbox.cs index 6b3e4e92d0..20cbcb9f91 100644 --- a/src/Paramore.Brighter.Outbox.MsSql/MsSqlOutbox.cs +++ b/src/Paramore.Brighter.Outbox.MsSql/MsSqlOutbox.cs @@ -65,7 +65,7 @@ public MsSqlOutbox(IAmARelationalDatabaseConfiguration configuration, IAmARelati /// /// The configuration. public MsSqlOutbox(IAmARelationalDatabaseConfiguration configuration) : this(configuration, - new MsSqlSqlAuthConnectionProvider(configuration)) + new MsSqlAuthConnectionProvider(configuration)) { } diff --git a/src/Paramore.Brighter.Outbox.MsSql/ServiceCollectionExtensions.cs b/src/Paramore.Brighter.Outbox.MsSql/ServiceCollectionExtensions.cs index 5c6e7ee571..6df97c20e9 100644 --- a/src/Paramore.Brighter.Outbox.MsSql/ServiceCollectionExtensions.cs +++ b/src/Paramore.Brighter.Outbox.MsSql/ServiceCollectionExtensions.cs @@ -25,55 +25,37 @@ public static class ServiceCollectionExtensions /// -- 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, RelationalDatabaseConfiguration configuration, Type connectionProvider, ServiceLifetime serviceLifetime = ServiceLifetime.Singleton) + this IBrighterBuilder brighterBuilder, + RelationalDatabaseConfiguration configuration, + Type transactionProvider, + int outboxBulkChunkSize = 100, + int outboxTimeout = 300, + ServiceLifetime serviceLifetime = ServiceLifetime.Scoped + ) { - if (!typeof(IAmARelationalDbConnectionProvider).IsAssignableFrom(connectionProvider)) - throw new Exception($"Unable to register provider of type {connectionProvider.Name}. Class does not implement interface {nameof(IAmARelationalDbConnectionProvider)}."); + if (brighterBuilder is null) + throw new ArgumentNullException($"{nameof(brighterBuilder)} cannot be null.", nameof(brighterBuilder)); - brighterBuilder.Services.AddSingleton(configuration); - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmARelationalDbConnectionProvider), connectionProvider, serviceLifetime)); - - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmAnOutboxSync), BuildMsSqlOutbox, serviceLifetime)); - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmAnOutboxAsync), BuildMsSqlOutbox, 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 UseMsSqlTransactionConnectionProvider( - this IBrighterBuilder brighterBuilder, Type transactionProvider, - ServiceLifetime serviceLifetime = ServiceLifetime.Scoped) - { if (transactionProvider is null) throw new ArgumentNullException($"{nameof(transactionProvider)} cannot be null.", nameof(transactionProvider)); - if (!typeof(IAmABoxTransactionProvider).IsAssignableFrom(transactionProvider)) - throw new Exception($"Unable to register provider of type {transactionProvider.Name}. Class does not implement interface {nameof(IAmABoxTransactionProvider)}."); - if (!typeof(IAmATransactionConnectionProvider).IsAssignableFrom(transactionProvider)) throw new Exception($"Unable to register provider of type {transactionProvider.Name}. Class does not implement interface {nameof(IAmATransactionConnectionProvider)}."); + brighterBuilder.Services.AddSingleton(configuration); + brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmARelationalDbConnectionProvider), typeof(MsSqlAuthConnectionProvider), serviceLifetime)); + brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmABoxTransactionProvider), transactionProvider, serviceLifetime)); + + //register the combined interface just in case brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmATransactionConnectionProvider), transactionProvider, serviceLifetime)); - - return brighterBuilder; - } - private static MsSqlOutbox BuildMsSqlOutbox(IServiceProvider provider) - { - var connectionProvider = provider.GetService(); - var config = provider.GetService(); - - return new MsSqlOutbox(config, connectionProvider); + var outbox = new MsSqlOutbox(configuration, new MsSqlAuthConnectionProvider(configuration)); + + brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmAnOutboxSync), outbox)); + brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmAnOutboxAsync), outbox)); + + return brighterBuilder.UseExternalOutbox(outbox, outboxBulkChunkSize, outboxTimeout); } } } diff --git a/src/Paramore.Brighter.Outbox.MySql/ServiceCollectionExtensions.cs b/src/Paramore.Brighter.Outbox.MySql/ServiceCollectionExtensions.cs index 839d2626ce..440fb20276 100644 --- a/src/Paramore.Brighter.Outbox.MySql/ServiceCollectionExtensions.cs +++ b/src/Paramore.Brighter.Outbox.MySql/ServiceCollectionExtensions.cs @@ -43,7 +43,6 @@ public static IBrighterBuilder UseMySqlOutbox( if (!typeof(IAmATransactionConnectionProvider).IsAssignableFrom(transactionProvider)) throw new Exception($"Unable to register provider of type {transactionProvider.Name}. Class does not implement interface {nameof(IAmATransactionConnectionProvider)}."); - brighterBuilder.Services.AddSingleton(configuration); brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmARelationalDbConnectionProvider), typeof(MySqlConnectionProvider), serviceLifetime)); diff --git a/src/Paramore.Brighter.Outbox.PostgreSql/ServiceCollectionExtensions.cs b/src/Paramore.Brighter.Outbox.PostgreSql/ServiceCollectionExtensions.cs index fb5c07ab46..5982df1612 100644 --- a/src/Paramore.Brighter.Outbox.PostgreSql/ServiceCollectionExtensions.cs +++ b/src/Paramore.Brighter.Outbox.PostgreSql/ServiceCollectionExtensions.cs @@ -1,6 +1,7 @@ using System; using System.Data.Common; using Microsoft.Extensions.DependencyInjection; +using Npgsql; using Paramore.Brighter.Extensions.DependencyInjection; using Paramore.Brighter.PostgreSql; @@ -8,57 +9,55 @@ namespace Paramore.Brighter.Outbox.PostgreSql { public static class ServiceCollectionExtensions { - public static IBrighterBuilder UsePostgreSqlOutbox( - this IBrighterBuilder brighterBuilder, RelationalDatabaseConfiguration configuration, Type connectionProvider, ServiceLifetime serviceLifetime = ServiceLifetime.Singleton) - { - if (!typeof(IAmARelationalDbConnectionProvider).IsAssignableFrom(connectionProvider)) - throw new Exception($"Unable to register provider of type {connectionProvider.Name}. Class does not implement interface {nameof(IAmARelationalDbConnectionProvider)}."); - - brighterBuilder.Services.AddSingleton(configuration); - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmARelationalDbConnectionProvider), 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 + /// Use Sqlite for the Outbox /// - /// Allows extension method - /// What is the type of the connection provider. Must implement interface IPostgreSqlTransactionConnectionProvider - /// What is the lifetime of registered interfaces + /// Allows extension method syntax + /// The connection for the Db and name of the Outbox table + /// The provider of transactions for the outbox to participate in + /// What size should we chunk bulk work in + /// What is the timeout in ms for the Outbox + /// What is the lifetime of the services that we add (outbox always singleton) /// Allows fluent syntax - /// This is paired with Use Outbox (above) when required /// Registers the following + /// -- MySqlOutboxConfiguration: connection string and outbox name + /// -- IAmARelationalDbConnectionProvider: lets us get a connection for the outbox that matches the entity store + /// -- IAmAnOutbox: an outbox to store messages we want to send + /// -- IAmAnOutboxSync<Message, DbTransaction>>: an outbox to store messages we want to send + /// -- IAmAnOutboxAsync<Message, DbTransaction>: an outbox to store messages we want to send /// -- IAmABoxTransactionConnectionProvider: the provider of a connection for any existing transaction - public static IBrighterBuilder UsePostgreSqlTransactionConnectionProvider( + public static IBrighterBuilder UsePostgreSqlOutbox( this IBrighterBuilder brighterBuilder, + RelationalDatabaseConfiguration configuration, Type transactionProvider, - ServiceLifetime serviceLifetime = ServiceLifetime.Scoped) + int outboxBulkChunkSize = 100, + int outboxTimeout = 300, + ServiceLifetime serviceLifetime = ServiceLifetime.Scoped + ) { + if (brighterBuilder is null) + throw new ArgumentNullException($"{nameof(brighterBuilder)} cannot be null.", nameof(brighterBuilder)); + if (transactionProvider is null) throw new ArgumentNullException($"{nameof(transactionProvider)} cannot be null.", nameof(transactionProvider)); - if (!typeof(IAmABoxTransactionProvider).IsAssignableFrom(transactionProvider)) - throw new Exception($"Unable to register provider of type {transactionProvider.Name}. Class does not implement interface {nameof(IAmABoxTransactionProvider)}."); - if (!typeof(IAmATransactionConnectionProvider).IsAssignableFrom(transactionProvider)) throw new Exception($"Unable to register provider of type {transactionProvider.Name}. Class does not implement interface {nameof(IAmATransactionConnectionProvider)}."); - + + brighterBuilder.Services.AddSingleton(configuration); + brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmARelationalDbConnectionProvider), typeof(PostgreSqlNpgsqlConnectionProvider), serviceLifetime)); brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmABoxTransactionProvider), transactionProvider, serviceLifetime)); - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmATransactionConnectionProvider), transactionProvider, serviceLifetime)); - - return brighterBuilder; - - } - - private static PostgreSqlOutbox BuildPostgreSqlOutboxSync(IServiceProvider provider) - { - var config = provider.GetService(); - var connectionProvider = provider.GetService(); + + //register the combined interface just in case + brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmATransactionConnectionProvider), transactionProvider, serviceLifetime)); - return new PostgreSqlOutbox(config, connectionProvider); + var outbox = new PostgreSqlOutbox(configuration, new PostgreSqlNpgsqlConnectionProvider(configuration)); + + brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmAnOutboxSync), outbox)); + brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmAnOutboxAsync), outbox)); + + return brighterBuilder.UseExternalOutbox(outbox, outboxBulkChunkSize, outboxTimeout); } } } diff --git a/src/Paramore.Brighter.Outbox.Sqlite/ServiceCollectionExtensions.cs b/src/Paramore.Brighter.Outbox.Sqlite/ServiceCollectionExtensions.cs index a6e15bc625..d1ae624b55 100644 --- a/src/Paramore.Brighter.Outbox.Sqlite/ServiceCollectionExtensions.cs +++ b/src/Paramore.Brighter.Outbox.Sqlite/ServiceCollectionExtensions.cs @@ -13,70 +13,50 @@ public static class ServiceCollectionExtensions /// /// 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 + /// The provider of transactions for the outbox to participate in + /// What size should we chunk bulk work in + /// What is the timeout in ms for the Outbox + /// What is the lifetime of the services that we add (outbox always singleton) /// 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, RelationalDatabaseConfiguration configuration, Type connectionProvider, ServiceLifetime serviceLifetime = ServiceLifetime.Singleton) - { - if (!typeof(IAmARelationalDbConnectionProvider).IsAssignableFrom(connectionProvider)) - throw new Exception($"Unable to register provider of type {connectionProvider.Name}. Class does not implement interface {nameof(IAmARelationalDbConnectionProvider)}."); - - brighterBuilder.Services.AddSingleton(configuration); - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmARelationalDbConnectionProvider), 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 transaction provider - /// What is the lifetime of registered interfaces - /// Allows fluent syntax - /// This is paired with Use Outbox (above) when required - /// Registers the following + /// -- MySqlOutboxConfiguration: connection string and outbox name + /// -- IAmARelationalDbConnectionProvider: lets us get a connection for the outbox that matches the entity store + /// -- IAmAnOutbox: an outbox to store messages we want to send + /// -- IAmAnOutboxSync<Message, DbTransaction>>: an outbox to store messages we want to send + /// -- IAmAnOutboxAsync<Message, DbTransaction>: an outbox to store messages we want to send /// -- IAmABoxTransactionConnectionProvider: the provider of a connection for any existing transaction - public static IBrighterBuilder UseSqliteTransactionConnectionProvider( + public static IBrighterBuilder UseSqliteOutbox( this IBrighterBuilder brighterBuilder, + RelationalDatabaseConfiguration configuration, Type transactionProvider, - ServiceLifetime serviceLifetime = ServiceLifetime.Scoped) + int outboxBulkChunkSize = 100, + int outboxTimeout = 300, + ServiceLifetime serviceLifetime = ServiceLifetime.Scoped + ) { + if (brighterBuilder is null) + throw new ArgumentNullException($"{nameof(brighterBuilder)} cannot be null.", nameof(brighterBuilder)); + if (transactionProvider is null) throw new ArgumentNullException($"{nameof(transactionProvider)} cannot be null.", nameof(transactionProvider)); - if (!typeof(IAmABoxTransactionProvider).IsAssignableFrom(transactionProvider)) - throw new Exception($"Unable to register provider of type {transactionProvider.Name}. Class does not implement interface {nameof(IAmABoxTransactionProvider)}."); - if (!typeof(IAmATransactionConnectionProvider).IsAssignableFrom(transactionProvider)) throw new Exception($"Unable to register provider of type {transactionProvider.Name}. Class does not implement interface {nameof(IAmATransactionConnectionProvider)}."); - //register the specific interface + brighterBuilder.Services.AddSingleton(configuration); + brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmARelationalDbConnectionProvider), typeof(SqliteConnectionProvider), serviceLifetime)); + brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmABoxTransactionProvider), transactionProvider, serviceLifetime)); //register the combined interface just in case brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmATransactionConnectionProvider), transactionProvider, serviceLifetime)); - - return brighterBuilder; - } - private static SqliteOutbox BuildSqliteOutbox(IServiceProvider provider) - { - var config = provider.GetService(); - var connectionProvider = provider.GetService(); - - return new SqliteOutbox(config, connectionProvider); + var outbox = new SqliteOutbox(configuration, new SqliteConnectionProvider(configuration)); + + brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmAnOutboxSync), outbox)); + brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmAnOutboxAsync), outbox)); + + return brighterBuilder.UseExternalOutbox(outbox, outboxBulkChunkSize, outboxTimeout); } } } diff --git a/tests/Paramore.Brighter.MSSQL.Tests/MsSqlTestHelper.cs b/tests/Paramore.Brighter.MSSQL.Tests/MsSqlTestHelper.cs index eec22b2050..4f37c1480c 100644 --- a/tests/Paramore.Brighter.MSSQL.Tests/MsSqlTestHelper.cs +++ b/tests/Paramore.Brighter.MSSQL.Tests/MsSqlTestHelper.cs @@ -59,9 +59,9 @@ public MsSqlTestHelper(bool binaryMessagePayload = false) _tableName = $"test_{Guid.NewGuid()}"; _connectionProvider = - new MsSqlSqlAuthConnectionProvider(new RelationalDatabaseConfiguration(_sqlSettings.TestsBrighterConnectionString)); + new MsSqlAuthConnectionProvider(new RelationalDatabaseConfiguration(_sqlSettings.TestsBrighterConnectionString)); _masterConnectionProvider = - new MsSqlSqlAuthConnectionProvider(new RelationalDatabaseConfiguration(_sqlSettings.TestsMasterConnectionString)); + new MsSqlAuthConnectionProvider(new RelationalDatabaseConfiguration(_sqlSettings.TestsMasterConnectionString)); } public void CreateDatabase() From d7628a1cb2ff8e366b91c3412354d5ba3094c339 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Sat, 27 May 2023 16:08:48 +0200 Subject: [PATCH 55/89] safety dance. --- .../GreetingsReceiverConsole/Program.cs | 1 - .../GreetingsScopedReceiverConsole/Program.cs | 1 - .../GreetingsSender.Web/Program.cs | 30 +- .../ASBTaskQueue/GreetingsSender/Program.cs | 41 +- .../ASBTaskQueue/GreetingsWorker/Program.cs | 2 +- .../AWSTaskQueue/GreetingsPumper/Program.cs | 25 +- .../GreetingsReceiverConsole/Program.cs | 1 - .../AWSTaskQueue/GreetingsSender/Program.cs | 25 +- .../GreetingsReceiverConsole/Program.cs | 1 - .../ClaimCheck/GreetingsSender/Program.cs | 31 +- .../GreetingsReceiverConsole/Program.cs | 1 - .../Compression/GreetingsSender/Program.cs | 31 +- .../GreetingsSender/Program.cs | 41 +- .../KafkaTaskQueue/GreetingsSender/Program.cs | 43 +- .../CompetingReceiverConsole/Program.cs | 2 - .../CompetingSender/Program.cs | 12 +- .../GreetingsReceiverConsole/Program.cs | 1 - .../GreetingsSender/Program.cs | 14 +- samples/OpenTelemetry/Consumer/Program.cs | 8 +- samples/OpenTelemetry/Producer/Program.cs | 8 +- samples/OpenTelemetry/Sweeper/Program.cs | 6 +- .../GreetingsClient/Program.cs | 31 +- .../GreetingsServer/Program.cs | 30 +- .../GreetingsReceiverConsole/Program.cs | 1 - .../RMQTaskQueue/GreetingsSender/Program.cs | 48 +- .../GreetingsReceiver/Program.cs | 3 +- .../RedisTaskQueue/GreetingsSender/Program.cs | 26 +- samples/WebAPI_Dapper/GreetingsWeb/Startup.cs | 43 +- .../Orders.API/Program.cs | 21 +- .../Extensions/BrighterExtensions.cs | 16 +- .../Orders.Worker/Program.cs | 2 +- .../BrighterOptions.cs | 38 +- .../IBrighterBuilder.cs | 13 +- .../ServiceCollectionBrighterBuilder.cs | 74 +-- .../ServiceCollectionExtensions.cs | 497 +++++++++--------- .../ServiceCollectionExtensions.cs | 61 --- .../ServiceCollectionExtensions.cs | 62 --- .../ServiceCollectionExtensions.cs | 63 --- .../ServiceCollectionExtensions.cs | 62 --- .../ServiceActivatorOptions.cs | 30 ++ .../ServiceCollectionExtensions.cs | 8 - .../ControlBusReceiverBuilder.cs | 3 +- src/Paramore.Brighter/CommandProcessor.cs | 81 +-- .../CommandProcessorBuilder.cs | 242 +++------ .../CommittableTransactionProvider.cs | 57 ++ .../ControlBusSenderFactory.cs | 10 +- .../ExternalBusConfiguration.cs | 113 ++-- src/Paramore.Brighter/ExternalBusType.cs | 5 +- src/Paramore.Brighter/inboxConfiguration.cs | 11 + ...ipeline_With_Global_Inbox_And_Use_Inbox.cs | 6 +- ...e_With_Global_Inbox_And_Use_Inbox_Async.cs | 6 +- ...ling_A_Server_Via_The_Command_Processor.cs | 2 +- ...The_Command_Processor_With_No_In_Mapper.cs | 2 +- ...he_Command_Processor_With_No_Out_Mapper.cs | 2 +- ...a_The_Command_Processor_With_No_Timeout.cs | 2 +- ...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 + ..._Limit_Total_Writes_To_OutBox_In_Window.cs | 5 +- .../When_Posting_With_A_Default_Policy.cs | 5 +- .../TestDifferentSetups.cs | 26 +- .../TestTransform.cs | 20 +- 63 files changed, 985 insertions(+), 1118 deletions(-) delete mode 100644 src/Paramore.Brighter.Outbox.MsSql/ServiceCollectionExtensions.cs delete mode 100644 src/Paramore.Brighter.Outbox.MySql/ServiceCollectionExtensions.cs delete mode 100644 src/Paramore.Brighter.Outbox.PostgreSql/ServiceCollectionExtensions.cs delete mode 100644 src/Paramore.Brighter.Outbox.Sqlite/ServiceCollectionExtensions.cs create mode 100644 src/Paramore.Brighter/CommittableTransactionProvider.cs diff --git a/samples/ASBTaskQueue/GreetingsReceiverConsole/Program.cs b/samples/ASBTaskQueue/GreetingsReceiverConsole/Program.cs index 9bf4a641b0..5661437160 100644 --- a/samples/ASBTaskQueue/GreetingsReceiverConsole/Program.cs +++ b/samples/ASBTaskQueue/GreetingsReceiverConsole/Program.cs @@ -60,7 +60,6 @@ public async static Task Main(string[] args) options.ChannelFactory = new AzureServiceBusChannelFactory(asbConsumerFactory); options.UseScoped = false; }) - .UseInMemoryOutbox() .AutoFromAssemblies(); services.AddHostedService(); 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 1297017e30..c7063d4dbe 100644 --- a/samples/ASBTaskQueue/GreetingsSender.Web/Program.cs +++ b/samples/ASBTaskQueue/GreetingsSender.Web/Program.cs @@ -39,30 +39,34 @@ 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 => { 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(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 87e8346d1d..41d9427e4a 100644 --- a/samples/ASBTaskQueue/GreetingsSender/Program.cs +++ b/samples/ASBTaskQueue/GreetingsSender/Program.cs @@ -1,4 +1,5 @@ using System; +using System.Data.Common; using System.Transactions; using Greetings.Ports.Events; using Microsoft.Extensions.DependencyInjection; @@ -21,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 4d90deeb01..47ab174ba8 100644 --- a/samples/ASBTaskQueue/GreetingsWorker/Program.cs +++ b/samples/ASBTaskQueue/GreetingsWorker/Program.cs @@ -81,7 +81,7 @@ options.ChannelFactory = new AzureServiceBusChannelFactory(asbConsumerFactory); options.UseScoped = true; - }).UseMsSqlOutbox(outboxConfig, typeof(MsSqlEntityFrameworkCoreConnectionProvider)) + }) .AutoFromAssemblies(); builder.Services.AddHostedService(); diff --git a/samples/AWSTaskQueue/GreetingsPumper/Program.cs b/samples/AWSTaskQueue/GreetingsPumper/Program.cs index 4bda09325b..0478b71099 100644 --- a/samples/AWSTaskQueue/GreetingsPumper/Program.cs +++ b/samples/AWSTaskQueue/GreetingsPumper/Program.cs @@ -32,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 6960327480..fcbf665c6d 100644 --- a/samples/AWSTaskQueue/GreetingsSender/Program.cs +++ b/samples/AWSTaskQueue/GreetingsSender/Program.cs @@ -53,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 dc7e8d5ac5..67213ae9c3 100644 --- a/samples/AWSTransfomers/ClaimCheck/GreetingsSender/Program.cs +++ b/samples/AWSTransfomers/ClaimCheck/GreetingsSender/Program.cs @@ -58,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 5f5aa2c62a..6e79ab36e8 100644 --- a/samples/AWSTransfomers/Compression/GreetingsSender/Program.cs +++ b/samples/AWSTransfomers/Compression/GreetingsSender/Program.cs @@ -58,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/KafkaSchemaRegistry/GreetingsSender/Program.cs b/samples/KafkaSchemaRegistry/GreetingsSender/Program.cs index 3a63e0ddac..027005bbe3 100644 --- a/samples/KafkaSchemaRegistry/GreetingsSender/Program.cs +++ b/samples/KafkaSchemaRegistry/GreetingsSender/Program.cs @@ -92,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 349a3de3c3..0d0f5493ee 100644 --- a/samples/KafkaTaskQueue/GreetingsSender/Program.cs +++ b/samples/KafkaTaskQueue/GreetingsSender/Program.cs @@ -86,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 c15985be38..5825614e38 100644 --- a/samples/MsSqlMessagingGateway/CompetingReceiverConsole/Program.cs +++ b/samples/MsSqlMessagingGateway/CompetingReceiverConsole/Program.cs @@ -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 a197b6b026..4707c8aab7 100644 --- a/samples/MsSqlMessagingGateway/CompetingSender/Program.cs +++ b/samples/MsSqlMessagingGateway/CompetingSender/Program.cs @@ -42,12 +42,16 @@ private static async Task Main(string[] args) //create the gateway 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 e04760ae03..ec814790da 100644 --- a/samples/MsSqlMessagingGateway/GreetingsReceiverConsole/Program.cs +++ b/samples/MsSqlMessagingGateway/GreetingsReceiverConsole/Program.cs @@ -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 6f78ae6bbc..b561543d80 100644 --- a/samples/MsSqlMessagingGateway/GreetingsSender/Program.cs +++ b/samples/MsSqlMessagingGateway/GreetingsSender/Program.cs @@ -26,13 +26,17 @@ static void Main() 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 b12f779fb8..34cfe93c16 100644 --- a/samples/OpenTelemetry/Producer/Program.cs +++ b/samples/OpenTelemetry/Producer/Program.cs @@ -28,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 57595bb943..110c88bdad 100644 --- a/samples/OpenTelemetry/Sweeper/Program.cs +++ b/samples/OpenTelemetry/Sweeper/Program.cs @@ -33,8 +33,10 @@ }); builder.Services.AddBrighter() - .UseExternalBus(producerRegistry) - .UseInMemoryOutbox() + .UseExternalBus((configure) => + { + configure.ProducerRegistry = producerRegistry; + }) .UseOutboxSweeper(options => { options.TimerInterval = 5; diff --git a/samples/RMQRequestReply/GreetingsClient/Program.cs b/samples/RMQRequestReply/GreetingsClient/Program.cs index e9ad16815a..d58ac3765d 100644 --- a/samples/RMQRequestReply/GreetingsClient/Program.cs +++ b/samples/RMQRequestReply/GreetingsClient/Program.cs @@ -62,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 fd041dce5b..59fb8d000e 100644 --- a/samples/RMQTaskQueue/GreetingsSender/Program.cs +++ b/samples/RMQTaskQueue/GreetingsSender/Program.cs @@ -53,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 276d22292f..30d7cd2b74 100644 --- a/samples/RedisTaskQueue/GreetingsSender/Program.cs +++ b/samples/RedisTaskQueue/GreetingsSender/Program.cs @@ -91,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/GreetingsWeb/Startup.cs b/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs index cc2c93ccf1..57b60507a3 100644 --- a/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs +++ b/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs @@ -161,6 +161,25 @@ private void ConfigureBrighter(IServiceCollection services) ); services.AddSingleton(outboxConfiguration); + 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(); + services.AddBrighter(options => { //we want to use scoped, so make sure everything understands that which needs to @@ -169,25 +188,11 @@ 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() - ) + .UseExternalBus((configure) => + { + configure.ProducerRegistry = producerRegistry; + + }) //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). diff --git a/samples/WebApiWithWorkerAndSweeper/Orders.API/Program.cs b/samples/WebApiWithWorkerAndSweeper/Orders.API/Program.cs index b6f7f605d9..15df555915 100644 --- a/samples/WebApiWithWorkerAndSweeper/Orders.API/Program.cs +++ b/samples/WebApiWithWorkerAndSweeper/Orders.API/Program.cs @@ -1,4 +1,5 @@ using System.ComponentModel; +using System.Data.Common; using System.Reflection; using System.Text.Json; using System.Text.Json.Serialization; @@ -34,23 +35,27 @@ 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 => { 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(MsSqlSqlAuthUnitOfWork); + } ) - .UseMsSqlOutbox(outboxConfig, typeof(MsSqlSqlAuthUnitOfWork)) .AutoFromAssemblies(Assembly.GetAssembly(typeof(NewOrderVersionEvent))); - builder.Services.AddControllersWithViews().AddJsonOptions(options => { options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter(allowIntegerValues: false)); diff --git a/samples/WebApiWithWorkerAndSweeper/Orders.Sweeper/Extensions/BrighterExtensions.cs b/samples/WebApiWithWorkerAndSweeper/Orders.Sweeper/Extensions/BrighterExtensions.cs index e3b3ba65d1..93a99e2a37 100644 --- a/samples/WebApiWithWorkerAndSweeper/Orders.Sweeper/Extensions/BrighterExtensions.cs +++ b/samples/WebApiWithWorkerAndSweeper/Orders.Sweeper/Extensions/BrighterExtensions.cs @@ -34,27 +34,31 @@ public static WebApplicationBuilder AddBrighter(this WebApplicationBuilder build }, boxSettings.BatchChunkSize).Create(); var outboxSettings = new RelationalDatabaseConfiguration(boxSettings.ConnectionString, outBoxTableName: boxSettings.OutboxTableName); - Type outboxType; + 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(MsSqlAuthConnectionProvider); + transactionProviderType = typeof(MsSqlAuthConnectionProvider); } 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.Worker/Program.cs b/samples/WebApiWithWorkerAndSweeper/Orders.Worker/Program.cs index 22cdcef3ff..ed42d85cd5 100644 --- a/samples/WebApiWithWorkerAndSweeper/Orders.Worker/Program.cs +++ b/samples/WebApiWithWorkerAndSweeper/Orders.Worker/Program.cs @@ -62,7 +62,7 @@ options.ChannelFactory = new AzureServiceBusChannelFactory(asbConsumerFactory); options.UseScoped = true; - }).UseMsSqlOutbox(outboxConfig, typeof(MsSqlSqlAuthUnitOfWork)) + }) .AutoFromAssemblies(Assembly.GetAssembly(typeof(CreateOrderCommand))); builder.Services.AddHostedService(); diff --git a/src/Paramore.Brighter.Extensions.DependencyInjection/BrighterOptions.cs b/src/Paramore.Brighter.Extensions.DependencyInjection/BrighterOptions.cs index 77d5ec5b02..5366731b06 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,27 +7,33 @@ 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. /// public ServiceLifetime HandlerLifetime { get; set; } = ServiceLifetime.Transient; + /// + /// Configures the inbox to de-duplicate requests; will default to in-memory inbox if not set. + /// + public InboxConfiguration InboxConfiguration { get; set; } = new InboxConfiguration(); + /// /// Configures the lifetime of mappers. Defaults to Singleton /// 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,25 +46,33 @@ 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; } + /// + /// Configures the inbox to de-duplicate requests; will default to in-memory inbox if not set. + /// + InboxConfiguration InboxConfiguration { get; set; } + /// /// Configures the lifetime of mappers. /// @@ -77,5 +92,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/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 6824902273..4766868255 100644 --- a/src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionExtensions.cs @@ -23,10 +23,7 @@ THE SOFTWARE. */ #endregion - using System; -using System.Collections.Generic; -using System.Net.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; @@ -34,272 +31,312 @@ THE SOFTWARE. */ using Paramore.Brighter.Logging; using System.Text.Json; using System.Transactions; +using Polly.Registry; namespace Paramore.Brighter.Extensions.DependencyInjection { public static class ServiceCollectionExtensions { - private static IAmAnExternalBusService s_inMemoryBusService; - private static IAmAnExternalBusService s_dbBusService; - private static IAmAnExternalBusService s_rpcBusService; - private static BrighterOptions s_options; - private static ExternalBusConfiguration s_externalBusConfiguration = new ExternalBusConfiguration(); - /// /// 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 /// Thrown if we have no IoC provided ServiceCollection public static IBrighterBuilder AddBrighter( - this IServiceCollection services, + this IServiceCollection services, Action configure = null) { if (services == null) throw new ArgumentNullException(nameof(services)); - s_options = new BrighterOptions(); - configure?.Invoke(s_options); - services.TryAddSingleton(s_options); + var options = new BrighterOptions(); + configure?.Invoke(options); + services.TryAddSingleton(options); - return BrighterHandlerBuilder(services, s_options); + 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 - /// Allows you to configure how we build Brighter - /// A builder that can be used to populate the IoC container with handlers and mappers by inspection - /// - used by built in factory from CommandProcessor - public static IBrighterBuilder BrighterHandlerBuilder(IServiceCollection services, BrighterOptions options) + /// 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.TryAddSingleton(transformRegistry); - services.TryAdd(new ServiceDescriptor(typeof(IAmACommandProcessor), + services.TryAdd(new ServiceDescriptor(typeof(IAmACommandProcessor), (serviceProvider) => (IAmACommandProcessor)BuildCommandProcessor(serviceProvider), options.CommandProcessorLifetime)); var mapperRegistry = new ServiceCollectionMessageMapperRegistry(services, options.MapperLifetime); - services.TryAddSingleton(mapperRegistry); + services.TryAddSingleton(mapperRegistry); + + 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); + + if (options.FeatureSwitchRegistry != null) + services.TryAddSingleton(options.FeatureSwitchRegistry); + + //Add the policy registry + IPolicyRegistry policyRegistry; + if (options.PolicyRegistry == null) policyRegistry = new DefaultPolicy(); + else policyRegistry = AddDefaults(options.PolicyRegistry); + + return new ServiceCollectionBrighterBuilder( + services, + subscriberRegistry, + mapperRegistry, + transformRegistry, + policyRegistry + ); + } - return new ServiceCollectionBrighterBuilder(services, subscriberRegistry, mapperRegistry, - transformRegistry); + /// + /// 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. + /// NOTE: This external bus will use an in memory outbox to facilitate sending; this will not survive restarts + /// of the service. Use the alternative constructor if you wish to provide an outbox that supports transactions + /// that persist to a Db. + /// 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 + /// + /// - 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 allow chaining of requests + public static IBrighterBuilder UseExternalBus( + this IBrighterBuilder brighterBuilder, + Action configure = null) + { + return UseExternalBus(brighterBuilder, configure); } /// - /// 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 - /// Whe using a bulk outbox how should we chunk - /// When using an outbox how to long to timeout in ms - /// - public static IBrighterBuilder UseExternalOutbox( - this IBrighterBuilder brighterBuilder, - IAmAnOutbox outbox, - int outboxBulkChunkSize = 100, - int outboxTimeout = 300) - where TMessage : Message + /// 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) { - s_externalBusConfiguration.UseInMemoryOutbox = false; - - s_externalBusConfiguration.Outbox = outbox; - s_externalBusConfiguration.OutboxBulkChunkSize = outboxBulkChunkSize; - s_externalBusConfiguration.OutboxWriteTimeout = outboxTimeout; + if (brighterBuilder is null) + throw new ArgumentNullException($"{nameof(brighterBuilder)} cannot be null.", nameof(brighterBuilder)); - if (outbox is IAmAnOutboxSync) - { - brighterBuilder.Services.TryAddSingleton>((IAmAnOutboxSync) outbox); - } + var busConfiguration = new ExternalBusConfiguration(); + configure?.Invoke(busConfiguration); + brighterBuilder.Services.TryAddSingleton(busConfiguration); - if (outbox is IAmAnOutboxAsync) - { - brighterBuilder.Services.TryAddSingleton>((IAmAnOutboxAsync) outbox); - } - - var bus = new ExternalBusServices( - s_externalBusConfiguration.ProducerRegistry, - s_options.PolicyRegistry, - s_externalBusConfiguration.Outbox, - s_externalBusConfiguration.OutboxBulkChunkSize, - s_externalBusConfiguration.OutboxWriteTimeout); - - brighterBuilder.Services.TryAddSingleton(bus); + //default to using System Transactions if nothing provided, so we always technically can share the outbox transaction + Type transactionProvider = busConfiguration.TransactionProvider ?? typeof(CommittableTransactionProvider); - return brighterBuilder; + if (transactionProvider.GenericTypeArguments[0] != typeof(TTransaction)) + throw new ConfigurationException( + $"Unable to register provider of type {transactionProvider.Name}. Generic type argument does not match {typeof(TTransaction).Name}."); + + if (!typeof(IAmABoxTransactionProvider).IsAssignableFrom(transactionProvider)) + throw new ConfigurationException( + $"Unable to register provider of type {transactionProvider.Name}. Class does not implement interface {nameof(IAmABoxTransactionProvider)}."); + + brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmABoxTransactionProvider), + transactionProvider, serviceLifetime)); + + RegisterRelationalProviderServicesMaybe(brighterBuilder, transactionProvider, + serviceLifetime); + + return ExternalBusBuilder(brighterBuilder, busConfiguration); } - /// - /// 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 - /// The lifetime for the inbox, defaults to singleton - /// - public static IBrighterBuilder UseExternalInbox( - this IBrighterBuilder brighterBuilder, - IAmAnInbox inbox, - InboxConfiguration inboxConfiguration = null, - ServiceLifetime serviceLifetime = ServiceLifetime.Singleton) + private static INeedARequestContext AddEventBus(IServiceProvider provider, INeedMessaging messagingBuilder, + IUseRpc useRequestResponse) { - if (inbox is IAmAnInboxSync) - { - brighterBuilder.Services.TryAdd(new ServiceDescriptor(typeof(IAmAnInboxSync), _ => inbox, - serviceLifetime)); - } - - if (inbox is IAmAnInboxAsync) + var eventBus = provider.GetService(); + var eventBusConfiguration = provider.GetService(); + var messageMapperRegistry = provider.GetService(); + var messageTransformerFactory = provider.GetService(); + + INeedARequestContext ret = null; + if (eventBus == null) ret = messagingBuilder.NoExternalBus(); + if (eventBus != null && useRequestResponse.RPC) { - brighterBuilder.Services.TryAdd(new ServiceDescriptor(typeof(IAmAnInboxAsync), _ => inbox, - serviceLifetime)); + ret = messagingBuilder.ExternalBus( + useRequestResponse.RPC ? ExternalBusType.RPC : ExternalBusType.FireAndForget, + eventBus, + messageMapperRegistry, + messageTransformerFactory, + eventBusConfiguration.ResponseChannelFactory, + eventBusConfiguration.ReplyQueueSubscriptions); } - - if (inboxConfiguration != null) + else if (eventBus != null && useRequestResponse.RPC) { - brighterBuilder.Services.TryAddSingleton(inboxConfiguration); + ret = messagingBuilder.ExternalBus( + useRequestResponse.RPC ? ExternalBusType.RPC : ExternalBusType.FireAndForget, + eventBus, + messageMapperRegistry, + messageTransformerFactory, + eventBusConfiguration.ResponseChannelFactory, + eventBusConfiguration.ReplyQueueSubscriptions + ); } - return brighterBuilder; + return ret; } - /// - /// 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) { - s_externalBusConfiguration.UseInMemoryOutbox = true; - s_externalBusConfiguration.Outbox = new InMemoryOutbox(); - s_externalBusConfiguration.OutboxBulkChunkSize = 100; - s_externalBusConfiguration.OutboxWriteTimeout = 300; - - brighterBuilder.Services.TryAdd(new ServiceDescriptor(typeof(IAmAnOutboxSync), - _ => s_externalBusConfiguration.Outbox, ServiceLifetime.Singleton)); - brighterBuilder.Services.TryAdd(new ServiceDescriptor(typeof(IAmAnOutboxAsync), - _ => s_externalBusConfiguration.Outbox, ServiceLifetime.Singleton)); - - var bus = new ExternalBusServices( - s_externalBusConfiguration.ProducerRegistry, - s_options.PolicyRegistry, - s_externalBusConfiguration.Outbox, - s_externalBusConfiguration.OutboxBulkChunkSize, - s_externalBusConfiguration.OutboxWriteTimeout - ); - - brighterBuilder.Services.TryAddSingleton(bus); + 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 + ) { - s_externalBusConfiguration.ProducerRegistry = producerRegistry; - - brighterBuilder.Services.TryAddSingleton(producerRegistry); + if (externalBusConfiguration.ProducerRegistry == null) + throw new ConfigurationException("An external bus must have an IAmAProducerRegistry"); - if (useRequestResponseQueues) + var serviceCollection = brighterBuilder.Services; + + serviceCollection.TryAddSingleton(externalBusConfiguration); + serviceCollection.TryAddSingleton(externalBusConfiguration.ProducerRegistry); + + var outbox = externalBusConfiguration.Outbox; + if (outbox == null) { - brighterBuilder.Services.TryAddSingleton(new UseRpc(useRequestResponseQueues, replyQueueSubscriptions)); + outbox = new InMemoryOutbox(); + serviceCollection.TryAddSingleton>( + (IAmAnOutboxSync)outbox + ); + serviceCollection.TryAddSingleton>( + (IAmAnOutboxAsync)outbox + ); + } + else + { + if (outbox is IAmAnOutboxSync outboxSync) + serviceCollection.TryAddSingleton(outboxSync); + if (outbox is IAmAnOutboxAsync outboxAsync) + serviceCollection.TryAddSingleton(outboxAsync); } - return brighterBuilder; - } + if (externalBusConfiguration.UseRpc) + { + serviceCollection.TryAddSingleton(new UseRpc(externalBusConfiguration.UseRpc, + externalBusConfiguration.ReplyQueueSubscriptions)); + } + + var bus = new ExternalBusServices( + producerRegistry: externalBusConfiguration.ProducerRegistry, + policyRegistry: brighterBuilder.PolicyRegistry, + outbox: outbox, + outboxBulkChunkSize: externalBusConfiguration.OutboxBulkChunkSize, + outboxTimeout: 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 @@ -336,6 +373,22 @@ public static MessageMapperRegistry MessageMapperRegistry(IServiceProvider provi return messageMapperRegistry; } + private static void RegisterRelationalProviderServicesMaybe( + IBrighterBuilder brighterBuilder, + Type transactionProvider, ServiceLifetime serviceLifetime) + { + //not all box transaction providers are also relational connection providers + if (typeof(IAmARelationalDbConnectionProvider).IsAssignableFrom(transactionProvider)) + { + brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmARelationalDbConnectionProvider), + transactionProvider, serviceLifetime)); + + //register the combined interface just in case + brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmATransactionConnectionProvider), + transactionProvider, serviceLifetime)); + } + } + /// /// Creates transforms. Normally you don't need to call this, it is called by the builder for Brighter or /// the Service Activator @@ -347,59 +400,5 @@ public static ServiceProviderTransformerFactory TransformFactory(IServiceProvide { return new ServiceProviderTransformerFactory(provider); } - - private static object BuildCommandProcessor(IServiceProvider provider) - { - 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 producerRegistry = provider.GetService(); - - 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 = CreateEventBusMaybe(messagingBuilder, producerRegistry, useRequestResponse); - - var commandProcessor = ret - .RequestContextFactory(options.RequestContextFactory) - .Build(); - - return commandProcessor; - } - - private static INeedARequestContext CreateEventBusMaybe( - INeedMessaging messagingBuilder, - IAmAProducerRegistry producerRegistry, - IUseRpc useRequestResponse - ) - { - if (producerRegistry == null) - return messagingBuilder.NoExternalBus(); - - IAmAnExternalBusService bus; - var busType = s_externalBusConfiguration.UseInMemoryOutbox ? ExternalBusType.InMemory : ExternalBusType.Db; - bus = busType ==ExternalBusType.InMemory ? s_inMemoryBusService : s_dbBusService; - if (useRequestResponse.RPC) busType = ExternalBusType.RPC; - - return messagingBuilder.ExternalBusNoCreate(busType, bus); - - } } } diff --git a/src/Paramore.Brighter.Outbox.MsSql/ServiceCollectionExtensions.cs b/src/Paramore.Brighter.Outbox.MsSql/ServiceCollectionExtensions.cs deleted file mode 100644 index 6df97c20e9..0000000000 --- a/src/Paramore.Brighter.Outbox.MsSql/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System; -using System.Data.Common; -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, - RelationalDatabaseConfiguration configuration, - Type transactionProvider, - int outboxBulkChunkSize = 100, - int outboxTimeout = 300, - ServiceLifetime serviceLifetime = ServiceLifetime.Scoped - ) - { - if (brighterBuilder is null) - throw new ArgumentNullException($"{nameof(brighterBuilder)} cannot be null.", nameof(brighterBuilder)); - - if (transactionProvider is null) - throw new ArgumentNullException($"{nameof(transactionProvider)} cannot be null.", nameof(transactionProvider)); - - if (!typeof(IAmATransactionConnectionProvider).IsAssignableFrom(transactionProvider)) - throw new Exception($"Unable to register provider of type {transactionProvider.Name}. Class does not implement interface {nameof(IAmATransactionConnectionProvider)}."); - - brighterBuilder.Services.AddSingleton(configuration); - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmARelationalDbConnectionProvider), typeof(MsSqlAuthConnectionProvider), serviceLifetime)); - - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmABoxTransactionProvider), transactionProvider, serviceLifetime)); - - //register the combined interface just in case - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmATransactionConnectionProvider), transactionProvider, serviceLifetime)); - - var outbox = new MsSqlOutbox(configuration, new MsSqlAuthConnectionProvider(configuration)); - - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmAnOutboxSync), outbox)); - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmAnOutboxAsync), outbox)); - - return brighterBuilder.UseExternalOutbox(outbox, outboxBulkChunkSize, outboxTimeout); - } - } -} diff --git a/src/Paramore.Brighter.Outbox.MySql/ServiceCollectionExtensions.cs b/src/Paramore.Brighter.Outbox.MySql/ServiceCollectionExtensions.cs deleted file mode 100644 index 440fb20276..0000000000 --- a/src/Paramore.Brighter.Outbox.MySql/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System; -using System.Data.Common; -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 - /// The provider of transactions for the outbox to participate in - /// What size should we chunk bulk work in - /// What is the timeout in ms for the Outbox - /// What is the lifetime of the services that we add (outbox always singleton) - /// Allows fluent syntax - /// Registers the following - /// -- MySqlOutboxConfiguration: connection string and outbox name - /// -- IAmARelationalDbConnectionProvider: lets us get a connection for the outbox that matches the entity store - /// -- IAmAnOutbox: an outbox to store messages we want to send - /// -- IAmAnOutboxSync<Message, DbTransaction>>: an outbox to store messages we want to send - /// -- IAmAnOutboxAsync<Message, DbTransaction>: an outbox to store messages we want to send - /// -- IAmABoxTransactionConnectionProvider: the provider of a connection for any existing transaction - public static IBrighterBuilder UseMySqlOutbox( - this IBrighterBuilder brighterBuilder, - RelationalDatabaseConfiguration configuration, - Type transactionProvider, - int outboxBulkChunkSize = 100, - int outboxTimeout = 300, - ServiceLifetime serviceLifetime = ServiceLifetime.Scoped - ) - { - if (brighterBuilder is null) - throw new ArgumentNullException($"{nameof(brighterBuilder)} cannot be null.", nameof(brighterBuilder)); - - if (transactionProvider is null) - throw new ArgumentNullException($"{nameof(transactionProvider)} cannot be null.", nameof(transactionProvider)); - - if (!typeof(IAmATransactionConnectionProvider).IsAssignableFrom(transactionProvider)) - throw new Exception($"Unable to register provider of type {transactionProvider.Name}. Class does not implement interface {nameof(IAmATransactionConnectionProvider)}."); - - brighterBuilder.Services.AddSingleton(configuration); - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmARelationalDbConnectionProvider), typeof(MySqlConnectionProvider), serviceLifetime)); - - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmABoxTransactionProvider), transactionProvider, serviceLifetime)); - - //register the combined interface just in case - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmATransactionConnectionProvider), transactionProvider, serviceLifetime)); - - var outbox = new MySqlOutbox(configuration, new MySqlConnectionProvider(configuration)); - - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmAnOutboxSync), outbox)); - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmAnOutboxAsync), outbox)); - - return brighterBuilder.UseExternalOutbox(outbox, outboxBulkChunkSize, outboxTimeout); - } - } -} diff --git a/src/Paramore.Brighter.Outbox.PostgreSql/ServiceCollectionExtensions.cs b/src/Paramore.Brighter.Outbox.PostgreSql/ServiceCollectionExtensions.cs deleted file mode 100644 index 5982df1612..0000000000 --- a/src/Paramore.Brighter.Outbox.PostgreSql/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System; -using System.Data.Common; -using Microsoft.Extensions.DependencyInjection; -using Npgsql; -using Paramore.Brighter.Extensions.DependencyInjection; -using Paramore.Brighter.PostgreSql; - -namespace Paramore.Brighter.Outbox.PostgreSql -{ - public static class ServiceCollectionExtensions - { - /// - /// Use Sqlite for the Outbox - /// - /// Allows extension method syntax - /// The connection for the Db and name of the Outbox table - /// The provider of transactions for the outbox to participate in - /// What size should we chunk bulk work in - /// What is the timeout in ms for the Outbox - /// What is the lifetime of the services that we add (outbox always singleton) - /// Allows fluent syntax - /// Registers the following - /// -- MySqlOutboxConfiguration: connection string and outbox name - /// -- IAmARelationalDbConnectionProvider: lets us get a connection for the outbox that matches the entity store - /// -- IAmAnOutbox: an outbox to store messages we want to send - /// -- IAmAnOutboxSync<Message, DbTransaction>>: an outbox to store messages we want to send - /// -- IAmAnOutboxAsync<Message, DbTransaction>: an outbox to store messages we want to send - /// -- IAmABoxTransactionConnectionProvider: the provider of a connection for any existing transaction - public static IBrighterBuilder UsePostgreSqlOutbox( - this IBrighterBuilder brighterBuilder, - RelationalDatabaseConfiguration configuration, - Type transactionProvider, - int outboxBulkChunkSize = 100, - int outboxTimeout = 300, - ServiceLifetime serviceLifetime = ServiceLifetime.Scoped - ) - { - if (brighterBuilder is null) - throw new ArgumentNullException($"{nameof(brighterBuilder)} cannot be null.", nameof(brighterBuilder)); - - if (transactionProvider is null) - throw new ArgumentNullException($"{nameof(transactionProvider)} cannot be null.", nameof(transactionProvider)); - - if (!typeof(IAmATransactionConnectionProvider).IsAssignableFrom(transactionProvider)) - throw new Exception($"Unable to register provider of type {transactionProvider.Name}. Class does not implement interface {nameof(IAmATransactionConnectionProvider)}."); - - brighterBuilder.Services.AddSingleton(configuration); - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmARelationalDbConnectionProvider), typeof(PostgreSqlNpgsqlConnectionProvider), serviceLifetime)); - - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmABoxTransactionProvider), transactionProvider, serviceLifetime)); - - //register the combined interface just in case - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmATransactionConnectionProvider), transactionProvider, serviceLifetime)); - - var outbox = new PostgreSqlOutbox(configuration, new PostgreSqlNpgsqlConnectionProvider(configuration)); - - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmAnOutboxSync), outbox)); - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmAnOutboxAsync), outbox)); - - return brighterBuilder.UseExternalOutbox(outbox, outboxBulkChunkSize, outboxTimeout); - } - } -} diff --git a/src/Paramore.Brighter.Outbox.Sqlite/ServiceCollectionExtensions.cs b/src/Paramore.Brighter.Outbox.Sqlite/ServiceCollectionExtensions.cs deleted file mode 100644 index d1ae624b55..0000000000 --- a/src/Paramore.Brighter.Outbox.Sqlite/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System; -using System.Data.Common; -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 - /// The provider of transactions for the outbox to participate in - /// What size should we chunk bulk work in - /// What is the timeout in ms for the Outbox - /// What is the lifetime of the services that we add (outbox always singleton) - /// Allows fluent syntax - /// Registers the following - /// -- MySqlOutboxConfiguration: connection string and outbox name - /// -- IAmARelationalDbConnectionProvider: lets us get a connection for the outbox that matches the entity store - /// -- IAmAnOutbox: an outbox to store messages we want to send - /// -- IAmAnOutboxSync<Message, DbTransaction>>: an outbox to store messages we want to send - /// -- IAmAnOutboxAsync<Message, DbTransaction>: an outbox to store messages we want to send - /// -- IAmABoxTransactionConnectionProvider: the provider of a connection for any existing transaction - public static IBrighterBuilder UseSqliteOutbox( - this IBrighterBuilder brighterBuilder, - RelationalDatabaseConfiguration configuration, - Type transactionProvider, - int outboxBulkChunkSize = 100, - int outboxTimeout = 300, - ServiceLifetime serviceLifetime = ServiceLifetime.Scoped - ) - { - if (brighterBuilder is null) - throw new ArgumentNullException($"{nameof(brighterBuilder)} cannot be null.", nameof(brighterBuilder)); - - if (transactionProvider is null) - throw new ArgumentNullException($"{nameof(transactionProvider)} cannot be null.", nameof(transactionProvider)); - - if (!typeof(IAmATransactionConnectionProvider).IsAssignableFrom(transactionProvider)) - throw new Exception($"Unable to register provider of type {transactionProvider.Name}. Class does not implement interface {nameof(IAmATransactionConnectionProvider)}."); - - brighterBuilder.Services.AddSingleton(configuration); - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmARelationalDbConnectionProvider), typeof(SqliteConnectionProvider), serviceLifetime)); - - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmABoxTransactionProvider), transactionProvider, serviceLifetime)); - - //register the combined interface just in case - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmATransactionConnectionProvider), transactionProvider, serviceLifetime)); - - var outbox = new SqliteOutbox(configuration, new SqliteConnectionProvider(configuration)); - - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmAnOutboxSync), outbox)); - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmAnOutboxAsync), outbox)); - - return brighterBuilder.UseExternalOutbox(outbox, outboxBulkChunkSize, outboxTimeout); - } - } -} diff --git a/src/Paramore.Brighter.ServiceActivator.Extensions.DependencyInjection/ServiceActivatorOptions.cs b/src/Paramore.Brighter.ServiceActivator.Extensions.DependencyInjection/ServiceActivatorOptions.cs index 12d8c9c669..5c80ed9ef6 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 { + /// + /// 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; } + /// /// 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 2484edd50f..44ff45cdf0 100644 --- a/src/Paramore.Brighter.ServiceActivator.Extensions.DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Paramore.Brighter.ServiceActivator.Extensions.DependencyInjection/ServiceCollectionExtensions.cs @@ -26,12 +26,6 @@ public static class ServiceActivatorServiceCollectionExtensions public static IBrighterBuilder AddServiceActivator( this IServiceCollection services, Action configure = null) - { - return AddServiceActivatorWithTransactionalMessaging(services, configure); - } - - private static IBrighterBuilder AddServiceActivatorWithTransactionalMessaging(IServiceCollection services, - Action configure) { if (services == null) throw new ArgumentNullException(nameof(services)); @@ -39,8 +33,6 @@ private static IBrighterBuilder AddServiceActivatorWithTransactionalMessaging(options); - services.TryAddSingleton(BuildDispatcher); return ServiceCollectionExtensions.BrighterHandlerBuilder(services, options); diff --git a/src/Paramore.Brighter.ServiceActivator/ControlBusReceiverBuilder.cs b/src/Paramore.Brighter.ServiceActivator/ControlBusReceiverBuilder.cs index c9fb571f14..aaba2543ce 100644 --- a/src/Paramore.Brighter.ServiceActivator/ControlBusReceiverBuilder.cs +++ b/src/Paramore.Brighter.ServiceActivator/ControlBusReceiverBuilder.cs @@ -157,7 +157,8 @@ public Dispatcher Build(string hostName) commandProcessor = CommandProcessorBuilder.With() .Handlers(new HandlerConfiguration(subscriberRegistry, new ControlBusHandlerFactorySync(_dispatcher, () => commandProcessor))) - .Policies(policyRegistry).ExternalBusWithOutbox(externalBusConfiguration, outbox) + .Policies(policyRegistry) + .ExternalBusCreate(externalBusConfiguration, outbox, new CommittableTransactionProvider()) .RequestContextFactory(new InMemoryRequestContextFactory()) .Build(); diff --git a/src/Paramore.Brighter/CommandProcessor.cs b/src/Paramore.Brighter/CommandProcessor.cs index d57587c07b..989347bd66 100644 --- a/src/Paramore.Brighter/CommandProcessor.cs +++ b/src/Paramore.Brighter/CommandProcessor.cs @@ -103,8 +103,8 @@ public class CommandProcessor : IAmACommandProcessor 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. @@ -118,8 +118,7 @@ public CommandProcessor( IAmARequestContextFactory requestContextFactory, IPolicyRegistry policyRegistry, IAmAFeatureSwitchRegistry featureSwitchRegistry = null, - InboxConfiguration inboxConfiguration = null - ) + InboxConfiguration inboxConfiguration = null) { _subscriberRegistry = subscriberRegistry; @@ -140,36 +139,8 @@ 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 register of producers via whom we send messages over the external bus - /// 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 factory used to create a transformer pipeline for a message mapper - public CommandProcessor(IAmARequestContextFactory requestContextFactory, - IPolicyRegistry policyRegistry, - IAmAMessageMapperRegistry mapperRegistry, - IAmAnExternalBusService bus, - IAmAFeatureSwitchRegistry featureSwitchRegistry = null, - InboxConfiguration inboxConfiguration = null, - IAmAMessageTransformerFactory messageTransformerFactory = null) - { - _requestContextFactory = requestContextFactory; - _policyRegistry = policyRegistry; - _featureSwitchRegistry = featureSwitchRegistry; - _inboxConfiguration = inboxConfiguration; - _transformPipelineBuilder = new TransformPipelineBuilder(mapperRegistry, messageTransformerFactory); - - InitExtServiceBus(bus); - } - - /// - /// 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. @@ -177,65 +148,66 @@ public CommandProcessor(IAmARequestContextFactory requestContextFactory, /// The policy registry. /// The mapper registry. /// The external service bus that we want to send messages over - /// The Subscriptions for creating the reply queues /// 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 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, IAmAnExternalBusService bus, - IEnumerable replySubscriptions, IAmAFeatureSwitchRegistry featureSwitchRegistry = null, - IAmAChannelFactory responseChannelFactory = null, InboxConfiguration inboxConfiguration = null, - 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; _replySubscriptions = replySubscriptions; _transformPipelineBuilder = new TransformPipelineBuilder(mapperRegistry, messageTransformerFactory); - InitExtServiceBus(bus); - + 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 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 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, IAmAnExternalBusService bus, IAmAFeatureSwitchRegistry featureSwitchRegistry = null, InboxConfiguration inboxConfiguration = null, - IAmAMessageTransformerFactory messageTransformerFactory = null) - : this(subscriberRegistry, handlerFactory, requestContextFactory, policyRegistry, featureSwitchRegistry) + IAmAMessageTransformerFactory messageTransformerFactory = null, + IEnumerable replySubscriptions = null) { + _requestContextFactory = requestContextFactory; + _policyRegistry = policyRegistry; + _featureSwitchRegistry = featureSwitchRegistry; _inboxConfiguration = inboxConfiguration; _transformPipelineBuilder = new TransformPipelineBuilder(mapperRegistry, messageTransformerFactory); + _replySubscriptions = replySubscriptions; InitExtServiceBus(bus); } + /// /// Sends the specified command. We expect only one handler. The command is handled synchronously. /// @@ -287,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) diff --git a/src/Paramore.Brighter/CommandProcessorBuilder.cs b/src/Paramore.Brighter/CommandProcessorBuilder.cs index 5e7ef6ec83..d857583590 100644 --- a/src/Paramore.Brighter/CommandProcessorBuilder.cs +++ b/src/Paramore.Brighter/CommandProcessorBuilder.cs @@ -78,13 +78,11 @@ public class CommandProcessorBuilder : INeedAHandlers, INeedPolicy, INeedMessagi private IAmAHandlerFactory _handlerFactory; private IPolicyRegistry _policyRegistry; private IAmAFeatureSwitchRegistry _featureSwitchRegistry; - private IAmAChannelFactory _responseChannelFactory; - private bool _useExternalBus = false; + private IAmAnExternalBusService _bus = null; private bool _useRequestReplyQueues = false; + private IAmAChannelFactory _responseChannelFactory; private IEnumerable _replySubscriptions; - private IAmAnExternalBusService _inMemoryBus; - private IAmAnExternalBusService _dbBus; - private IAmAnExternalBusService _rpcBus; + private InboxConfiguration _inboxConfiguration; private CommandProcessorBuilder() { @@ -122,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; } @@ -159,33 +157,36 @@ public INeedMessaging DefaultPolicy() /// 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 ExternalBusNoCreate( + 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: - _useExternalBus = false; break; - case ExternalBusType.InMemory: - _useExternalBus = true; - _inMemoryBus = bus; - break; - case ExternalBusType.Db: - _useExternalBus = true; - _dbBus = bus; + case ExternalBusType.FireAndForget: + _bus = bus; break; case ExternalBusType.RPC: - _useExternalBus = true; - _rpcBus = bus; + _bus = bus; _useRequestReplyQueues = true; _replySubscriptions = subscriptions; + _responseChannelFactory = responseChannelFactory; break; default: throw new ConfigurationException("Bus type not supported"); @@ -193,7 +194,7 @@ public INeedARequestContext ExternalBusNoCreate( return this; } - + /// /// The wants to support or using an external bus. /// You need to provide a policy to specify how QoS issues, specifically or @@ -202,55 +203,23 @@ public INeedARequestContext ExternalBusNoCreate( /// /// The Task Queues configuration. /// The Outbox. - /// + /// /// INeedARequestContext. - public INeedARequestContext ExternalBusWithOutbox( + public INeedARequestContext ExternalBusCreate( ExternalBusConfiguration configuration, - IAmAnOutbox outbox - ) where TMessage : Message + IAmAnOutbox outbox, + IAmABoxTransactionProvider transactionProvider) { - _useExternalBus = true; _messageMapperRegistry = configuration.MessageMapperRegistry; _responseChannelFactory = configuration.ResponseChannelFactory; _transformerFactory = configuration.TransformerFactory; - var bus = new ExternalBusServices( + _bus = new ExternalBusServices( configuration.ProducerRegistry, _policyRegistry, outbox, configuration.OutboxBulkChunkSize, - configuration.OutboxWriteTimeout); - _dbBus = bus; - - 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 Task Queues configuration. - /// The Outbox. - /// - /// INeedARequestContext. - public INeedARequestContext ExternalBusInMemoryOutbox( - ExternalBusConfiguration configuration - ) - { - _useExternalBus = true; - _messageMapperRegistry = configuration.MessageMapperRegistry; - _responseChannelFactory = configuration.ResponseChannelFactory; - _transformerFactory = configuration.TransformerFactory; - - var bus = new ExternalBusServices( - configuration.ProducerRegistry, - _policyRegistry, - new InMemoryOutbox(), - configuration.OutboxBulkChunkSize, - configuration.OutboxWriteTimeout); - _inMemoryBus = bus; + configuration.OutboxTimeout); return this; } @@ -264,36 +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 - ) where TMessage : Message - { - _useRequestReplyQueues = true; - _replySubscriptions = subscriptions; - _messageMapperRegistry = configuration.MessageMapperRegistry; - _responseChannelFactory = configuration.ResponseChannelFactory; - _transformerFactory = configuration.TransformerFactory; - - var bus = new ExternalBusServices( - configuration.ProducerRegistry, - _policyRegistry, - outbox, - configuration.OutboxBulkChunkSize, - configuration.OutboxWriteTimeout); - _rpcBus = bus; - - return this; - } - /// /// The factory for used within the pipeline to pass information between steps. If you do not need to override /// provide . @@ -312,45 +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, - bus: _dbBus ?? _inMemoryBus, - featureSwitchRegistry: _featureSwitchRegistry, - messageTransformerFactory: _transformerFactory - ); - } - else if (_useRequestReplyQueues) - { - return new CommandProcessor( - subscriberRegistry: _registry, - handlerFactory: _handlerFactory, - requestContextFactory: _requestContextFactory, - policyRegistry: _policyRegistry, - mapperRegistry: _messageMapperRegistry, - bus: _rpcBus, - replySubscriptions: _replySubscriptions, + + 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); - } - else - { - throw new ConfigurationException("Unknown Command Processor Type"); - } + + throw new ConfigurationException( + "The configuration options chosen cannot be used to construct a command processor"); } } @@ -400,18 +324,6 @@ public interface INeedPolicy /// public interface INeedMessaging { - /// - /// Configure a task queue to send messages out of process - /// - /// The configuration. - /// The outbox. - /// INeedARequestContext. - INeedARequestContext ExternalBusWithOutbox( - ExternalBusConfiguration configuration, - IAmAnOutbox outbox - ) - where T : Message; - /// /// The wants to support or using an external bus. /// You need to provide a policy to specify how QoS issues, specifically or @@ -419,9 +331,25 @@ IAmAnOutbox outbox /// /// The type of Bus: In-memory, Db, or RPC /// The bus that we wish to use - /// If we are RPC, any reply subscriptons + /// 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 ExternalBusNoCreate(ExternalBusType busType, IAmAnExternalBusService bus, IEnumerable subscriptions = null); + INeedARequestContext ExternalBus( + ExternalBusType busType, + IAmAnExternalBusService bus, + IAmAMessageMapperRegistry messageMapperRegistry, + IAmAMessageTransformerFactory transformerFactory, + IAmAChannelFactory responseChannelFactory = null, + IEnumerable subscriptions = null + ); + + /// + /// We don't send messages out of process + /// + /// INeedARequestContext. + INeedARequestContext NoExternalBus(); /// /// The wants to support or using an external bus. @@ -431,32 +359,12 @@ IAmAnOutbox outbox /// /// The Task Queues configuration. /// The Outbox. - /// - /// INeedARequestContext. - INeedARequestContext ExternalBusInMemoryOutbox( - ExternalBusConfiguration configuration - ); - - /// - /// We want to use RPC to send messages to another process - /// - /// - /// The outbox - /// Subscriptions for creating Reply queues - INeedARequestContext ExternalRPC( - ExternalBusConfiguration externalBusConfiguration, - IAmAnOutbox outboxSync, - IEnumerable subscriptions - ) where T: Message; - - /// - /// We don't send messages out of process - /// + /// /// INeedARequestContext. - INeedARequestContext NoExternalBus(); - - - + 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..4861696d82 --- /dev/null +++ b/src/Paramore.Brighter/CommittableTransactionProvider.cs @@ -0,0 +1,57 @@ +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 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/ControlBusSenderFactory.cs b/src/Paramore.Brighter/ControlBusSenderFactory.cs index e78fe8439a..e69a4b2f2a 100644 --- a/src/Paramore.Brighter/ControlBusSenderFactory.cs +++ b/src/Paramore.Brighter/ControlBusSenderFactory.cs @@ -49,9 +49,15 @@ public IAmAControlBusSender Create(IAmAnOutbox outbox, IAmAProd var busConfiguration = new ExternalBusConfiguration(); busConfiguration.ProducerRegistry = producerRegistry; busConfiguration.MessageMapperRegistry = mapper; - return new ControlBusSender(CommandProcessorBuilder.With() + return new ControlBusSender( + CommandProcessorBuilder.With() .Handlers(new HandlerConfiguration()) - .DefaultPolicy().ExternalBusWithOutbox(busConfiguration,outbox) + .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 488ef0bbc6..b014ee4fda 100644 --- a/src/Paramore.Brighter/ExternalBusConfiguration.cs +++ b/src/Paramore.Brighter/ExternalBusConfiguration.cs @@ -22,15 +22,74 @@ 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 { /// /// The registry is a collection of producers @@ -58,7 +117,12 @@ public class ExternalBusConfiguration /// /// When do we timeout writing to the outbox /// - public int OutboxWriteTimeout { get; set; } + 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 @@ -71,55 +135,26 @@ public class ExternalBusConfiguration /// of your MessageMappers /// public IAmAMessageTransformerFactory TransformerFactory { get; set; } - + /// - /// The configuration of our inbox + /// The transaction provider for the outbox /// - public InboxConfiguration UseInbox { get; set; } + public Type TransactionProvider { get; set; } /// - /// Should we use an in-memory outbox + /// Do we want to support RPC on an external bus? /// - public bool UseInMemoryOutbox { get; set; } + public bool UseRpc { get; set; } /// /// Initializes a new instance of the class. /// public ExternalBusConfiguration() { - /*allows setting of properties one-by-one*/ - } - + /*allows setting of properties one-by-one, we default the required values here*/ - /// - /// 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 outbox we wish to use for messaging - /// 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, - IAmAnOutbox outbox, - int outboxBulkChunkSize = 100, - int outboxWriteTimeout = 300, - IAmAChannelFactory responseChannelFactory = null, - IAmAMessageTransformerFactory transformerFactory = null, - InboxConfiguration useInbox = null) - { - ProducerRegistry = producerRegistry; - MessageMapperRegistry = messageMapperRegistry; - Outbox = outbox; - OutboxWriteTimeout = outboxWriteTimeout; - ResponseChannelFactory = responseChannelFactory; - UseInbox = useInbox; - OutboxBulkChunkSize = outboxBulkChunkSize; - TransformerFactory = transformerFactory; + ProducerRegistry = new ProducerRegistry(new Dictionary()); } + } } diff --git a/src/Paramore.Brighter/ExternalBusType.cs b/src/Paramore.Brighter/ExternalBusType.cs index bd99f8f750..239595e877 100644 --- a/src/Paramore.Brighter/ExternalBusType.cs +++ b/src/Paramore.Brighter/ExternalBusType.cs @@ -28,8 +28,7 @@ namespace Paramore.Brighter public enum ExternalBusType { None = 0, - InMemory = 1, - Db = 2, - RPC = 3 + FireAndForget = 1, + RPC = 2 } } diff --git a/src/Paramore.Brighter/inboxConfiguration.cs b/src/Paramore.Brighter/inboxConfiguration.cs index 3eac967297..ae5b7f8f2e 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) { + if (inbox == null) Inbox = new InMemoryInbox(); + Scope = scope; Context = context; OnceOnly = onceOnly; 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_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 c93e5c334f..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 @@ -91,7 +91,7 @@ public CommandProcessorCallTests() policyRegistry, messageMapperRegistry, bus, - replySubs, + 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 311c3c0ecf..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 @@ -66,7 +66,7 @@ public CommandProcessorNoInMapperTests() policyRegistry, messageMapperRegistry, bus, - replySubscriptions, + replySubscriptions:replySubscriptions, responseChannelFactory: new InMemoryChannelFactory() ); 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 1ffca10f6f..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 @@ -69,7 +69,7 @@ public CommandProcessorMissingOutMapperTests() policyRegistry, messageMapperRegistry, bus, - replySubs, + 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 2da541c938..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 @@ -72,7 +72,7 @@ public CommandProcessorCallTestsNoTimeout() policyRegistry, messageMapperRegistry, bus, - replySubs, + replySubscriptions:replySubs, responseChannelFactory: new InMemoryChannelFactory()); PipelineBuilder.ClearPipelineCache(); 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_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 9777617909..d1c8f643b4 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 @@ -61,7 +61,10 @@ public PostFailureLimitCommandTests() _commandProcessor = CommandProcessorBuilder.With() .Handlers(new HandlerConfiguration(new SubscriberRegistry(), new EmptyHandlerFactorySync())) .DefaultPolicy() - .ExternalBusWithOutbox(busConfiguration, _outbox) + .ExternalBusCreate( + busConfiguration, + _outbox, + new CommittableTransactionProvider()) .RequestContextFactory(new InMemoryRequestContextFactory()) .Build(); } 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 8f29690543..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 @@ -70,7 +70,10 @@ public PostCommandTests() _commandProcessor = CommandProcessorBuilder.With() .Handlers(new HandlerConfiguration(new SubscriberRegistry(), new EmptyHandlerFactorySync())) .DefaultPolicy() - .ExternalBusWithOutbox(busConfiguration, _fakeOutbox) + .ExternalBusCreate( + busConfiguration, + _fakeOutbox, + new CommittableTransactionProvider()) .RequestContextFactory(new InMemoryRequestContextFactory()) .Build(); } diff --git a/tests/Paramore.Brighter.Extensions.Tests/TestDifferentSetups.cs b/tests/Paramore.Brighter.Extensions.Tests/TestDifferentSetups.cs index 917710b1d5..67305790ed 100644 --- a/tests/Paramore.Brighter.Extensions.Tests/TestDifferentSetups.cs +++ b/tests/Paramore.Brighter.Extensions.Tests/TestDifferentSetups.cs @@ -30,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; + }, null) .AutoFromAssemblies(); var serviceProvider = serviceCollection.BuildServiceProvider(); @@ -99,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; @@ -108,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; } } From ec10ef79a6feaee5d64c154b4e73e877eb1818dd Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Sat, 27 May 2023 21:35:58 +0100 Subject: [PATCH 56/89] Builds; tests and samples need debugging --- .../GreetingsWeb/Database/OutboxExtensions.cs | 27 ++---- samples/WebAPI_Dapper/GreetingsWeb/Startup.cs | 16 ++-- .../SalutationAnalytics/Program.cs | 51 ++++++----- .../GreetingsWeb/Database/OutboxExtensions.cs | 27 ++---- .../GreetingsWeb/Startup.cs | 67 +++++++------- .../SalutationAnalytics/Program.cs | 59 ++++++------ samples/WebAPI_Dynamo/GreetingsWeb/Startup.cs | 64 ++++++------- .../SalutationAnalytics/Program.cs | 53 ++++++----- samples/WebAPI_EFCore/GreetingsWeb/Startup.cs | 91 +++++++++++-------- .../SalutationAnalytics/Program.cs | 90 +++++++++--------- .../BrighterOptions.cs | 10 -- .../ServiceCollectionExtensions.cs | 5 - .../ServiceCollectionExtensions.cs | 10 +- .../TestDifferentSetups.cs | 2 +- 14 files changed, 292 insertions(+), 280 deletions(-) diff --git a/samples/WebAPI_Dapper/GreetingsWeb/Database/OutboxExtensions.cs b/samples/WebAPI_Dapper/GreetingsWeb/Database/OutboxExtensions.cs index 8a0b349693..ea94c7383f 100644 --- a/samples/WebAPI_Dapper/GreetingsWeb/Database/OutboxExtensions.cs +++ b/samples/WebAPI_Dapper/GreetingsWeb/Database/OutboxExtensions.cs @@ -13,48 +13,41 @@ namespace GreetingsWeb.Database { - public static class OutboxExtensions + public class OutboxExtensions { - public static IBrighterBuilder AddOutbox( - this IBrighterBuilder brighterBuilder, + public static (IAmAnOutbox, Type) MakeOutbox( IWebHostEnvironment env, DatabaseType databaseType, RelationalDatabaseConfiguration configuration) { + (IAmAnOutbox, Type) outbox; if (env.IsDevelopment()) { - AddSqliteOutBox(brighterBuilder, configuration); + outbox = MakeSqliteOutBox(configuration); } else { switch (databaseType) { case DatabaseType.MySql: - AddMySqlOutbox(brighterBuilder, configuration); + outbox = MakeMySqlOutbox(configuration); break; default: throw new InvalidOperationException("Unknown Db type for Outbox configuration"); } } - return brighterBuilder; + return outbox; } - private static void AddMySqlOutbox(IBrighterBuilder brighterBuilder, - RelationalDatabaseConfiguration configuration) + private static (IAmAnOutbox, Type) MakeMySqlOutbox(RelationalDatabaseConfiguration configuration) { - brighterBuilder.UseMySqlOutbox(configuration, typeof(MySqlUnitOfWork)).UseOutboxSweeper(); + return (new MySqlOutbox(configuration), typeof(MySqlUnitOfWork)); } - private static void AddSqliteOutBox(IBrighterBuilder brighterBuilder, - RelationalDatabaseConfiguration configuration) + private static (IAmAnOutbox, Type) MakeSqliteOutBox(RelationalDatabaseConfiguration configuration) { - brighterBuilder.UseSqliteOutbox(configuration, typeof(SqliteUnitOfWork)) - .UseOutboxSweeper(options => - { - options.TimerInterval = 5; - options.MinimumMessageAge = 5000; - }); + return (new SqliteOutbox(configuration), typeof(SqliteUnitOfWork)); } } } diff --git a/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs b/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs index 57b60507a3..3ef9916cbb 100644 --- a/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs +++ b/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs @@ -18,6 +18,7 @@ using Microsoft.OpenApi.Models; using Paramore.Brighter; using Paramore.Brighter.Extensions.DependencyInjection; +using Paramore.Brighter.Extensions.Hosting; using Paramore.Brighter.MessagingGateway.RMQ; using Paramore.Darker.AspNetCore; using Paramore.Darker.Policies; @@ -179,6 +180,9 @@ private void ConfigureBrighter(IServiceCollection services) } } ).Create(); + + (IAmAnOutbox outbox, Type transactionProvider) makeOutbox = + OutboxExtensions.MakeOutbox(_env, GetDatabaseType(), outboxConfiguration); services.AddBrighter(options => { @@ -191,13 +195,13 @@ private void ConfigureBrighter(IServiceCollection services) .UseExternalBus((configure) => { configure.ProducerRegistry = producerRegistry; - + configure.Outbox = makeOutbox.outbox; + configure.TransactionProvider = makeOutbox.transactionProvider; }) - //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(), outboxConfiguration) + .UseOutboxSweeper(options => { + options.TimerInterval = 5; + options.MinimumMessageAge = 5000; + }) .AutoFromAssemblies(typeof(AddPersonHandlerAsync).Assembly); } diff --git a/samples/WebAPI_Dapper/SalutationAnalytics/Program.cs b/samples/WebAPI_Dapper/SalutationAnalytics/Program.cs index aaaa7079a1..aa8f4be54b 100644 --- a/samples/WebAPI_Dapper/SalutationAnalytics/Program.cs +++ b/samples/WebAPI_Dapper/SalutationAnalytics/Program.cs @@ -86,6 +86,21 @@ private static void ConfigureBrighter(HostBuilderContext hostContext, IServiceCo 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; @@ -95,36 +110,24 @@ private static void ConfigureBrighter(HostBuilderContext hostContext, IServiceCo 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( - CreateInbox(hostContext, relationalDatabaseConfiguration), - new InboxConfiguration( - scope: InboxScope.Commands, - onceOnly: true, - actionOnExists: OnceOnlyAction.Throw - ) - ); + .UseExternalBus((config) => + { + config.ProducerRegistry = producerRegistry; + }) + .AutoFromAssemblies(); services.AddHostedService(); } diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/OutboxExtensions.cs b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/OutboxExtensions.cs index 8a0b349693..ea94c7383f 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/OutboxExtensions.cs +++ b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/OutboxExtensions.cs @@ -13,48 +13,41 @@ namespace GreetingsWeb.Database { - public static class OutboxExtensions + public class OutboxExtensions { - public static IBrighterBuilder AddOutbox( - this IBrighterBuilder brighterBuilder, + public static (IAmAnOutbox, Type) MakeOutbox( IWebHostEnvironment env, DatabaseType databaseType, RelationalDatabaseConfiguration configuration) { + (IAmAnOutbox, Type) outbox; if (env.IsDevelopment()) { - AddSqliteOutBox(brighterBuilder, configuration); + outbox = MakeSqliteOutBox(configuration); } else { switch (databaseType) { case DatabaseType.MySql: - AddMySqlOutbox(brighterBuilder, configuration); + outbox = MakeMySqlOutbox(configuration); break; default: throw new InvalidOperationException("Unknown Db type for Outbox configuration"); } } - return brighterBuilder; + return outbox; } - private static void AddMySqlOutbox(IBrighterBuilder brighterBuilder, - RelationalDatabaseConfiguration configuration) + private static (IAmAnOutbox, Type) MakeMySqlOutbox(RelationalDatabaseConfiguration configuration) { - brighterBuilder.UseMySqlOutbox(configuration, typeof(MySqlUnitOfWork)).UseOutboxSweeper(); + return (new MySqlOutbox(configuration), typeof(MySqlUnitOfWork)); } - private static void AddSqliteOutBox(IBrighterBuilder brighterBuilder, - RelationalDatabaseConfiguration configuration) + private static (IAmAnOutbox, Type) MakeSqliteOutBox(RelationalDatabaseConfiguration configuration) { - brighterBuilder.UseSqliteOutbox(configuration, typeof(SqliteUnitOfWork)) - .UseOutboxSweeper(options => - { - options.TimerInterval = 5; - options.MinimumMessageAge = 5000; - }); + return (new SqliteOutbox(configuration), typeof(SqliteUnitOfWork)); } } } diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs index eb540d99dc..8203de1585 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs +++ b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs @@ -19,6 +19,7 @@ using Microsoft.OpenApi.Models; using Paramore.Brighter; using Paramore.Brighter.Extensions.DependencyInjection; +using Paramore.Brighter.Extensions.Hosting; using Paramore.Brighter.MessagingGateway.Kafka; using Paramore.Brighter.MySql; using Paramore.Brighter.Sqlite; @@ -160,57 +161,57 @@ private void ConfigureBrighter(IServiceCollection services) { var outboxConfiguration = new RelationalDatabaseConfiguration( DbConnectionString(), - outBoxTableName:_outBoxTableName, + outBoxTableName: _outBoxTableName, //NOTE: With the Serdes serializer, if we don't use a binary payload, the payload will be corrupted binaryMessagePayload: true ); services.AddSingleton(outboxConfiguration); - - var schemaRegistryConfig = new SchemaRegistryConfig { Url = "http://localhost:8081"}; + + var schemaRegistryConfig = new SchemaRegistryConfig { Url = "http://localhost:8081" }; var cachedSchemaRegistryClient = new CachedSchemaRegistryClient(schemaRegistryConfig); services.AddSingleton(cachedSchemaRegistryClient); var kafkaConfiguration = new KafkaMessagingGatewayConfiguration { - Name = "paramore.brighter.greetingsender", - BootStrapServers = new[] { "localhost:9092" } + Name = "paramore.brighter.greetingsender", BootStrapServers = new[] { "localhost:9092" } }; + var producerRegistry = new KafkaProducerRegistryFactory( + kafkaConfiguration, + new KafkaPublication[] + { + new KafkaPublication + { + Topic = new RoutingKey("greeting.event"), + MessageSendMaxRetries = 3, + MessageTimeoutMs = 1000, + MaxInFlightRequestsPerConnection = 1, + MakeChannels = OnMissingChannel.Create + } + }) + .Create(); + + (IAmAnOutbox outbox, Type transactionProvider) makeOutbox = + OutboxExtensions.MakeOutbox(_env, GetDatabaseType(), outboxConfiguration); + services.AddBrighter(options => { //we want to use scoped, so make sure everything understands that which needs to options.HandlerLifetime = ServiceLifetime.Scoped; - options.ChannelFactory = new ChannelFactory(new KafkaMessageConsumerFactory(kafkaConfiguration)); options.CommandProcessorLifetime = ServiceLifetime.Scoped; options.MapperLifetime = ServiceLifetime.Singleton; options.PolicyRegistry = new GreetingsPolicy(); }) - .UseExternalBus( - new KafkaProducerRegistryFactory( - kafkaConfiguration, - new KafkaPublication[] - { - new KafkaPublication - { - Topic = new RoutingKey("greeting.event"), - MessageSendMaxRetries = 3, - MessageTimeoutMs = 1000, - MaxInFlightRequestsPerConnection = 1, - 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 - - * KAFKA and BINARY: Because Kafka here uses the Serdes serializer which sets magic bytes in the first - * five bytes of the the payload we have binary payload messages, and we need to set the binaryMessagePayload to true - *if we don't do that, and use text the database will corrupt the leading bytes - */ - .AddOutbox(_env, GetDatabaseType(), outboxConfiguration) + .UseExternalBus((configure) => + { + configure.ProducerRegistry = producerRegistry; + configure.Outbox = makeOutbox.outbox; + configure.TransactionProvider = makeOutbox.transactionProvider; + }) + .UseOutboxSweeper(options => + { + options.TimerInterval = 5; + options.MinimumMessageAge = 5000; + }) .AutoFromAssemblies(typeof(AddPersonHandlerAsync).Assembly); } diff --git a/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Program.cs b/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Program.cs index 2b358b130e..8a48162ad4 100644 --- a/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Program.cs +++ b/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Program.cs @@ -98,6 +98,25 @@ private static void ConfigureBrighter(HostBuilderContext hostContext, IServiceCo services.AddSingleton(relationalDatabaseConfiguration); + var producerRegistry = new KafkaProducerRegistryFactory( + new KafkaMessagingGatewayConfiguration + { + Name = "paramore.brighter.greetingsender", + BootStrapServers = new[] { "localhost:9092" } + }, + new KafkaPublication[] + { + new KafkaPublication + { + Topic = new RoutingKey("salutationrecieved.event"), + MessageSendMaxRetries = 3, + MessageTimeoutMs = 1000, + MaxInFlightRequestsPerConnection = 1, + MakeChannels = OnMissingChannel.Create + } + } + ).Create(); + services.AddServiceActivator(options => { options.Subscriptions = subscriptions; @@ -107,41 +126,23 @@ private static void ConfigureBrighter(HostBuilderContext hostContext, IServiceCo options.MapperLifetime = ServiceLifetime.Singleton; options.CommandProcessorLifetime = ServiceLifetime.Scoped; options.PolicyRegistry = new SalutationPolicy(); + options.InboxConfiguration = new InboxConfiguration( + ConfigureInbox(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 KafkaProducerRegistryFactory( - new KafkaMessagingGatewayConfiguration - { - Name = "paramore.brighter.greetingsender", - BootStrapServers = new[] { "localhost:9092" } - }, - new KafkaPublication[] - { - new KafkaPublication - { - Topic = new RoutingKey("salutationrecieved.event"), - MessageSendMaxRetries = 3, - MessageTimeoutMs = 1000, - MaxInFlightRequestsPerConnection = 1, - MakeChannels = OnMissingChannel.Create - } - } - ).Create() - ) - .AutoFromAssemblies() - .UseExternalInbox( - ConfigureInbox(hostContext, relationalDatabaseConfiguration), - new InboxConfiguration( - scope: InboxScope.Commands, - onceOnly: true, - actionOnExists: OnceOnlyAction.Throw - ) - ); + .UseExternalBus((configure) => + { + configure.ProducerRegistry = producerRegistry; + }) + .AutoFromAssemblies(); services.AddHostedService(); } diff --git a/samples/WebAPI_Dynamo/GreetingsWeb/Startup.cs b/samples/WebAPI_Dynamo/GreetingsWeb/Startup.cs index dbd582526a..5380a6e003 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,12 @@ 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.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 a8c5520bdd..dc9b32a998 100644 --- a/samples/WebAPI_Dynamo/SalutationAnalytics/Program.cs +++ b/samples/WebAPI_Dynamo/SalutationAnalytics/Program.cs @@ -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,26 @@ private static void ConfigureBrighter( options.MapperLifetime = ServiceLifetime.Singleton; options.CommandProcessorLifetime = ServiceLifetime.Scoped; options.PolicyRegistry = new SalutationPolicy(); + options.InboxConfiguration = new InboxConfiguration( + ConfigureInbox(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(dynamoDb), - new InboxConfiguration( - scope: InboxScope.Commands, - onceOnly: true, - actionOnExists: OnceOnlyAction.Throw - ) + .UseExternalBus((configure) => + { + configure.ProducerRegistry = producerRegistry; + configure.Outbox = ConfigureOutbox(awsCredentials, dynamoDb); + configure.TransactionProvider = typeof(DynamoDbUnitOfWork); + } ) - .UseExternalOutbox(ConfigureOutbox(awsCredentials, dynamoDb)) - .UseDynamoDbTransactionConnectionProvider(typeof(DynamoDbUnitOfWork), ServiceLifetime.Scoped); + .AutoFromAssemblies(); services.AddHostedService(); } diff --git a/samples/WebAPI_EFCore/GreetingsWeb/Startup.cs b/samples/WebAPI_EFCore/GreetingsWeb/Startup.cs index feb79507d5..9783798be9 100644 --- a/samples/WebAPI_EFCore/GreetingsWeb/Startup.cs +++ b/samples/WebAPI_EFCore/GreetingsWeb/Startup.cs @@ -111,7 +111,24 @@ private void ConfigureBrighter(IServiceCollection services) { if (_env.IsDevelopment()) { - services.AddBrighter(options => + 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(); + + services.AddBrighter(options => { //we want to use scoped, so make sure everything understands that which needs to options.HandlerLifetime = ServiceLifetime.Scoped; @@ -119,26 +136,15 @@ 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() + .UseExternalBus((configure) => + { + configure.ProducerRegistry = producerRegistry; + configure.Outbox = + new SqliteOutbox( + new RelationalDatabaseConfiguration(DbConnectionString(), _outBoxTableName)); + configure.TransactionProvider = typeof(SqliteUnitOfWork); + } ) - .UseSqliteOutbox( - new RelationalDatabaseConfiguration(DbConnectionString(), _outBoxTableName), - typeof(SqliteEntityFrameworkConnectionProvider)) .UseOutboxSweeper(options => { options.TimerInterval = 5; @@ -148,33 +154,38 @@ private void ConfigureBrighter(IServiceCollection services) } else { + IAmAProducerRegistry producerRegistry = 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(); + 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() + .UseExternalBus((config) => + { + config.ProducerRegistry = producerRegistry; + config.Outbox = + new MySqlOutbox( + new RelationalDatabaseConfiguration(DbConnectionString(), _outBoxTableName)); + config.TransactionProvider = typeof(MySqlUnitOfWork); + } ) - .UseMySqlOutbox( - new RelationalDatabaseConfiguration(DbConnectionString(), _outBoxTableName), - typeof(MySqlEntityFrameworkConnectionProvider) - ) .UseOutboxSweeper() .AutoFromAssemblies(); } diff --git a/samples/WebAPI_EFCore/SalutationAnalytics/Program.cs b/samples/WebAPI_EFCore/SalutationAnalytics/Program.cs index 6a7e895cfc..031c1bcc1d 100644 --- a/samples/WebAPI_EFCore/SalutationAnalytics/Program.cs +++ b/samples/WebAPI_EFCore/SalutationAnalytics/Program.cs @@ -41,7 +41,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); }) @@ -68,7 +70,8 @@ private static void ConfigureBrighter(HostBuilderContext hostContext, IServiceCo runAsync: true, 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"; @@ -81,6 +84,21 @@ private static void ConfigureBrighter(HostBuilderContext hostContext, IServiceCo 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; @@ -90,31 +108,18 @@ 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; + }) + .AutoFromAssemblies(); services.AddHostedService(); } @@ -124,7 +129,7 @@ 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,7 +139,7 @@ private static void ConfigureEFCore(HostBuilderContext hostContext, IServiceColl services.AddDbContext( builder => { - builder.UseSqlite(connectionString, + builder.UseSqlite(connectionString, optionsBuilder => { optionsBuilder.MigrationsAssembly("Salutations_SqliteMigrations"); @@ -143,16 +148,16 @@ private static void ConfigureEFCore(HostBuilderContext hostContext, IServiceColl } 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 +165,20 @@ private static IAmAnInbox ConfigureInbox(HostBuilderContext hostContext) { if (hostContext.HostingEnvironment.IsDevelopment()) { - return new SqliteInbox(new RelationalDatabaseConfiguration(DbConnectionString(hostContext), SchemaCreation.INBOX_TABLE_NAME)); + return new SqliteInbox(new RelationalDatabaseConfiguration(DbConnectionString(hostContext), + SchemaCreation.INBOX_TABLE_NAME)); } - return new MySqlInbox(new RelationalDatabaseConfiguration(DbConnectionString(hostContext), SchemaCreation.INBOX_TABLE_NAME)); + return new MySqlInbox(new RelationalDatabaseConfiguration(DbConnectionString(hostContext), + SchemaCreation.INBOX_TABLE_NAME)); } - + 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"); } - } } diff --git a/src/Paramore.Brighter.Extensions.DependencyInjection/BrighterOptions.cs b/src/Paramore.Brighter.Extensions.DependencyInjection/BrighterOptions.cs index 5366731b06..6b92ee7a2c 100644 --- a/src/Paramore.Brighter.Extensions.DependencyInjection/BrighterOptions.cs +++ b/src/Paramore.Brighter.Extensions.DependencyInjection/BrighterOptions.cs @@ -22,11 +22,6 @@ public class BrighterOptions : IBrighterOptions /// public ServiceLifetime HandlerLifetime { get; set; } = ServiceLifetime.Transient; - /// - /// Configures the inbox to de-duplicate requests; will default to in-memory inbox if not set. - /// - public InboxConfiguration InboxConfiguration { get; set; } = new InboxConfiguration(); - /// /// Configures the lifetime of mappers. Defaults to Singleton /// @@ -68,11 +63,6 @@ public interface IBrighterOptions /// ServiceLifetime HandlerLifetime { get; set; } - /// - /// Configures the inbox to de-duplicate requests; will default to in-memory inbox if not set. - /// - InboxConfiguration InboxConfiguration { get; set; } - /// /// Configures the lifetime of mappers. /// diff --git a/src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionExtensions.cs b/src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionExtensions.cs index 4766868255..03a20b672e 100644 --- a/src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionExtensions.cs @@ -95,11 +95,6 @@ public static IBrighterBuilder BrighterHandlerBuilder(IServiceCollection service var mapperRegistry = new ServiceCollectionMessageMapperRegistry(services, options.MapperLifetime); services.TryAddSingleton(mapperRegistry); - 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); - if (options.FeatureSwitchRegistry != null) services.TryAddSingleton(options.FeatureSwitchRegistry); diff --git a/src/Paramore.Brighter.ServiceActivator.Extensions.DependencyInjection/ServiceCollectionExtensions.cs b/src/Paramore.Brighter.ServiceActivator.Extensions.DependencyInjection/ServiceCollectionExtensions.cs index 44ff45cdf0..7ed56b02d4 100644 --- a/src/Paramore.Brighter.ServiceActivator.Extensions.DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Paramore.Brighter.ServiceActivator.Extensions.DependencyInjection/ServiceCollectionExtensions.cs @@ -33,7 +33,15 @@ public static IBrighterBuilder AddServiceActivator( var options = new ServiceActivatorOptions(); configure?.Invoke(options); services.TryAddSingleton(options); - services.TryAddSingleton(BuildDispatcher); + + 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); } diff --git a/tests/Paramore.Brighter.Extensions.Tests/TestDifferentSetups.cs b/tests/Paramore.Brighter.Extensions.Tests/TestDifferentSetups.cs index 67305790ed..7d83c4448f 100644 --- a/tests/Paramore.Brighter.Extensions.Tests/TestDifferentSetups.cs +++ b/tests/Paramore.Brighter.Extensions.Tests/TestDifferentSetups.cs @@ -42,7 +42,7 @@ public void WithExternalBus() .UseExternalBus((config) => { config.ProducerRegistry = producerRegistry; - }, null) + }) .AutoFromAssemblies(); var serviceProvider = serviceCollection.BuildServiceProvider(); From 2c087eea569a1c789631ab2c1f9fc9834cf231a6 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Sat, 27 May 2023 21:49:46 +0100 Subject: [PATCH 57/89] Ensure that we use the transaction on an external bus with an outbox --- samples/ASBTaskQueue/GreetingsSender.Web/Program.cs | 2 +- samples/WebAPI_Dapper/GreetingsWeb/Startup.cs | 2 +- samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs | 2 +- .../Orders.Sweeper/Extensions/BrighterExtensions.cs | 3 ++- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/samples/ASBTaskQueue/GreetingsSender.Web/Program.cs b/samples/ASBTaskQueue/GreetingsSender.Web/Program.cs index c7063d4dbe..adae737e2c 100644 --- a/samples/ASBTaskQueue/GreetingsSender.Web/Program.cs +++ b/samples/ASBTaskQueue/GreetingsSender.Web/Program.cs @@ -62,7 +62,7 @@ r.Add(typeof(GreetingAsyncEvent), typeof(GreetingEventAsyncMessageMapper)); r.Add(typeof(AddGreetingCommand), typeof(AddGreetingMessageMapper)); }) - .UseExternalBus((configure) => + .UseExternalBus((configure) => { configure.ProducerRegistry = producerRegistry; configure.Outbox = new MsSqlOutbox(outboxConfig); diff --git a/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs b/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs index 3ef9916cbb..d751a47406 100644 --- a/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs +++ b/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs @@ -192,7 +192,7 @@ private void ConfigureBrighter(IServiceCollection services) options.MapperLifetime = ServiceLifetime.Singleton; options.PolicyRegistry = new GreetingsPolicy(); }) - .UseExternalBus((configure) => + .UseExternalBus((configure) => { configure.ProducerRegistry = producerRegistry; configure.Outbox = makeOutbox.outbox; diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs index 8203de1585..ba30696750 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs +++ b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs @@ -201,7 +201,7 @@ private void ConfigureBrighter(IServiceCollection services) options.MapperLifetime = ServiceLifetime.Singleton; options.PolicyRegistry = new GreetingsPolicy(); }) - .UseExternalBus((configure) => + .UseExternalBus((configure) => { configure.ProducerRegistry = producerRegistry; configure.Outbox = makeOutbox.outbox; diff --git a/samples/WebApiWithWorkerAndSweeper/Orders.Sweeper/Extensions/BrighterExtensions.cs b/samples/WebApiWithWorkerAndSweeper/Orders.Sweeper/Extensions/BrighterExtensions.cs index 93a99e2a37..fff00ad266 100644 --- a/samples/WebApiWithWorkerAndSweeper/Orders.Sweeper/Extensions/BrighterExtensions.cs +++ b/samples/WebApiWithWorkerAndSweeper/Orders.Sweeper/Extensions/BrighterExtensions.cs @@ -1,3 +1,4 @@ +using System.Data.Common; using System.Transactions; using Azure.Identity; using Orders.Sweeper.Settings; @@ -53,7 +54,7 @@ public static WebApplicationBuilder AddBrighter(this WebApplicationBuilder build } builder.Services.AddBrighter() - .UseExternalBus((configure) => + .UseExternalBus((configure) => { configure.ProducerRegistry = producerRegistry; configure.Outbox = new MsSqlOutbox(outboxSettings); From 97be4fc437ee83186a8d1e3b5612d8e126479a57 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Sun, 28 May 2023 13:43:13 +0100 Subject: [PATCH 58/89] use reflection to remove need to pass generic type parameters --- .../GreetingsSender.Web/Program.cs | 2 +- samples/WebAPI_Dapper/GreetingsWeb/Startup.cs | 2 +- .../GreetingsWeb/Startup.cs | 2 +- samples/WebAPI_Dynamo/GreetingsWeb/Startup.cs | 2 +- .../SalutationAnalytics/Program.cs | 2 +- samples/WebAPI_EFCore/GreetingsWeb/Startup.cs | 4 +- .../Orders.API/Program.cs | 2 +- .../Extensions/BrighterExtensions.cs | 2 +- .../ServiceCollectionExtensions.cs | 122 ++++++++---------- .../ServiceActivatorOptions.cs | 6 +- .../ServiceCollectionExtensions.cs | 5 +- .../TestDifferentSetups.cs | 2 +- 12 files changed, 72 insertions(+), 81 deletions(-) diff --git a/samples/ASBTaskQueue/GreetingsSender.Web/Program.cs b/samples/ASBTaskQueue/GreetingsSender.Web/Program.cs index adae737e2c..c7063d4dbe 100644 --- a/samples/ASBTaskQueue/GreetingsSender.Web/Program.cs +++ b/samples/ASBTaskQueue/GreetingsSender.Web/Program.cs @@ -62,7 +62,7 @@ r.Add(typeof(GreetingAsyncEvent), typeof(GreetingEventAsyncMessageMapper)); r.Add(typeof(AddGreetingCommand), typeof(AddGreetingMessageMapper)); }) - .UseExternalBus((configure) => + .UseExternalBus((configure) => { configure.ProducerRegistry = producerRegistry; configure.Outbox = new MsSqlOutbox(outboxConfig); diff --git a/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs b/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs index d751a47406..3ef9916cbb 100644 --- a/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs +++ b/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs @@ -192,7 +192,7 @@ private void ConfigureBrighter(IServiceCollection services) options.MapperLifetime = ServiceLifetime.Singleton; options.PolicyRegistry = new GreetingsPolicy(); }) - .UseExternalBus((configure) => + .UseExternalBus((configure) => { configure.ProducerRegistry = producerRegistry; configure.Outbox = makeOutbox.outbox; diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs index ba30696750..8203de1585 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs +++ b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs @@ -201,7 +201,7 @@ private void ConfigureBrighter(IServiceCollection services) options.MapperLifetime = ServiceLifetime.Singleton; options.PolicyRegistry = new GreetingsPolicy(); }) - .UseExternalBus((configure) => + .UseExternalBus((configure) => { configure.ProducerRegistry = producerRegistry; configure.Outbox = makeOutbox.outbox; diff --git a/samples/WebAPI_Dynamo/GreetingsWeb/Startup.cs b/samples/WebAPI_Dynamo/GreetingsWeb/Startup.cs index 5380a6e003..153780462e 100644 --- a/samples/WebAPI_Dynamo/GreetingsWeb/Startup.cs +++ b/samples/WebAPI_Dynamo/GreetingsWeb/Startup.cs @@ -187,7 +187,7 @@ private void ConfigureBrighter(IServiceCollection services) options.MapperLifetime = ServiceLifetime.Singleton; options.PolicyRegistry = new GreetingsPolicy(); }) - .UseExternalBus((configure) => + .UseExternalBus((configure) => { configure.ProducerRegistry = producerRegistry; configure.Outbox = new DynamoDbOutbox(_client, new DynamoDbConfiguration()); diff --git a/samples/WebAPI_Dynamo/SalutationAnalytics/Program.cs b/samples/WebAPI_Dynamo/SalutationAnalytics/Program.cs index dc9b32a998..fe7c780cb1 100644 --- a/samples/WebAPI_Dynamo/SalutationAnalytics/Program.cs +++ b/samples/WebAPI_Dynamo/SalutationAnalytics/Program.cs @@ -120,7 +120,7 @@ private static void ConfigureBrighter( //We don't strictly need this, but added as an example options.PropertyNameCaseInsensitive = true; }) - .UseExternalBus((configure) => + .UseExternalBus((configure) => { configure.ProducerRegistry = producerRegistry; configure.Outbox = ConfigureOutbox(awsCredentials, dynamoDb); diff --git a/samples/WebAPI_EFCore/GreetingsWeb/Startup.cs b/samples/WebAPI_EFCore/GreetingsWeb/Startup.cs index 9783798be9..9e868ca01e 100644 --- a/samples/WebAPI_EFCore/GreetingsWeb/Startup.cs +++ b/samples/WebAPI_EFCore/GreetingsWeb/Startup.cs @@ -136,7 +136,7 @@ private void ConfigureBrighter(IServiceCollection services) options.MapperLifetime = ServiceLifetime.Singleton; options.PolicyRegistry = new GreetingsPolicy(); }) - .UseExternalBus((configure) => + .UseExternalBus((configure) => { configure.ProducerRegistry = producerRegistry; configure.Outbox = @@ -177,7 +177,7 @@ private void ConfigureBrighter(IServiceCollection services) options.MapperLifetime = ServiceLifetime.Singleton; options.PolicyRegistry = new GreetingsPolicy(); }) - .UseExternalBus((config) => + .UseExternalBus((config) => { config.ProducerRegistry = producerRegistry; config.Outbox = diff --git a/samples/WebApiWithWorkerAndSweeper/Orders.API/Program.cs b/samples/WebApiWithWorkerAndSweeper/Orders.API/Program.cs index 15df555915..a0dd1fc195 100644 --- a/samples/WebApiWithWorkerAndSweeper/Orders.API/Program.cs +++ b/samples/WebApiWithWorkerAndSweeper/Orders.API/Program.cs @@ -47,7 +47,7 @@ opt.PolicyRegistry = new DefaultPolicy(); opt.CommandProcessorLifetime = ServiceLifetime.Scoped; }) - .UseExternalBus((configure) => + .UseExternalBus((configure) => { configure.ProducerRegistry = producerRegistry; configure.Outbox = new MsSqlOutbox(outboxConfig); diff --git a/samples/WebApiWithWorkerAndSweeper/Orders.Sweeper/Extensions/BrighterExtensions.cs b/samples/WebApiWithWorkerAndSweeper/Orders.Sweeper/Extensions/BrighterExtensions.cs index fff00ad266..9fc9e7fcbc 100644 --- a/samples/WebApiWithWorkerAndSweeper/Orders.Sweeper/Extensions/BrighterExtensions.cs +++ b/samples/WebApiWithWorkerAndSweeper/Orders.Sweeper/Extensions/BrighterExtensions.cs @@ -54,7 +54,7 @@ public static WebApplicationBuilder AddBrighter(this WebApplicationBuilder build } builder.Services.AddBrighter() - .UseExternalBus((configure) => + .UseExternalBus((configure) => { configure.ProducerRegistry = producerRegistry; configure.Outbox = new MsSqlOutbox(outboxSettings); diff --git a/src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionExtensions.cs b/src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionExtensions.cs index 03a20b672e..50bf7c6a72 100644 --- a/src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionExtensions.cs @@ -112,36 +112,6 @@ public static IBrighterBuilder BrighterHandlerBuilder(IServiceCollection service ); } - /// - /// 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. - /// NOTE: This external bus will use an in memory outbox to facilitate sending; this will not survive restarts - /// of the service. Use the alternative constructor if you wish to provide an outbox that supports transactions - /// that persist to a Db. - /// 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 - /// - /// - 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 allow chaining of requests - public static IBrighterBuilder UseExternalBus( - this IBrighterBuilder brighterBuilder, - Action configure = null) - { - return UseExternalBus(brighterBuilder, configure); - } - /// /// 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 @@ -166,7 +136,7 @@ public static IBrighterBuilder UseExternalBus( /// /// The lifetime of the transaction provider /// The Brighter builder to allow chaining of requests - public static IBrighterBuilder UseExternalBus( + public static IBrighterBuilder UseExternalBus( this IBrighterBuilder brighterBuilder, Action configure, ServiceLifetime serviceLifetime = ServiceLifetime.Scoped) @@ -180,22 +150,31 @@ public static IBrighterBuilder UseExternalBus( //default to using System Transactions if nothing provided, so we always technically can share the outbox transaction Type transactionProvider = busConfiguration.TransactionProvider ?? typeof(CommittableTransactionProvider); - - if (transactionProvider.GenericTypeArguments[0] != typeof(TTransaction)) - throw new ConfigurationException( - $"Unable to register provider of type {transactionProvider.Name}. Generic type argument does not match {typeof(TTransaction).Name}."); - - if (!typeof(IAmABoxTransactionProvider).IsAssignableFrom(transactionProvider)) + + //Find the transaction type from the provider + Type transactionProviderInterface = typeof(IAmABoxTransactionProvider<>); + Type[] interfaces = transactionProvider.GetInterfaces(); + Type transactionType = null; + foreach (Type i in interfaces) + { + if (i.IsGenericType && i.GetGenericTypeDefinition() == transactionProviderInterface) + { + transactionType = i.GetGenericArguments()[0]; + } + } + + if (transactionType == null) throw new ConfigurationException( - $"Unable to register provider of type {transactionProvider.Name}. Class does not implement interface {nameof(IAmABoxTransactionProvider)}."); + $"Unable to register provider of type {transactionProvider.Name}. It does not implement {typeof(IAmABoxTransactionProvider<>).Name}."); - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmABoxTransactionProvider), - transactionProvider, serviceLifetime)); + //register the generic interface with the transaction type + var boxProviderType = transactionProviderInterface.MakeGenericType(transactionType); + + brighterBuilder.Services.Add(new ServiceDescriptor(boxProviderType, transactionProvider, serviceLifetime)); - RegisterRelationalProviderServicesMaybe(brighterBuilder, transactionProvider, - serviceLifetime); + RegisterRelationalProviderServicesMaybe(brighterBuilder, transactionProvider, serviceLifetime); - return ExternalBusBuilder(brighterBuilder, busConfiguration); + return ExternalBusBuilder(brighterBuilder, busConfiguration, transactionType); } private static INeedARequestContext AddEventBus(IServiceProvider provider, INeedMessaging messagingBuilder, @@ -280,10 +259,10 @@ private static object BuildCommandProcessor(IServiceProvider provider) return commandProcessor; } - private static IBrighterBuilder ExternalBusBuilder( + private static IBrighterBuilder ExternalBusBuilder( IBrighterBuilder brighterBuilder, - IAmExternalBusConfiguration externalBusConfiguration - ) + IAmExternalBusConfiguration externalBusConfiguration, + Type transactionType) { if (externalBusConfiguration.ProducerRegistry == null) throw new ConfigurationException("An external bus must have an IAmAProducerRegistry"); @@ -292,24 +271,31 @@ IAmExternalBusConfiguration externalBusConfiguration 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(); - serviceCollection.TryAddSingleton>( - (IAmAnOutboxSync)outbox - ); - serviceCollection.TryAddSingleton>( - (IAmAnOutboxAsync)outbox - ); } - else + + //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); + + Type[] interfaces = outbox.GetType().GetInterfaces(); + foreach (Type i in interfaces) { - if (outbox is IAmAnOutboxSync outboxSync) - serviceCollection.TryAddSingleton(outboxSync); - if (outbox is IAmAnOutboxAsync outboxAsync) - serviceCollection.TryAddSingleton(outboxAsync); + if (i.IsGenericType && i.GetGenericTypeDefinition() == syncOutboxType) + { + new ServiceDescriptor(syncOutboxType, _ => outbox, ServiceLifetime.Singleton); + } + + if (i.IsGenericType && i.GetGenericTypeDefinition() == asyncOutboxType) + { + new ServiceDescriptor(asyncOutboxType, _ => outbox, ServiceLifetime.Singleton); + } } if (externalBusConfiguration.UseRpc) @@ -317,13 +303,17 @@ IAmExternalBusConfiguration externalBusConfiguration serviceCollection.TryAddSingleton(new UseRpc(externalBusConfiguration.UseRpc, externalBusConfiguration.ReplyQueueSubscriptions)); } - - var bus = new ExternalBusServices( - producerRegistry: externalBusConfiguration.ProducerRegistry, - policyRegistry: brighterBuilder.PolicyRegistry, - outbox: outbox, - outboxBulkChunkSize: externalBusConfiguration.OutboxBulkChunkSize, - outboxTimeout: externalBusConfiguration.OutboxTimeout); + + //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); + + IAmAnExternalBusService bus = (IAmAnExternalBusService)Activator.CreateInstance(busType, + externalBusConfiguration.ProducerRegistry, + brighterBuilder.PolicyRegistry, + outbox, + externalBusConfiguration.OutboxBulkChunkSize, + externalBusConfiguration.OutboxTimeout); serviceCollection.TryAddSingleton(bus); @@ -368,7 +358,7 @@ public static MessageMapperRegistry MessageMapperRegistry(IServiceProvider provi return messageMapperRegistry; } - private static void RegisterRelationalProviderServicesMaybe( + private static void RegisterRelationalProviderServicesMaybe( IBrighterBuilder brighterBuilder, Type transactionProvider, ServiceLifetime serviceLifetime) { diff --git a/src/Paramore.Brighter.ServiceActivator.Extensions.DependencyInjection/ServiceActivatorOptions.cs b/src/Paramore.Brighter.ServiceActivator.Extensions.DependencyInjection/ServiceActivatorOptions.cs index 5c80ed9ef6..2a73ca4c39 100644 --- a/src/Paramore.Brighter.ServiceActivator.Extensions.DependencyInjection/ServiceActivatorOptions.cs +++ b/src/Paramore.Brighter.ServiceActivator.Extensions.DependencyInjection/ServiceActivatorOptions.cs @@ -26,17 +26,17 @@ public interface IServiceActivatorOptions /// /// 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; } + 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 7ed56b02d4..14dea5fd13 100644 --- a/src/Paramore.Brighter.ServiceActivator.Extensions.DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Paramore.Brighter.ServiceActivator.Extensions.DependencyInjection/ServiceCollectionExtensions.cs @@ -32,7 +32,8 @@ public static IBrighterBuilder AddServiceActivator( var options = new ServiceActivatorOptions(); configure?.Invoke(options); - services.TryAddSingleton(options); + services.TryAddSingleton(options); + services.TryAddSingleton(options); services.TryAdd(new ServiceDescriptor(typeof(IDispatcher), (serviceProvider) => (IDispatcher)BuildDispatcher(serviceProvider), @@ -51,7 +52,7 @@ 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/tests/Paramore.Brighter.Extensions.Tests/TestDifferentSetups.cs b/tests/Paramore.Brighter.Extensions.Tests/TestDifferentSetups.cs index 7d83c4448f..89bd7ec271 100644 --- a/tests/Paramore.Brighter.Extensions.Tests/TestDifferentSetups.cs +++ b/tests/Paramore.Brighter.Extensions.Tests/TestDifferentSetups.cs @@ -39,7 +39,7 @@ public void WithExternalBus() serviceCollection .AddBrighter() - .UseExternalBus((config) => + .UseExternalBus((config) => { config.ProducerRegistry = producerRegistry; }) From aa1fa1bba969b9e251fe02808c1ee5a8b5a1c66c Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Sun, 28 May 2023 13:46:00 +0100 Subject: [PATCH 59/89] Remove DynamoDb extension for creation of outbox --- .../ServiceCollectionExtensions.cs | 70 ------------------- 1 file changed, 70 deletions(-) delete mode 100644 src/Paramore.Brighter.Outbox.DynamoDB/ServiceCollectionExtensions.cs diff --git a/src/Paramore.Brighter.Outbox.DynamoDB/ServiceCollectionExtensions.cs b/src/Paramore.Brighter.Outbox.DynamoDB/ServiceCollectionExtensions.cs deleted file mode 100644 index 33ec44b72d..0000000000 --- a/src/Paramore.Brighter.Outbox.DynamoDB/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System; -using Amazon.DynamoDBv2; -using Amazon.DynamoDBv2.Model; -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 transactionProvider, - ServiceLifetime serviceLifetime = ServiceLifetime.Scoped) - { - if (transactionProvider is null) - throw new ArgumentNullException($"{nameof(transactionProvider)} cannot be null.", nameof(transactionProvider)); - - if (!typeof(IAmABoxTransactionProvider).IsAssignableFrom(transactionProvider)) - throw new Exception($"Unable to register provider of type {transactionProvider.Name}. Class does not implement interface {nameof(IAmABoxTransactionProvider)}."); - - brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmABoxTransactionProvider), transactionProvider, 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); - } - } -} From 0de9b33a7358b390be8a86ae7c140851622e8446 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Sun, 28 May 2023 15:00:18 +0100 Subject: [PATCH 60/89] Fix how we get the message mapper and transformer --- .../ServiceCollectionExtensions.cs | 57 ++++++++++--------- 1 file changed, 30 insertions(+), 27 deletions(-) diff --git a/src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionExtensions.cs b/src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionExtensions.cs index 50bf7c6a72..6fe341e88b 100644 --- a/src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionExtensions.cs @@ -88,10 +88,6 @@ public static IBrighterBuilder BrighterHandlerBuilder(IServiceCollection service var transformRegistry = new ServiceCollectionTransformerRegistry(services, options.TransformerLifetime); services.TryAddSingleton(transformRegistry); - services.TryAdd(new ServiceDescriptor(typeof(IAmACommandProcessor), - (serviceProvider) => (IAmACommandProcessor)BuildCommandProcessor(serviceProvider), - options.CommandProcessorLifetime)); - var mapperRegistry = new ServiceCollectionMessageMapperRegistry(services, options.MapperLifetime); services.TryAddSingleton(mapperRegistry); @@ -102,6 +98,10 @@ public static IBrighterBuilder BrighterHandlerBuilder(IServiceCollection service 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, @@ -153,16 +153,11 @@ public static IBrighterBuilder UseExternalBus( //Find the transaction type from the provider Type transactionProviderInterface = typeof(IAmABoxTransactionProvider<>); - Type[] interfaces = transactionProvider.GetInterfaces(); Type transactionType = null; - foreach (Type i in interfaces) - { + 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}."); @@ -177,33 +172,40 @@ public static IBrighterBuilder UseExternalBus( return ExternalBusBuilder(brighterBuilder, busConfiguration, transactionType); } - private static INeedARequestContext AddEventBus(IServiceProvider provider, INeedMessaging messagingBuilder, + private static INeedARequestContext AddEventBus( + IServiceProvider provider, + INeedMessaging messagingBuilder, IUseRpc useRequestResponse) { var eventBus = provider.GetService(); var eventBusConfiguration = provider.GetService(); - var messageMapperRegistry = provider.GetService(); - var messageTransformerFactory = provider.GetService(); + var messageMapperRegistry = MessageMapperRegistry(provider); + var messageTransformFactory = TransformFactory(provider); INeedARequestContext ret = null; - if (eventBus == null) ret = messagingBuilder.NoExternalBus(); - if (eventBus != null && useRequestResponse.RPC) + var hasEventBus = eventBus != null; + bool useRpc = useRequestResponse != null && useRequestResponse.RPC; + + if (!hasEventBus) ret = messagingBuilder.NoExternalBus(); + + if (hasEventBus && !useRpc) { ret = messagingBuilder.ExternalBus( - useRequestResponse.RPC ? ExternalBusType.RPC : ExternalBusType.FireAndForget, + ExternalBusType.FireAndForget, eventBus, messageMapperRegistry, - messageTransformerFactory, + messageTransformFactory, eventBusConfiguration.ResponseChannelFactory, eventBusConfiguration.ReplyQueueSubscriptions); } - else if (eventBus != null && useRequestResponse.RPC) + + if (hasEventBus && useRpc) { ret = messagingBuilder.ExternalBus( - useRequestResponse.RPC ? ExternalBusType.RPC : ExternalBusType.FireAndForget, + ExternalBusType.RPC, eventBus, messageMapperRegistry, - messageTransformerFactory, + messageTransformFactory, eventBusConfiguration.ResponseChannelFactory, eventBusConfiguration.ReplyQueueSubscriptions ); @@ -283,18 +285,19 @@ private static IBrighterBuilder ExternalBusBuilder( //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); - - Type[] interfaces = outbox.GetType().GetInterfaces(); - foreach (Type i in interfaces) + + foreach (Type i in outbox.GetType().GetInterfaces()) { if (i.IsGenericType && i.GetGenericTypeDefinition() == syncOutboxType) { - new ServiceDescriptor(syncOutboxType, _ => outbox, ServiceLifetime.Singleton); + var outboxDescriptor = new ServiceDescriptor(syncOutboxType, _ => outbox, ServiceLifetime.Singleton); + serviceCollection.Add(outboxDescriptor); } - + if (i.IsGenericType && i.GetGenericTypeDefinition() == asyncOutboxType) { - new ServiceDescriptor(asyncOutboxType, _ => outbox, ServiceLifetime.Singleton); + var asyncOutboxdescriptor = new ServiceDescriptor(asyncOutboxType, _ => outbox, ServiceLifetime.Singleton); + serviceCollection.Add(asyncOutboxdescriptor); } } From dfa5a7119bfaf4184588ed013a1343cf5870c877 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Sun, 28 May 2023 16:02:40 +0100 Subject: [PATCH 61/89] Sample fixes --- .../.idea/httpRequests/http-requests-log.http | 156 +++++++++--------- Docker/dynamodb/shared-local-instance.db | Bin 552960 -> 552960 bytes .../Handlers/AddGreetingHandlerAsync.cs | 26 +-- .../Handlers/GreetingMadeHandler.cs | 5 +- .../Handlers/AddGreetingHandlerAsync.cs | 7 +- .../Handlers/GreetingMadeHandler.cs | 4 +- .../Handlers/AddGreetingHandlerAsync.cs | 5 +- .../Handlers/GreetingMadeHandler.cs | 6 +- .../Handlers/AddGreetingHandlerAsync.cs | 31 ++-- .../Handlers/GreetingMadeHandler.cs | 28 +++- src/Paramore.Brighter/CommandProcessor.cs | 4 +- 11 files changed, 153 insertions(+), 119 deletions(-) diff --git a/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http b/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http index abe5eae21d..94b512c914 100644 --- a/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http +++ b/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http @@ -9,6 +9,81 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } +<> 2023-05-28T155036.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 + +### + +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" +} + +### + +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" +} + +### + +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-05-28T153646.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-05-28T153237.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-05-28T153234.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-05-16T084354.200.json ### @@ -541,84 +616,3 @@ Accept-Encoding: br,deflate,gzip,x-gzip ### -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-05-08T131957.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-05-08T131953.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-05-08T131712.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" -} - -<> 2023-05-08T131709.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-05-08T131704.500.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-05-08T131659.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" -} - -<> 2023-05-06T164909.500.json - -### - diff --git a/Docker/dynamodb/shared-local-instance.db b/Docker/dynamodb/shared-local-instance.db index 6b7f933c98391a2370fabcb7dcb24ccb0b91af2a..f88a226447c36374f2462fbfe850838387e01705 100644 GIT binary patch delta 20884 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 T%%$4qN->Oh`&=op{KEeN_pxI{ diff --git a/samples/WebAPI_Dapper/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs b/samples/WebAPI_Dapper/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs index ef7665b1df..d00cd0c318 100644 --- a/samples/WebAPI_Dapper/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs +++ b/samples/WebAPI_Dapper/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs @@ -38,32 +38,38 @@ public override async Task HandleAsync(AddGreeting addGreeting, Can //to share them 'behind the scenes' var conn = await _transactionProvider.GetConnectionAsync(cancellationToken); - await conn.OpenAsync(cancellationToken); - var tx = _transactionProvider.GetTransaction(); + 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 + + //write the added child entity to the Db await conn.InsertAsync(greeting, 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()), cancellationToken: cancellationToken)); - + posts.Add(await _postBox.DepositPostAsync( + new GreetingMade(greeting.Greet()), + _transactionProvider, + cancellationToken: cancellationToken)); + //commit both new greeting and outgoing message - await tx.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 - 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/SalutationPorts/Handlers/GreetingMadeHandler.cs b/samples/WebAPI_Dapper/SalutationPorts/Handlers/GreetingMadeHandler.cs index 18f3bda611..ad567bb463 100644 --- a/samples/WebAPI_Dapper/SalutationPorts/Handlers/GreetingMadeHandler.cs +++ b/samples/WebAPI_Dapper/SalutationPorts/Handlers/GreetingMadeHandler.cs @@ -39,7 +39,10 @@ public override async Task HandleAsync(GreetingMade @event, Cancel await _transactionConnectionProvider.GetConnection().InsertAsync(salutation, tx); - posts.Add(await _postBox.DepositPostAsync(new SalutationReceived(DateTimeOffset.Now), cancellationToken: cancellationToken)); + posts.Add(await _postBox.DepositPostAsync( + new SalutationReceived(DateTimeOffset.Now), + _transactionConnectionProvider, + cancellationToken: cancellationToken)); await tx.CommitAsync(cancellationToken); } diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs index d4cf2d72b7..d87aca5968 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs +++ b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs @@ -52,8 +52,11 @@ public override async Task HandleAsync(AddGreeting addGreeting, Can await conn.InsertAsync(greeting, 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()), - cancellationToken: cancellationToken)); + posts.Add(await _postBox.DepositPostAsync( + new GreetingMade(greeting.Greet()), + _transactionConnectionProvider, + cancellationToken: cancellationToken) + ); //commit both new greeting and outgoing message await _transactionConnectionProvider.CommitAsync(cancellationToken); diff --git a/samples/WebAPI_Dapper_Kafka/SalutationPorts/Handlers/GreetingMadeHandler.cs b/samples/WebAPI_Dapper_Kafka/SalutationPorts/Handlers/GreetingMadeHandler.cs index 1da7a15221..4fdde9d753 100644 --- a/samples/WebAPI_Dapper_Kafka/SalutationPorts/Handlers/GreetingMadeHandler.cs +++ b/samples/WebAPI_Dapper_Kafka/SalutationPorts/Handlers/GreetingMadeHandler.cs @@ -47,7 +47,9 @@ public override GreetingMade Handle(GreetingMade @event) _transactionConnectionProvider.GetConnection().Insert(salutation, tx); - posts.Add(_postBox.DepositPost(new SalutationReceived(DateTimeOffset.Now))); + posts.Add(_postBox.DepositPost( + new SalutationReceived(DateTimeOffset.Now), + _transactionConnectionProvider)); _transactionConnectionProvider.Commit(); } diff --git a/samples/WebAPI_Dynamo/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs b/samples/WebAPI_Dynamo/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs index 66a1215d07..2f3748ca2b 100644 --- a/samples/WebAPI_Dynamo/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs +++ b/samples/WebAPI_Dynamo/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs @@ -52,7 +52,10 @@ public override async Task HandleAsync(AddGreeting addGreeting, Can 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), + _unitOfWork, + cancellationToken: cancellationToken)); //commit both new greeting and outgoing message await _unitOfWork.CommitAsync(cancellationToken); diff --git a/samples/WebAPI_Dynamo/SalutationPorts/Handlers/GreetingMadeHandler.cs b/samples/WebAPI_Dynamo/SalutationPorts/Handlers/GreetingMadeHandler.cs index 03f86dcde9..fce98f0035 100644 --- a/samples/WebAPI_Dynamo/SalutationPorts/Handlers/GreetingMadeHandler.cs +++ b/samples/WebAPI_Dynamo/SalutationPorts/Handlers/GreetingMadeHandler.cs @@ -42,7 +42,11 @@ public override async Task HandleAsync(GreetingMade @event, Cancel tx.TransactItems.Add(new TransactWriteItem{Put = new Put{ TableName = "Salutations", Item = attributes}}); - posts.Add(await _postBox.DepositPostAsync(new SalutationReceived(DateTimeOffset.Now), cancellationToken: cancellationToken)); + posts.Add(await _postBox.DepositPostAsync( + new SalutationReceived(DateTimeOffset.Now), + _uow, + cancellationToken: cancellationToken) + ); await _uow.CommitAsync(cancellationToken); } 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/SalutationPorts/Handlers/GreetingMadeHandler.cs b/samples/WebAPI_EFCore/SalutationPorts/Handlers/GreetingMadeHandler.cs index 7f50726fda..109f5bf588 100644 --- a/samples/WebAPI_EFCore/SalutationPorts/Handlers/GreetingMadeHandler.cs +++ b/samples/WebAPI_EFCore/SalutationPorts/Handlers/GreetingMadeHandler.cs @@ -16,11 +16,13 @@ public class GreetingMadeHandlerAsync : RequestHandlerAsync { private readonly SalutationsEntityGateway _uow; private readonly IAmACommandProcessor _postBox; + private readonly IAmATransactionConnectionProvider _transactionProvider; - public GreetingMadeHandlerAsync(SalutationsEntityGateway uow, IAmACommandProcessor postBox) + public GreetingMadeHandlerAsync(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!! @@ -30,29 +32,37 @@ public override async Task HandleAsync(GreetingMade @event, Cancel { var posts = new List(); - var tx = await _uow.Database.BeginTransactionAsync(cancellationToken); + await _transactionProvider.GetTransactionAsync(cancellationToken); try { var salutation = new Salutation(@event.Greeting); _uow.Salutations.Add(salutation); - - posts.Add(await _postBox.DepositPostAsync(new SalutationReceived(DateTimeOffset.Now), cancellationToken: cancellationToken)); - + + posts.Add(await _postBox.DepositPostAsync( + new SalutationReceived(DateTimeOffset.Now), + _transactionProvider, + cancellationToken: cancellationToken) + ); + await _uow.SaveChangesAsync(cancellationToken); - await tx.CommitAsync(cancellationToken); + await _transactionProvider.CommitAsync(cancellationToken); } catch (Exception e) { Console.WriteLine(e); - - await tx.RollbackAsync(cancellationToken); - + + await _transactionProvider.RollbackAsync(cancellationToken); + Console.WriteLine("Salutation analytical record not saved"); throw; } + finally + { + _transactionProvider.Close(); + } await _postBox.ClearOutboxAsync(posts, cancellationToken: cancellationToken); diff --git a/src/Paramore.Brighter/CommandProcessor.cs b/src/Paramore.Brighter/CommandProcessor.cs index 989347bd66..2ed9dbc4ed 100644 --- a/src/Paramore.Brighter/CommandProcessor.cs +++ b/src/Paramore.Brighter/CommandProcessor.cs @@ -628,7 +628,9 @@ IAmABoxTransactionProvider transactionProvider /// 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 From c52996177977c8a75128bcb2af2ff89f9219f802 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Fri, 2 Jun 2023 19:07:42 +0100 Subject: [PATCH 62/89] Was not marking dispatched as missing background thread --- .../.idea/httpRequests/http-requests-log.http | 252 ++++++++++-------- .../MySqlOutbox.cs | 14 +- src/Paramore.Brighter/ExternalBusServices.cs | 117 ++++---- 3 files changed, 200 insertions(+), 183 deletions(-) diff --git a/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http b/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http index 94b512c914..b3a44174ec 100644 --- a/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http +++ b/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http @@ -9,14 +9,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-05-28T155036.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-06-02T190642.200.json ### @@ -31,6 +24,8 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } +<> 2023-06-02T190609.200.json + ### POST http://localhost:5000/Greetings/Tyrion/new @@ -46,30 +41,16 @@ 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 - -<> 2023-05-28T153646.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-05-28T153237.200.json - -### - -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 -<> 2023-05-28T153234.200.json +{ + "Greeting" : "I drink, and I know things" +} ### @@ -84,7 +65,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-05-16T084354.200.json +<> 2023-06-02T184827.200.json ### @@ -99,7 +80,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-05-16T083851.200.json +<> 2023-06-02T182443.200.json ### @@ -114,7 +95,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-05-16T081219.200.json +<> 2023-06-02T164746.500.json ### @@ -123,7 +104,7 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-05-16T081215.200.json +<> 2023-06-02T164740.200.json ### @@ -138,7 +119,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-05-15T110426.200.json +<> 2023-06-02T164556.200.json ### @@ -153,22 +134,16 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-05-15T110422.200.json +<> 2023-06-02T164104.200.json ### -POST http://localhost:5000/Greetings/Tyrion/new -Content-Type: application/json -Content-Length: 47 +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-05-15T102620.200.json +<> 2023-06-02T164101.200.json ### @@ -177,7 +152,7 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-05-15T102555.200.json +<> 2023-06-02T164056.200.json ### @@ -192,7 +167,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-05-15T101101.200.json +<> 2023-05-28T155036.200.json ### @@ -201,32 +176,19 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-05-15T101027.200.json - ### -POST http://localhost:5000/People/new +POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json -Content-Length: 23 +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" } -<> 2023-05-15T101025.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-05-15T101020.500.json - ### POST http://localhost:5000/Greetings/Tyrion/new @@ -240,8 +202,6 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-05-14T210531.500.json - ### GET http://localhost:5000/People/Tyrion @@ -249,49 +209,41 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-05-14T210524.200.json +<> 2023-05-28T153646.200.json ### -POST http://localhost:5000/People/new -Content-Type: application/json -Content-Length: 23 +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 -{ - "Name" : "Tyrion" -} - -<> 2023-05-14T210520.200.json +<> 2023-05-28T153237.200.json ### -POST http://localhost:5000/People/new -Content-Type: application/json -Content-Length: 23 +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 -{ - "Name" : "Tyrion" -} +<> 2023-05-28T153234.200.json ### -POST http://localhost:5000/People/new +POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json -Content-Length: 23 +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" } +<> 2023-05-16T084354.200.json + ### POST http://localhost:5000/Greetings/Tyrion/new @@ -305,7 +257,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-05-14T203228.200.json +<> 2023-05-16T083851.200.json ### @@ -320,7 +272,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-05-14T203141.200.json +<> 2023-05-16T081219.200.json ### @@ -329,7 +281,7 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-05-14T203136.200.json +<> 2023-05-16T081215.200.json ### @@ -344,22 +296,22 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-05-11T110559.200.json +<> 2023-05-15T110426.200.json ### -POST http://localhost:5000/People/new +POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json -Content-Length: 26 +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" : "Ned Stark" + "Greeting" : "I drink, and I know things" } -<> 2023-05-11T110539.200.json +<> 2023-05-15T110422.200.json ### @@ -374,7 +326,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-05-11T083445.500.json +<> 2023-05-15T102620.200.json ### @@ -383,7 +335,7 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-05-11T083438.200.json +<> 2023-05-15T102555.200.json ### @@ -398,7 +350,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-05-11T083357.200.json +<> 2023-05-15T101101.200.json ### @@ -407,22 +359,22 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-05-11T083353.200.json +<> 2023-05-15T101027.200.json ### -POST http://localhost:5000/Greetings/Tyrion/new +POST http://localhost:5000/People/new Content-Type: application/json -Content-Length: 47 +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" } -<> 2023-05-11T082416.500.json +<> 2023-05-15T101025.200.json ### @@ -431,7 +383,7 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-05-11T082408.200.json +<> 2023-05-15T101020.500.json ### @@ -446,7 +398,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-05-11T082332.200.json +<> 2023-05-14T210531.500.json ### @@ -455,7 +407,7 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-05-11T082326.200.json +<> 2023-05-14T210524.200.json ### @@ -470,16 +422,20 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Name" : "Tyrion" } -<> 2023-05-10T193837.500.json +<> 2023-05-14T210520.200.json ### -GET http://localhost:5000/People/Tyrion +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 -<> 2023-05-10T193523.500.json +{ + "Name" : "Tyrion" +} ### @@ -494,7 +450,20 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Name" : "Tyrion" } -<> 2023-05-10T193516.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" +} + +<> 2023-05-14T203228.200.json ### @@ -509,7 +478,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-05-10T140942.200.json +<> 2023-05-14T203141.200.json ### @@ -518,22 +487,37 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-05-10T140922.200.json +<> 2023-05-14T203136.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-05-11T110559.200.json ### POST http://localhost:5000/People/new Content-Type: application/json -Content-Length: 23 +Content-Length: 26 Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip { - "Name" : "Tyrion" + "Name" : "Ned Stark" } -<> 2023-05-10T140918.500.json +<> 2023-05-11T110539.200.json ### @@ -548,7 +532,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-05-08T200327.200.json +<> 2023-05-11T083445.500.json ### @@ -557,7 +541,22 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-05-08T200232.200.json +<> 2023-05-11T083438.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-05-11T083357.200.json ### @@ -566,30 +565,32 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-05-08T155920.200.json +<> 2023-05-11T083353.200.json ### -POST http://localhost:5000/People/new +POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json -Content-Length: 23 +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" } -<> 2023-05-08T155916.500.json +<> 2023-05-11T082416.500.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 +<> 2023-05-11T082408.200.json + ### POST http://localhost:5000/Greetings/Tyrion/new @@ -603,7 +604,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-05-08T132600.200.json +<> 2023-05-11T082332.200.json ### @@ -612,7 +613,22 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-05-08T132419.200.json +<> 2023-05-11T082326.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" +} + +<> 2023-05-10T193837.500.json ### diff --git a/src/Paramore.Brighter.Outbox.MySql/MySqlOutbox.cs b/src/Paramore.Brighter.Outbox.MySql/MySqlOutbox.cs index cb54b9eec7..c8e94057d4 100644 --- a/src/Paramore.Brighter.Outbox.MySql/MySqlOutbox.cs +++ b/src/Paramore.Brighter.Outbox.MySql/MySqlOutbox.cs @@ -374,14 +374,13 @@ private Message MapAMessage(IDataReader dr) } var body = _configuration.BinaryMessagePayload - ? new MessageBody(GetBodyAsBytes((MySqlDataReader)dr), "application/octet-stream", - CharacterEncoding.Raw) - : new MessageBody(dr.GetString(dr.GetOrdinal("Body")), "application/json", CharacterEncoding.UTF8); + ? new MessageBody(GetBodyAsBytes((MySqlDataReader)dr), "application/octet-stream", CharacterEncoding.Raw) + : new MessageBody(GetBodyAsString(dr), "application/json", CharacterEncoding.UTF8); return new Message(header, body); } - private byte[] GetBodyAsBytes(MySqlDataReader dr) + private byte[] GetBodyAsBytes(MySqlDataReader dr) { var i = dr.GetOrdinal("Body"); using (var ms = new MemoryStream()) @@ -402,7 +401,12 @@ private byte[] GetBodyAsBytes(MySqlDataReader dr) } } - private static Dictionary GetContextBag(IDataReader dr) + 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); diff --git a/src/Paramore.Brighter/ExternalBusServices.cs b/src/Paramore.Brighter/ExternalBusServices.cs index c395397284..9a14341686 100644 --- a/src/Paramore.Brighter/ExternalBusServices.cs +++ b/src/Paramore.Brighter/ExternalBusServices.cs @@ -20,20 +20,15 @@ public class ExternalBusServices : IAmAnExternalBusServi { 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 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); @@ -64,16 +59,18 @@ public ExternalBusServices( 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"); + _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; - } + 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. @@ -89,8 +86,8 @@ protected virtual void Dispose(bool disposing) if (_disposed) return; - if (disposing && ProducerRegistry != null) - ProducerRegistry.CloseAll(); + if (disposing && _producerRegistry != null) + _producerRegistry.CloseAll(); _disposed = true; } @@ -116,7 +113,7 @@ public async Task AddToOutboxAsync( var written = await RetryAsync( async ct => { - await AsyncOutbox.AddAsync(message, OutboxTimeout, ct, overridingTransactionProvider) + await _asyncOutbox.AddAsync(message, _outboxTimeout, ct, overridingTransactionProvider) .ConfigureAwait(continueOnCapturedContext); }, continueOnCapturedContext, cancellationToken).ConfigureAwait(continueOnCapturedContext); @@ -145,7 +142,7 @@ public async Task AddToOutboxAsync( 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)) @@ -153,7 +150,7 @@ public async Task AddToOutboxAsync( var written = await RetryAsync( async ct => { - await box.AddAsync(chunk, OutboxTimeout, ct, overridingTransactionProvider) + await box.AddAsync(chunk, _outboxTimeout, ct, overridingTransactionProvider) .ConfigureAwait(continueOnCapturedContext); }, continueOnCapturedContext, cancellationToken).ConfigureAwait(continueOnCapturedContext); @@ -164,7 +161,7 @@ await box.AddAsync(chunk, OutboxTimeout, ct, overridingTransactionProvider) } else { - throw new InvalidOperationException($"{AsyncOutbox.GetType()} does not implement IAmABulkOutboxAsync"); + throw new InvalidOperationException($"{_asyncOutbox.GetType()} does not implement IAmABulkOutboxAsync"); } } @@ -185,7 +182,7 @@ public void AddToOutbox( { CheckOutboxOutstandingLimit(); - var written = Retry(() => { OutBox.Add(message, OutboxTimeout, overridingTransactionProvider); }); + var written = Retry(() => { _outBox.Add(message, _outboxTimeout, overridingTransactionProvider); }); if (!written) throw new ChannelFailureException($"Could not write request {request.Id} to the outbox"); @@ -201,13 +198,13 @@ public void AddToOutbox( 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, overridingTransactionProvider); }); + Retry(() => { box.Add(chunk, _outboxTimeout, overridingTransactionProvider); }); if (!written) throw new ChannelFailureException($"Could not write {chunk.Count()} messages to the outbox"); @@ -215,7 +212,7 @@ public void AddToOutbox( } else { - throw new InvalidOperationException($"{OutBox.GetType()} does not implement IAmABulkOutboxSync"); + throw new InvalidOperationException($"{_outBox.GetType()} does not implement IAmABulkOutboxSync"); } } @@ -229,7 +226,7 @@ public 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); + var producer = _producerRegistry.LookupByOrDefault(outMessage.Header.Topic); if (producer is IAmAMessageProducerSync producerSync) Retry(() => producerSync.Send(outMessage)); } @@ -251,7 +248,7 @@ public void ClearOutbox(params Guid[] posts) { 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"); @@ -287,7 +284,7 @@ public async Task ClearOutboxAsync( { 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"); @@ -347,7 +344,7 @@ public void ClearOutbox( private void ConfigureCallbacks() { //Only register one, to avoid two callbacks where we support both interfaces on a producer - foreach (var producer in ProducerRegistry.Producers) + foreach (var producer in _producerRegistry.Producers) { if (!ConfigurePublisherCallbackMaybe(producer)) ConfigureAsyncPublisherCallbackMaybe(producer); @@ -369,9 +366,9 @@ private bool ConfigureAsyncPublisherCallbackMaybe(IAmAMessageProducer producer) if (success) { s_logger.LogInformation("Sent message: Id:{Id}", id.ToString()); - if (AsyncOutbox != null) + if (_asyncOutbox != null) await RetryAsync(async ct => - await AsyncOutbox.MarkDispatchedAsync(id, DateTime.UtcNow, cancellationToken: ct)); + await _asyncOutbox.MarkDispatchedAsync(id, DateTime.UtcNow, cancellationToken: ct)); } }; return true; @@ -394,8 +391,8 @@ private bool ConfigurePublisherCallbackMaybe(IAmAMessageProducer producer) if (success) { s_logger.LogInformation("Sent message: Id:{Id}", id.ToString()); - if (OutBox != null) - Retry(() => OutBox.MarkDispatched(id, DateTime.UtcNow)); + if (_outBox != null) + Retry(() => _outBox.MarkDispatched(id, DateTime.UtcNow)); } }; return true; @@ -410,7 +407,7 @@ private bool ConfigurePublisherCallbackMaybe(IAmAMessageProducer producer) /// true if defined public bool HasAsyncOutbox() { - return AsyncOutbox != null; + return _asyncOutbox != null; } /// @@ -420,7 +417,7 @@ public bool HasAsyncOutbox() public bool HasAsyncBulkOutbox() { #pragma warning disable CS0618 - return AsyncOutbox is IAmABulkOutboxAsync; + return _asyncOutbox is IAmABulkOutboxAsync; #pragma warning restore CS0618 } @@ -430,7 +427,7 @@ public bool HasAsyncBulkOutbox() /// true if defined public bool HasOutbox() { - return OutBox != null; + return _outBox != null; } /// @@ -440,7 +437,7 @@ public bool HasOutbox() public bool HasBulkOutbox() { #pragma warning disable CS0618 - return OutBox is IAmABulkOutboxSync; + return _outBox is IAmABulkOutboxSync; #pragma warning restore CS0618 } @@ -451,7 +448,7 @@ public bool HasBulkOutbox() /// public bool Retry(Action action) { - var policy = PolicyRegistry.Get(CommandProcessor.RETRYPOLICY); + var policy = _policyRegistry.Get(CommandProcessor.RETRYPOLICY); var result = policy.ExecuteAndCapture(action); if (result.Outcome != OutcomeType.Successful) { @@ -469,20 +466,20 @@ public bool Retry(Action action) private IEnumerable> ChunkMessages(IEnumerable messages) { - return Enumerable.Range(0, (int)Math.Ceiling((messages.Count() / (decimal)OutboxBulkChunkSize))) + return Enumerable.Range(0, (int)Math.Ceiling((messages.Count() / (decimal)_outboxBulkChunkSize))) .Select(i => new List(messages - .Skip(i * OutboxBulkChunkSize) - .Take(OutboxBulkChunkSize) + .Skip(i * _outboxBulkChunkSize) + .Take(_outboxBulkChunkSize) .ToArray())); } private void CheckOutboxOutstandingLimit() { - bool hasOutBox = (OutBox != null || AsyncOutbox != null); + bool hasOutBox = (_outBox != null || _asyncOutbox != null); if (!hasOutBox) return; - int maxOutStandingMessages = ProducerRegistry.GetDefaultProducer().MaxOutStandingMessages; + 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 @@ -498,7 +495,7 @@ private void CheckOutstandingMessages() { var now = DateTime.UtcNow; var checkInterval = - TimeSpan.FromMilliseconds(ProducerRegistry.GetDefaultProducer() + TimeSpan.FromMilliseconds(_producerRegistry.GetDefaultProducer() .MaxOutStandingCheckIntervalMilliSeconds); @@ -528,7 +525,7 @@ private async Task BackgroundDispatchUsingSync(int amountToClear, int minimumAge 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() } })); s_logger.LogInformation("Found {NumberOfMessages} to clear out of amount {AmountToClear}", @@ -569,7 +566,7 @@ private async Task BackgroundDispatchUsingAsync(int amountToClear, int minimumAg 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}", @@ -621,7 +618,7 @@ private void Dispatch(IEnumerable posts) 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) { @@ -634,7 +631,7 @@ 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 @@ -655,7 +652,7 @@ private async Task DispatchAsync(IEnumerable posts, bool continueOnCapt 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 IAmAMessageProducerAsync producerAsync) { @@ -680,7 +677,7 @@ await producerAsync.SendAsync(message).ConfigureAwait(continueOnCapturedContext) if (sent) await RetryAsync( - async ct => await AsyncOutbox.MarkDispatchedAsync(message.Id, DateTime.UtcNow, + async ct => await _asyncOutbox.MarkDispatchedAsync(message.Id, DateTime.UtcNow, cancellationToken: cancellationToken), cancellationToken: cancellationToken); } @@ -698,7 +695,7 @@ 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) { @@ -716,7 +713,7 @@ private async Task BulkDispatchAsync(IEnumerable posts, CancellationTok { if (!(producer is ISupportPublishConfirmation)) { - await RetryAsync(async ct => await AsyncOutbox.MarkDispatchedAsync(successfulMessage, + await RetryAsync(async ct => await _asyncOutbox.MarkDispatchedAsync(successfulMessage, DateTime.UtcNow, cancellationToken: cancellationToken), cancellationToken: cancellationToken); } @@ -737,10 +734,10 @@ private void OutstandingMessagesCheck() 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 @@ -768,7 +765,7 @@ private void OutstandingMessagesCheck() 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); From c5cd45af23c500e7b2afa64ccbbfa39d7a6ecf87 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Sun, 4 Jun 2023 18:35:49 -0600 Subject: [PATCH 63/89] Add missing migrations to support multiple production databases --- Brighter.sln | 60 ++++++++ .../GreetingsWeb/Database/SchemaCreation.cs | 129 +++++++++++----- .../GreetingsWeb/GreetingsWeb.csproj | 5 + .../Properties/launchSettings.json | 11 ++ samples/WebAPI_Dapper/GreetingsWeb/Startup.cs | 111 ++++++++++---- .../GreetingsWeb/appsettings.Production.json | 6 +- .../Greetings_MsSqlMigrations.csproj | 13 ++ .../Migrations/202306041332_InitialCreate.cs | 30 ++++ .../Greetings_PostgreSqlMigrations.csproj | 13 ++ .../Migrations/202306031734_InitialCreate.cs | 30 ++++ .../GreetingsWeb/Database/SchemaCreation.cs | 138 +++++++++++++----- .../GreetingsWeb/GreetingsWeb.csproj | 5 + .../GreetingsWeb/Startup.cs | 114 +++++++++++---- .../Greetings_MsSqlMigrations.csproj | 13 ++ .../Migrations/202306041332_InitialCreate.cs | 30 ++++ .../Greetings_PostgreSqlMigrations.csproj | 13 ++ .../Migrations/202306031734_InitialCreate.cs | 30 ++++ .../PostgreSqlOutboxBulder.cs | 2 +- 18 files changed, 620 insertions(+), 133 deletions(-) create mode 100644 samples/WebAPI_Dapper/Greetings_MsSqlMigrations/Greetings_MsSqlMigrations.csproj create mode 100644 samples/WebAPI_Dapper/Greetings_MsSqlMigrations/Migrations/202306041332_InitialCreate.cs create mode 100644 samples/WebAPI_Dapper/Greetings_PostgreSqlMigrations/Greetings_PostgreSqlMigrations.csproj create mode 100644 samples/WebAPI_Dapper/Greetings_PostgreSqlMigrations/Migrations/202306031734_InitialCreate.cs create mode 100644 samples/WebAPI_Dapper_Kafka/Greetings_MsSqlMigrations/Greetings_MsSqlMigrations.csproj create mode 100644 samples/WebAPI_Dapper_Kafka/Greetings_MsSqlMigrations/Migrations/202306041332_InitialCreate.cs create mode 100644 samples/WebAPI_Dapper_Kafka/Greetings_PostgreSqlMigrations/Greetings_PostgreSqlMigrations.csproj create mode 100644 samples/WebAPI_Dapper_Kafka/Greetings_PostgreSqlMigrations/Migrations/202306031734_InitialCreate.cs diff --git a/Brighter.sln b/Brighter.sln index cb97462ee8..a0178f5408 100644 --- a/Brighter.sln +++ b/Brighter.sln @@ -356,6 +356,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Salutations_mySqlMigrations EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Greetings_MySqlMigrations", "samples\WebAPI_Dapper_Kafka\Greetings_MySqlMigrations\Greetings_MySqlMigrations.csproj", "{2FADCED2-8563-4261-9AA8-514E5888075F}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Greetings_PostgreSqlMigrations", "samples\WebAPI_Dapper\Greetings_PostgreSqlMigrations\Greetings_PostgreSqlMigrations.csproj", "{BD636016-AC13-45BA-85CB-2A077568EDFA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Greetings_MsSqlMigrations", "samples\WebAPI_Dapper\Greetings_MsSqlMigrations\Greetings_MsSqlMigrations.csproj", "{838E3E45-7546-4EC2-B4C5-0CB239A3BD58}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Greetings_PostgreSqlMigrations", "samples\WebAPI_Dapper_Kafka\Greetings_PostgreSqlMigrations\Greetings_PostgreSqlMigrations.csproj", "{B64979D7-5FAD-4C5E-BCFC-B7FFA9C962C4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Greetings_MsSqlMigrations", "samples\WebAPI_Dapper_Kafka\Greetings_MsSqlMigrations\Greetings_MsSqlMigrations.csproj", "{2B11781D-13B0-4602-A6B4-96881F670CC0}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -2058,6 +2066,54 @@ Global {2FADCED2-8563-4261-9AA8-514E5888075F}.Release|Mixed Platforms.Build.0 = Release|Any CPU {2FADCED2-8563-4261-9AA8-514E5888075F}.Release|x86.ActiveCfg = Release|Any CPU {2FADCED2-8563-4261-9AA8-514E5888075F}.Release|x86.Build.0 = Release|Any CPU + {BD636016-AC13-45BA-85CB-2A077568EDFA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BD636016-AC13-45BA-85CB-2A077568EDFA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BD636016-AC13-45BA-85CB-2A077568EDFA}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {BD636016-AC13-45BA-85CB-2A077568EDFA}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {BD636016-AC13-45BA-85CB-2A077568EDFA}.Debug|x86.ActiveCfg = Debug|Any CPU + {BD636016-AC13-45BA-85CB-2A077568EDFA}.Debug|x86.Build.0 = Debug|Any CPU + {BD636016-AC13-45BA-85CB-2A077568EDFA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BD636016-AC13-45BA-85CB-2A077568EDFA}.Release|Any CPU.Build.0 = Release|Any CPU + {BD636016-AC13-45BA-85CB-2A077568EDFA}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {BD636016-AC13-45BA-85CB-2A077568EDFA}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {BD636016-AC13-45BA-85CB-2A077568EDFA}.Release|x86.ActiveCfg = Release|Any CPU + {BD636016-AC13-45BA-85CB-2A077568EDFA}.Release|x86.Build.0 = Release|Any CPU + {838E3E45-7546-4EC2-B4C5-0CB239A3BD58}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {838E3E45-7546-4EC2-B4C5-0CB239A3BD58}.Debug|Any CPU.Build.0 = Debug|Any CPU + {838E3E45-7546-4EC2-B4C5-0CB239A3BD58}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {838E3E45-7546-4EC2-B4C5-0CB239A3BD58}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {838E3E45-7546-4EC2-B4C5-0CB239A3BD58}.Debug|x86.ActiveCfg = Debug|Any CPU + {838E3E45-7546-4EC2-B4C5-0CB239A3BD58}.Debug|x86.Build.0 = Debug|Any CPU + {838E3E45-7546-4EC2-B4C5-0CB239A3BD58}.Release|Any CPU.ActiveCfg = Release|Any CPU + {838E3E45-7546-4EC2-B4C5-0CB239A3BD58}.Release|Any CPU.Build.0 = Release|Any CPU + {838E3E45-7546-4EC2-B4C5-0CB239A3BD58}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {838E3E45-7546-4EC2-B4C5-0CB239A3BD58}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {838E3E45-7546-4EC2-B4C5-0CB239A3BD58}.Release|x86.ActiveCfg = Release|Any CPU + {838E3E45-7546-4EC2-B4C5-0CB239A3BD58}.Release|x86.Build.0 = Release|Any CPU + {B64979D7-5FAD-4C5E-BCFC-B7FFA9C962C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B64979D7-5FAD-4C5E-BCFC-B7FFA9C962C4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B64979D7-5FAD-4C5E-BCFC-B7FFA9C962C4}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {B64979D7-5FAD-4C5E-BCFC-B7FFA9C962C4}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {B64979D7-5FAD-4C5E-BCFC-B7FFA9C962C4}.Debug|x86.ActiveCfg = Debug|Any CPU + {B64979D7-5FAD-4C5E-BCFC-B7FFA9C962C4}.Debug|x86.Build.0 = Debug|Any CPU + {B64979D7-5FAD-4C5E-BCFC-B7FFA9C962C4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B64979D7-5FAD-4C5E-BCFC-B7FFA9C962C4}.Release|Any CPU.Build.0 = Release|Any CPU + {B64979D7-5FAD-4C5E-BCFC-B7FFA9C962C4}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {B64979D7-5FAD-4C5E-BCFC-B7FFA9C962C4}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {B64979D7-5FAD-4C5E-BCFC-B7FFA9C962C4}.Release|x86.ActiveCfg = Release|Any CPU + {B64979D7-5FAD-4C5E-BCFC-B7FFA9C962C4}.Release|x86.Build.0 = Release|Any CPU + {2B11781D-13B0-4602-A6B4-96881F670CC0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2B11781D-13B0-4602-A6B4-96881F670CC0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2B11781D-13B0-4602-A6B4-96881F670CC0}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {2B11781D-13B0-4602-A6B4-96881F670CC0}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {2B11781D-13B0-4602-A6B4-96881F670CC0}.Debug|x86.ActiveCfg = Debug|Any CPU + {2B11781D-13B0-4602-A6B4-96881F670CC0}.Debug|x86.Build.0 = Debug|Any CPU + {2B11781D-13B0-4602-A6B4-96881F670CC0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2B11781D-13B0-4602-A6B4-96881F670CC0}.Release|Any CPU.Build.0 = Release|Any CPU + {2B11781D-13B0-4602-A6B4-96881F670CC0}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {2B11781D-13B0-4602-A6B4-96881F670CC0}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {2B11781D-13B0-4602-A6B4-96881F670CC0}.Release|x86.ActiveCfg = Release|Any CPU + {2B11781D-13B0-4602-A6B4-96881F670CC0}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -2184,6 +2240,10 @@ Global {B50A44CE-5F54-4559-90E0-1AF81655E944} = {36612AF5-23DB-46C0-9EB5-0F21911741F3} {E66CA4D7-8339-4674-B102-BC7CA890B7C5} = {36612AF5-23DB-46C0-9EB5-0F21911741F3} {2FADCED2-8563-4261-9AA8-514E5888075F} = {36612AF5-23DB-46C0-9EB5-0F21911741F3} + {BD636016-AC13-45BA-85CB-2A077568EDFA} = {202BA107-89D5-4868-AC5A-3527114C0109} + {838E3E45-7546-4EC2-B4C5-0CB239A3BD58} = {202BA107-89D5-4868-AC5A-3527114C0109} + {B64979D7-5FAD-4C5E-BCFC-B7FFA9C962C4} = {36612AF5-23DB-46C0-9EB5-0F21911741F3} + {2B11781D-13B0-4602-A6B4-96881F670CC0} = {36612AF5-23DB-46C0-9EB5-0F21911741F3} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {8B7C7E31-2E32-4E0D-9426-BC9AF22E9F4C} diff --git a/samples/WebAPI_Dapper/GreetingsWeb/Database/SchemaCreation.cs b/samples/WebAPI_Dapper/GreetingsWeb/Database/SchemaCreation.cs index d628c0bdb4..9ced14b6d6 100644 --- a/samples/WebAPI_Dapper/GreetingsWeb/Database/SchemaCreation.cs +++ b/samples/WebAPI_Dapper/GreetingsWeb/Database/SchemaCreation.cs @@ -10,7 +10,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,13 +30,13 @@ 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(GetDbConnection(dbType, connectionString)); return webHost; } @@ -109,34 +112,44 @@ private static void CreateOutboxDevelopment(string connectionString) 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(); - } - - private static void CreateOutboxProduction(DatabaseType databaseType, string connectionString) - { + private static void CreateOutboxProduction(DatabaseType databaseType, string connectionString) + { switch (databaseType) { case DatabaseType.MySql: CreateOutboxMySql(connectionString); break; + case DatabaseType.MsSql: + CreateOutboxMsSql(connectionString); + break; + case DatabaseType.Postgres: + CreateOutboxPostgres(connectionString); + break; + case DatabaseType.Sqlite: + CreateOutboxSqlite(connectionString); + break; default: throw new InvalidOperationException("Could not create instance of Outbox for unknown Db type"); } } + private static void CreateOutboxMsSql(string connectionString) + { + using var sqlConnection = new SqlConnection(connectionString); + sqlConnection.Open(); + + using var existsQuery = sqlConnection.CreateCommand(); + existsQuery.CommandText = SqlOutboxBuilder.GetExistsQuery(OUTBOX_TABLE_NAME); + bool exists = existsQuery.ExecuteScalar() != null; + + if (exists) return; + + using var command = sqlConnection.CreateCommand(); + command.CommandText = SqlOutboxBuilder.GetDDL(OUTBOX_TABLE_NAME); + command.ExecuteScalar(); + + } + private static void CreateOutboxMySql(string connectionString) { using var sqlConnection = new MySqlConnection(connectionString); @@ -152,21 +165,50 @@ private static void CreateOutboxMySql(string connectionString) command.CommandText = MySqlOutboxBuilder.GetDDL(OUTBOX_TABLE_NAME); command.ExecuteScalar(); } + + private static void CreateOutboxPostgres(string connectionString) + { + using var sqlConnection = new NpgsqlConnection(connectionString); + sqlConnection.Open(); + + using var existsQuery = sqlConnection.CreateCommand(); + existsQuery.CommandText = PostgreSqlOutboxBulder.GetExistsQuery(OUTBOX_TABLE_NAME); + bool exists = existsQuery.ExecuteScalar() != null; + + if (exists) return; + + using var command = sqlConnection.CreateCommand(); + command.CommandText = PostgreSqlOutboxBulder.GetDDL(OUTBOX_TABLE_NAME); + command.ExecuteScalar(); + } - private static string DbConnectionString(IConfiguration config, IWebHostEnvironment env) + private static void CreateOutboxSqlite(string connectionString) { - //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); + 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 +221,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 +232,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 +245,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 +264,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 +275,21 @@ 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) + }; + } } } diff --git a/samples/WebAPI_Dapper/GreetingsWeb/GreetingsWeb.csproj b/samples/WebAPI_Dapper/GreetingsWeb/GreetingsWeb.csproj index fb7ffc1ade..3dd483522d 100644 --- a/samples/WebAPI_Dapper/GreetingsWeb/GreetingsWeb.csproj +++ b/samples/WebAPI_Dapper/GreetingsWeb/GreetingsWeb.csproj @@ -8,6 +8,7 @@ + @@ -17,10 +18,14 @@ + + + + diff --git a/samples/WebAPI_Dapper/GreetingsWeb/Properties/launchSettings.json b/samples/WebAPI_Dapper/GreetingsWeb/Properties/launchSettings.json index 1186c52e1f..26a2d04afa 100644 --- a/samples/WebAPI_Dapper/GreetingsWeb/Properties/launchSettings.json +++ b/samples/WebAPI_Dapper/GreetingsWeb/Properties/launchSettings.json @@ -30,6 +30,17 @@ "ASPNETCORE_ENVIRONMENT": "Production", "BRIGHTER_GREETINGS_DATABASE": "MySQL" } + }, + "ProductionPostgres": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Production", + "BRIGHTER_GREETINGS_DATABASE": "PostgresSQL" + } } } } diff --git a/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs b/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs index 3ef9916cbb..3ce43ecf3a 100644 --- a/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs +++ b/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs @@ -1,9 +1,9 @@ using System; -using System.Data.Common; using DapperExtensions; using DapperExtensions.Sql; using FluentMigrator.Runner; using Greetings_MySqlMigrations.Migrations; +using Greetings_PostgreSqlMigrations.Migrations; using Greetings_SqliteMigrations.Migrations; using GreetingsPorts.EntityMappers; using GreetingsPorts.Handlers; @@ -28,17 +28,17 @@ namespace GreetingsWeb { public class Startup { - private const string _outBoxTableName = "Outbox"; - private IWebHostEnvironment _env; + private const string OUTBOX_TABLE_NAME = "Outbox"; + + private readonly IConfiguration _configuration; + private readonly IWebHostEnvironment _env; public Startup(IConfiguration configuration, IWebHostEnvironment env) { - Configuration = configuration; + _configuration = configuration; _env = env; } - public IConfiguration Configuration { get; } - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { @@ -73,28 +73,18 @@ public void ConfigureServices(IServiceCollection services) }); ConfigureMigration(services); - ConfigureDapper(services); + ConfigureDapper(); 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) @@ -104,11 +94,30 @@ 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 ConfigureMsSql(IServiceCollection services) + { + services + .AddFluentMigratorCore() + .ConfigureRunner(c => c.AddSqlServer() + .WithGlobalConnectionString(DbConnectionString()) + .ScanIn(typeof(MsSqlInitialCreate).Assembly).For.Migrations() + ); + } + private void ConfigureMySql(IServiceCollection services) { services @@ -119,46 +128,87 @@ private void ConfigureMySql(IServiceCollection services) ); } - private void ConfigureDapper(IServiceCollection services) + private void ConfigurePostgreSql(IServiceCollection services) + { + //TODO: add Postgres migrations + services + .AddFluentMigratorCore() + .ConfigureRunner(c => c.AddMySql5() + .WithGlobalConnectionString(DbConnectionString()) + .ScanIn(typeof(PostgreSqlInitialCreate).Assembly).For.Migrations() + ); + } + + private void ConfigureSqlite(IServiceCollection services) + { + services + .AddFluentMigratorCore() + .ConfigureRunner(c => + { + c.AddSQLite() + .WithGlobalConnectionString(DbConnectionString()) + .ScanIn(typeof(SqlliteInitialCreate).Assembly).For.Migrations(); + }); + } + + private void ConfigureDapper() { - ConfigureDapperByHost(GetDatabaseType(), services); + ConfigureDapperByHost(GetDatabaseType()); DapperExtensions.DapperExtensions.SetMappingAssemblies(new[] { typeof(PersonMapper).Assembly }); DapperAsyncExtensions.SetMappingAssemblies(new[] { typeof(PersonMapper).Assembly }); } - private static void ConfigureDapperByHost(DatabaseType databaseType, IServiceCollection services) + private static void ConfigureDapperByHost(DatabaseType databaseType) { switch (databaseType) { case DatabaseType.Sqlite: - ConfigureDapperSqlite(services); + ConfigureDapperSqlite(); break; case DatabaseType.MySql: - ConfigureDapperMySql(services); + ConfigureDapperMySql(); + break; + case DatabaseType.MsSql: + ConfigureDapperMsSql(); + break; + case DatabaseType.Postgres: + ConfigureDapperPostgreSql(); break; default: throw new ArgumentOutOfRangeException(nameof(databaseType), "Database type is not supported"); } } - private static void ConfigureDapperSqlite(IServiceCollection services) + private static void ConfigureDapperMsSql() + { + DapperExtensions.DapperExtensions.SqlDialect = new SqlServerDialect(); + DapperAsyncExtensions.SqlDialect = new SqlServerDialect(); + } + + private static void ConfigureDapperSqlite() { DapperExtensions.DapperExtensions.SqlDialect = new SqliteDialect(); DapperAsyncExtensions.SqlDialect = new SqliteDialect(); } - private static void ConfigureDapperMySql(IServiceCollection services) + private static void ConfigureDapperMySql() { DapperExtensions.DapperExtensions.SqlDialect = new MySqlDialect(); DapperAsyncExtensions.SqlDialect = new MySqlDialect(); } + private static void ConfigureDapperPostgreSql() + { + DapperExtensions.DapperExtensions.SqlDialect = new PostgreSqlDialect(); + DapperAsyncExtensions.SqlDialect = new PostgreSqlDialect(); + } + private void ConfigureBrighter(IServiceCollection services) { var outboxConfiguration = new RelationalDatabaseConfiguration( DbConnectionString(), - outBoxTableName:_outBoxTableName + outBoxTableName:OUTBOX_TABLE_NAME ); services.AddSingleton(outboxConfiguration); @@ -225,7 +275,7 @@ 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, @@ -244,7 +294,10 @@ private string GetConnectionString(DatabaseType databaseType) { return databaseType switch { - DatabaseType.MySql => Configuration.GetConnectionString("GreetingsMySql"), + DatabaseType.MySql => _configuration.GetConnectionString("GreetingsMySql"), + DatabaseType.MsSql => _configuration.GetConnectionString("GreetingsMsSql"), + DatabaseType.Postgres => _configuration.GetConnectionString("GreetingsPostgreSql"), + DatabaseType.Sqlite => GetDevDbConnectionString(), _ => throw new InvalidOperationException("Could not determine the database type") }; } diff --git a/samples/WebAPI_Dapper/GreetingsWeb/appsettings.Production.json b/samples/WebAPI_Dapper/GreetingsWeb/appsettings.Production.json index c02f13f346..3ad297d5da 100644 --- a/samples/WebAPI_Dapper/GreetingsWeb/appsettings.Production.json +++ b/samples/WebAPI_Dapper/GreetingsWeb/appsettings.Production.json @@ -8,6 +8,10 @@ }, "ConnectionStrings": { "GreetingsMySql": "server=localhost; port=3306; uid=root; pwd=root; database=Greetings", - "GreetingsMySqlDb": "server=localhost; port=3306; uid=root; pwd=root" + "MySqlDb": "server=localhost; port=3306; uid=root; pwd=root", + "GreetingsPostgreSql": "Host=localhost;Port=5432;Username=postgres;Password=password;Database=Greetings", + "PostgreSqlDb": "Host=localhost;Port=5432;Username=postgres;Password=password", + "GreetingsMsSql": "Server=localhost,1433;Username=sa;Password=Password123!;Database=Greetings;", + "MsSqlDb": "Server=localhost,1433;Username=sa;Password=Password123!" } } \ No newline at end of file diff --git a/samples/WebAPI_Dapper/Greetings_MsSqlMigrations/Greetings_MsSqlMigrations.csproj b/samples/WebAPI_Dapper/Greetings_MsSqlMigrations/Greetings_MsSqlMigrations.csproj new file mode 100644 index 0000000000..9107f11c61 --- /dev/null +++ b/samples/WebAPI_Dapper/Greetings_MsSqlMigrations/Greetings_MsSqlMigrations.csproj @@ -0,0 +1,13 @@ + + + + net6.0 + enable + enable + + + + + + + diff --git a/samples/WebAPI_Dapper/Greetings_MsSqlMigrations/Migrations/202306041332_InitialCreate.cs b/samples/WebAPI_Dapper/Greetings_MsSqlMigrations/Migrations/202306041332_InitialCreate.cs new file mode 100644 index 0000000000..e6db18eccc --- /dev/null +++ b/samples/WebAPI_Dapper/Greetings_MsSqlMigrations/Migrations/202306041332_InitialCreate.cs @@ -0,0 +1,30 @@ +using FluentMigrator; + +namespace Greetings_PostgreSqlMigrations.Migrations; + +[Migration(1)] +public class MsSqlInitialCreate : 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/Greetings_PostgreSqlMigrations/Greetings_PostgreSqlMigrations.csproj b/samples/WebAPI_Dapper/Greetings_PostgreSqlMigrations/Greetings_PostgreSqlMigrations.csproj new file mode 100644 index 0000000000..78d464b3d5 --- /dev/null +++ b/samples/WebAPI_Dapper/Greetings_PostgreSqlMigrations/Greetings_PostgreSqlMigrations.csproj @@ -0,0 +1,13 @@ + + + + net6.0 + enable + enable + + + + + + + diff --git a/samples/WebAPI_Dapper/Greetings_PostgreSqlMigrations/Migrations/202306031734_InitialCreate.cs b/samples/WebAPI_Dapper/Greetings_PostgreSqlMigrations/Migrations/202306031734_InitialCreate.cs new file mode 100644 index 0000000000..7ada805d2b --- /dev/null +++ b/samples/WebAPI_Dapper/Greetings_PostgreSqlMigrations/Migrations/202306031734_InitialCreate.cs @@ -0,0 +1,30 @@ +using FluentMigrator; + +namespace Greetings_PostgreSqlMigrations.Migrations; + +[Migration(1)] +public class PostgreSqlInitialCreate : 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_Kafka/GreetingsWeb/Database/SchemaCreation.cs b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/SchemaCreation.cs index 843c248b92..c979c115bc 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/SchemaCreation.cs +++ b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/SchemaCreation.cs @@ -10,7 +10,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,13 +30,13 @@ 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(GetDbConnection(dbType, connectionString)); return webHost; } @@ -61,7 +64,7 @@ public static IHost MigrateDatabase(this IHost webHost) return webHost; } - public static IHost CreateOutbox(this IHost webHost, bool hasBinaryPayload) + public static IHost CreateOutbox(this IHost webHost, bool hasBinaryPayload = false) { using (var scope = webHost.Services.CreateScope()) { @@ -92,9 +95,9 @@ private static void CreateOutbox(IConfiguration config, IWebHostEnvironment env, var connectionString = DbConnectionString(config, env); if (env.IsDevelopment()) - CreateOutboxDevelopment(connectionString, hasBinaryPayload: hasBinaryPayload); + CreateOutboxDevelopment(connectionString, hasBinaryPayload); else - CreateOutboxProduction(GetDatabaseType(config), connectionString, hasBinaryPayload: hasBinaryPayload); + CreateOutboxProduction(GetDatabaseType(config), connectionString, hasBinaryPayload); } catch (System.Exception e) { @@ -109,34 +112,45 @@ private static void CreateOutboxDevelopment(string connectionString, bool hasBin CreateOutboxSqlite(connectionString, hasBinaryPayload); } - private static void CreateOutboxSqlite(string connectionString, bool hasBinaryPayload) - { - 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: hasBinaryPayload); - command.ExecuteScalar(); - } - - private static void CreateOutboxProduction(DatabaseType databaseType, string connectionString, bool hasBinaryPayload) - { + private static void CreateOutboxProduction(DatabaseType databaseType, string connectionString, + bool hasBinaryPayload) + { switch (databaseType) { case DatabaseType.MySql: 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 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); + bool exists = existsQuery.ExecuteScalar() != null; + + 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); @@ -149,24 +163,53 @@ private static void CreateOutboxMySql(string connectionString, bool hasBinaryPay if (exists) return; using var command = sqlConnection.CreateCommand(); - command.CommandText = MySqlOutboxBuilder.GetDDL(OUTBOX_TABLE_NAME, hasBinaryMessagePayload: hasBinaryPayload); + 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); + bool exists = existsQuery.ExecuteScalar() != null; + + 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 +222,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 +233,10 @@ private static string GetProductionConnectionString(IConfiguration config, Datab { return databaseType switch { - DatabaseType.MySql => config.GetConnectionString("GreetingsMySqlDb"), + 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 +246,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 +265,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 +276,21 @@ 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) + }; + } } } diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/GreetingsWeb.csproj b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/GreetingsWeb.csproj index e98e6f3cb1..de68323afb 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/GreetingsWeb.csproj +++ b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/GreetingsWeb.csproj @@ -8,6 +8,7 @@ + @@ -17,10 +18,14 @@ + + + + diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs index 8203de1585..1ec1d4543c 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs +++ b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs @@ -1,10 +1,10 @@ using System; -using System.Data.Common; using Confluent.SchemaRegistry; using DapperExtensions; using DapperExtensions.Sql; using FluentMigrator.Runner; using Greetings_MySqlMigrations.Migrations; +using Greetings_PostgreSqlMigrations.Migrations; using Greetings_SqliteMigrations.Migrations; using GreetingsPorts.EntityMappers; using GreetingsPorts.Handlers; @@ -21,8 +21,6 @@ using Paramore.Brighter.Extensions.DependencyInjection; using Paramore.Brighter.Extensions.Hosting; using Paramore.Brighter.MessagingGateway.Kafka; -using Paramore.Brighter.MySql; -using Paramore.Brighter.Sqlite; using Paramore.Darker.AspNetCore; using Paramore.Darker.Policies; using Paramore.Darker.QueryLogging; @@ -31,17 +29,17 @@ namespace GreetingsWeb { public class Startup { - private const string _outBoxTableName = "Outbox"; - private IWebHostEnvironment _env; + private const string OUTBOX_TABLE_NAME = "Outbox"; + + private readonly IConfiguration _configuration; + private readonly IWebHostEnvironment _env; public Startup(IConfiguration configuration, IWebHostEnvironment env) { - Configuration = configuration; + _configuration = configuration; _env = env; } - public IConfiguration Configuration { get; } - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { @@ -76,28 +74,18 @@ public void ConfigureServices(IServiceCollection services) }); ConfigureMigration(services); - ConfigureDapper(services); + ConfigureDapper(); 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) @@ -107,11 +95,30 @@ 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 ConfigureMsSql(IServiceCollection services) + { + services + .AddFluentMigratorCore() + .ConfigureRunner(c => c.AddSqlServer() + .WithGlobalConnectionString(DbConnectionString()) + .ScanIn(typeof(MsSqlInitialCreate).Assembly).For.Migrations() + ); + } + private void ConfigureMySql(IServiceCollection services) { services @@ -122,46 +129,87 @@ private void ConfigureMySql(IServiceCollection services) ); } - private void ConfigureDapper(IServiceCollection services) + private void ConfigurePostgreSql(IServiceCollection services) { - ConfigureDapperByHost(GetDatabaseType(), services); + //TODO: add Postgres migrations + services + .AddFluentMigratorCore() + .ConfigureRunner(c => c.AddMySql5() + .WithGlobalConnectionString(DbConnectionString()) + .ScanIn(typeof(PostgreSqlInitialCreate).Assembly).For.Migrations() + ); + } + + private void ConfigureSqlite(IServiceCollection services) + { + services + .AddFluentMigratorCore() + .ConfigureRunner(c => + { + c.AddSQLite() + .WithGlobalConnectionString(DbConnectionString()) + .ScanIn(typeof(SqlliteInitialCreate).Assembly).For.Migrations(); + }); + } + + private void ConfigureDapper() + { + ConfigureDapperByHost(GetDatabaseType()); DapperExtensions.DapperExtensions.SetMappingAssemblies(new[] { typeof(PersonMapper).Assembly }); DapperAsyncExtensions.SetMappingAssemblies(new[] { typeof(PersonMapper).Assembly }); } - private static void ConfigureDapperByHost(DatabaseType databaseType, IServiceCollection services) + private static void ConfigureDapperByHost(DatabaseType databaseType) { switch (databaseType) { case DatabaseType.Sqlite: - ConfigureDapperSqlite(services); + ConfigureDapperSqlite(); break; case DatabaseType.MySql: - ConfigureDapperMySql(services); + ConfigureDapperMySql(); + break; + case DatabaseType.MsSql: + ConfigureDapperMsSql(); + break; + case DatabaseType.Postgres: + ConfigureDapperPostgreSql(); break; default: throw new ArgumentOutOfRangeException(nameof(databaseType), "Database type is not supported"); } } - private static void ConfigureDapperSqlite(IServiceCollection services) + private static void ConfigureDapperMsSql() + { + DapperExtensions.DapperExtensions.SqlDialect = new SqlServerDialect(); + DapperAsyncExtensions.SqlDialect = new SqlServerDialect(); + } + + private static void ConfigureDapperSqlite() { DapperExtensions.DapperExtensions.SqlDialect = new SqliteDialect(); DapperAsyncExtensions.SqlDialect = new SqliteDialect(); } - private static void ConfigureDapperMySql(IServiceCollection services) + private static void ConfigureDapperMySql() { DapperExtensions.DapperExtensions.SqlDialect = new MySqlDialect(); DapperAsyncExtensions.SqlDialect = new MySqlDialect(); + } + + private static void ConfigureDapperPostgreSql() + { + DapperExtensions.DapperExtensions.SqlDialect = new PostgreSqlDialect(); + DapperAsyncExtensions.SqlDialect = new PostgreSqlDialect(); } private void ConfigureBrighter(IServiceCollection services) { var outboxConfiguration = new RelationalDatabaseConfiguration( DbConnectionString(), - outBoxTableName: _outBoxTableName, + outBoxTableName: OUTBOX_TABLE_NAME, //NOTE: With the Serdes serializer, if we don't use a binary payload, the payload will be corrupted binaryMessagePayload: true ); @@ -215,7 +263,6 @@ private void ConfigureBrighter(IServiceCollection services) .AutoFromAssemblies(typeof(AddPersonHandlerAsync).Assembly); } - private void ConfigureDarker(IServiceCollection services) { services.AddDarker(options => @@ -236,7 +283,7 @@ 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, @@ -255,7 +302,10 @@ private string GetConnectionString(DatabaseType databaseType) { return databaseType switch { - DatabaseType.MySql => Configuration.GetConnectionString("GreetingsMySql"), + DatabaseType.MySql => _configuration.GetConnectionString("GreetingsMySql"), + DatabaseType.MsSql => _configuration.GetConnectionString("GreetingsMsSql"), + DatabaseType.Postgres => _configuration.GetConnectionString("GreetingsPostgreSql"), + DatabaseType.Sqlite => GetDevDbConnectionString(), _ => throw new InvalidOperationException("Could not determine the database type") }; } diff --git a/samples/WebAPI_Dapper_Kafka/Greetings_MsSqlMigrations/Greetings_MsSqlMigrations.csproj b/samples/WebAPI_Dapper_Kafka/Greetings_MsSqlMigrations/Greetings_MsSqlMigrations.csproj new file mode 100644 index 0000000000..9107f11c61 --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/Greetings_MsSqlMigrations/Greetings_MsSqlMigrations.csproj @@ -0,0 +1,13 @@ + + + + net6.0 + enable + enable + + + + + + + diff --git a/samples/WebAPI_Dapper_Kafka/Greetings_MsSqlMigrations/Migrations/202306041332_InitialCreate.cs b/samples/WebAPI_Dapper_Kafka/Greetings_MsSqlMigrations/Migrations/202306041332_InitialCreate.cs new file mode 100644 index 0000000000..e6db18eccc --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/Greetings_MsSqlMigrations/Migrations/202306041332_InitialCreate.cs @@ -0,0 +1,30 @@ +using FluentMigrator; + +namespace Greetings_PostgreSqlMigrations.Migrations; + +[Migration(1)] +public class MsSqlInitialCreate : 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_Kafka/Greetings_PostgreSqlMigrations/Greetings_PostgreSqlMigrations.csproj b/samples/WebAPI_Dapper_Kafka/Greetings_PostgreSqlMigrations/Greetings_PostgreSqlMigrations.csproj new file mode 100644 index 0000000000..78d464b3d5 --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/Greetings_PostgreSqlMigrations/Greetings_PostgreSqlMigrations.csproj @@ -0,0 +1,13 @@ + + + + net6.0 + enable + enable + + + + + + + diff --git a/samples/WebAPI_Dapper_Kafka/Greetings_PostgreSqlMigrations/Migrations/202306031734_InitialCreate.cs b/samples/WebAPI_Dapper_Kafka/Greetings_PostgreSqlMigrations/Migrations/202306031734_InitialCreate.cs new file mode 100644 index 0000000000..7ada805d2b --- /dev/null +++ b/samples/WebAPI_Dapper_Kafka/Greetings_PostgreSqlMigrations/Migrations/202306031734_InitialCreate.cs @@ -0,0 +1,30 @@ +using FluentMigrator; + +namespace Greetings_PostgreSqlMigrations.Migrations; + +[Migration(1)] +public class PostgreSqlInitialCreate : 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/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutboxBulder.cs b/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutboxBulder.cs index c5b001b7a7..d910953074 100644 --- a/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutboxBulder.cs +++ b/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutboxBulder.cs @@ -73,7 +73,7 @@ Body bytea NULL /// The name you want to use for the table /// /// The required DDL - public static string GetDDL(string outboxTableName, bool binaryMessagePayload) + public static string GetDDL(string outboxTableName, bool binaryMessagePayload = false) { return binaryMessagePayload ? string.Format(BinaryOutboxDdl, outboxTableName) : string.Format(TextOutboxDdl, outboxTableName); } From edb94ce1573a8192da789e62fa3490589b157992 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Thu, 8 Jun 2023 07:39:57 -0600 Subject: [PATCH 64/89] Dapper support for all dbs --- .../GreetingsWeb/GreetingsWeb.csproj | 4 ++++ .../Properties/launchSettings.json | 13 +++++++++- samples/WebAPI_Dapper/GreetingsWeb/Startup.cs | 10 ++++++++ .../GreetingsWeb/GreetingsWeb.csproj | 6 ++++- .../Properties/launchSettings.json | 24 ++++++++++++++++++- .../GreetingsWeb/Startup.cs | 10 ++++++++ 6 files changed, 64 insertions(+), 3 deletions(-) diff --git a/samples/WebAPI_Dapper/GreetingsWeb/GreetingsWeb.csproj b/samples/WebAPI_Dapper/GreetingsWeb/GreetingsWeb.csproj index 3dd483522d..9d96ae2d7e 100644 --- a/samples/WebAPI_Dapper/GreetingsWeb/GreetingsWeb.csproj +++ b/samples/WebAPI_Dapper/GreetingsWeb/GreetingsWeb.csproj @@ -9,6 +9,10 @@ + + + + diff --git a/samples/WebAPI_Dapper/GreetingsWeb/Properties/launchSettings.json b/samples/WebAPI_Dapper/GreetingsWeb/Properties/launchSettings.json index 26a2d04afa..57c58ccaa2 100644 --- a/samples/WebAPI_Dapper/GreetingsWeb/Properties/launchSettings.json +++ b/samples/WebAPI_Dapper/GreetingsWeb/Properties/launchSettings.json @@ -20,7 +20,7 @@ "BRIGHTER_GREETINGS_DATABASE": "Sqlite" } }, - "Production": { + "ProductionMySql": { "commandName": "Project", "dotnetRunMessages": "true", "launchBrowser": true, @@ -40,6 +40,17 @@ "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Production", "BRIGHTER_GREETINGS_DATABASE": "PostgresSQL" + }, + "ProductionMsSql": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Production", + "BRIGHTER_GREETINGS_DATABASE": "MySQL" + } } } } diff --git a/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs b/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs index 3ce43ecf3a..9111337743 100644 --- a/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs +++ b/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs @@ -16,6 +16,8 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.OpenApi.Models; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; using Paramore.Brighter; using Paramore.Brighter.Extensions.DependencyInjection; using Paramore.Brighter.Extensions.Hosting; @@ -72,6 +74,14 @@ 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(); ConfigureBrighter(services); diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/GreetingsWeb.csproj b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/GreetingsWeb.csproj index de68323afb..0eced8cda6 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/GreetingsWeb.csproj +++ b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/GreetingsWeb.csproj @@ -9,8 +9,12 @@ + + + + - + diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Properties/launchSettings.json b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Properties/launchSettings.json index 1186c52e1f..57c58ccaa2 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Properties/launchSettings.json +++ b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Properties/launchSettings.json @@ -20,7 +20,7 @@ "BRIGHTER_GREETINGS_DATABASE": "Sqlite" } }, - "Production": { + "ProductionMySql": { "commandName": "Project", "dotnetRunMessages": "true", "launchBrowser": true, @@ -30,6 +30,28 @@ "ASPNETCORE_ENVIRONMENT": "Production", "BRIGHTER_GREETINGS_DATABASE": "MySQL" } + }, + "ProductionPostgres": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Production", + "BRIGHTER_GREETINGS_DATABASE": "PostgresSQL" + }, + "ProductionMsSql": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Production", + "BRIGHTER_GREETINGS_DATABASE": "MySQL" + } + } } } } diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs index 1ec1d4543c..4aa1f7ed06 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs +++ b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs @@ -17,6 +17,8 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.OpenApi.Models; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; using Paramore.Brighter; using Paramore.Brighter.Extensions.DependencyInjection; using Paramore.Brighter.Extensions.Hosting; @@ -72,6 +74,14 @@ 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(); From af7ed44bf2ee94635d5fc3cc00feace66b4f7021 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Fri, 9 Jun 2023 16:53:59 -0600 Subject: [PATCH 65/89] safety dance --- .../.idea/httpRequests/http-requests-log.http | 228 +++++++++--------- Brighter.sln | 47 +--- .../Handlers/AddPersonHandlerAsync.cs | 12 +- .../FIndGreetingsForPersonHandlerAsync.cs | 34 ++- .../Handlers/FindPersonByNameHandlerAsync.cs | 12 +- .../GreetingsWeb/Database/OutboxExtensions.cs | 28 ++- .../GreetingsWeb/Database/SchemaCreation.cs | 48 +++- .../GreetingsWeb/GreetingsWeb.csproj | 5 +- .../Properties/launchSettings.json | 24 +- samples/WebAPI_Dapper/GreetingsWeb/Startup.cs | 18 +- .../GreetingsWeb/appsettings.Production.json | 4 +- .../Greetings_Migrations.csproj} | 1 + .../Migrations/20220527_InitialCreate.cs | 2 +- .../Greetings_SqliteMigrations.csproj | 13 - .../Migrations/202204221833_InitialCreate.cs | 30 --- .../Orders.API/Program.cs | 2 +- ...qlAuthUnitOfWork.cs => MsSqlUnitOfWork.cs} | 4 +- .../PostgreSqlOutbox.cs | 11 +- ...pgsqlUnitOfWork.cs => NpgsqlUnitOfWork.cs} | 4 +- 19 files changed, 237 insertions(+), 290 deletions(-) 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 (95%) delete mode 100644 samples/WebAPI_Dapper/Greetings_SqliteMigrations/Greetings_SqliteMigrations.csproj delete mode 100644 samples/WebAPI_Dapper/Greetings_SqliteMigrations/Migrations/202204221833_InitialCreate.cs rename src/Paramore.Brighter.MsSql/{MsSqlSqlAuthUnitOfWork.cs => MsSqlUnitOfWork.cs} (94%) rename src/Paramore.Brighter.PostgreSql/{PostgreSqlNpgsqlUnitOfWork.cs => NpgsqlUnitOfWork.cs} (95%) diff --git a/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http b/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http index b3a44174ec..0a54c4581c 100644 --- a/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http +++ b/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http @@ -1,3 +1,120 @@ +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" +} + +<> 2023-06-09T164917.500.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-06-09T164624.500.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" +} + +<> 2023-06-09T164620.500.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-06-09T154849.500.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" +} + +<> 2023-06-09T154836.500.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" +} + +<> 2023-06-09T154833.500.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" +} + +<> 2023-06-09T154824.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" +} + +<> 2023-06-08T222134.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-06-08T222121.200.json + +### + POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json Content-Length: 47 @@ -521,114 +638,3 @@ Accept-Encoding: br,deflate,gzip,x-gzip ### -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-05-11T083445.500.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-05-11T083438.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-05-11T083357.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-05-11T083353.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-05-11T082416.500.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-05-11T082408.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-05-11T082332.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-05-11T082326.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" -} - -<> 2023-05-10T193837.500.json - -### - diff --git a/Brighter.sln b/Brighter.sln index a0178f5408..afd60ef0db 100644 --- a/Brighter.sln +++ b/Brighter.sln @@ -253,11 +253,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GreetingsEntities", "sample 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}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Greetings_Migrations", "samples\WebAPI_Dapper\Greetings_Migrations\Greetings_Migrations.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}" EndProject @@ -356,10 +354,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Salutations_mySqlMigrations EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Greetings_MySqlMigrations", "samples\WebAPI_Dapper_Kafka\Greetings_MySqlMigrations\Greetings_MySqlMigrations.csproj", "{2FADCED2-8563-4261-9AA8-514E5888075F}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Greetings_PostgreSqlMigrations", "samples\WebAPI_Dapper\Greetings_PostgreSqlMigrations\Greetings_PostgreSqlMigrations.csproj", "{BD636016-AC13-45BA-85CB-2A077568EDFA}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Greetings_MsSqlMigrations", "samples\WebAPI_Dapper\Greetings_MsSqlMigrations\Greetings_MsSqlMigrations.csproj", "{838E3E45-7546-4EC2-B4C5-0CB239A3BD58}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Greetings_PostgreSqlMigrations", "samples\WebAPI_Dapper_Kafka\Greetings_PostgreSqlMigrations\Greetings_PostgreSqlMigrations.csproj", "{B64979D7-5FAD-4C5E-BCFC-B7FFA9C962C4}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Greetings_MsSqlMigrations", "samples\WebAPI_Dapper_Kafka\Greetings_MsSqlMigrations\Greetings_MsSqlMigrations.csproj", "{2B11781D-13B0-4602-A6B4-96881F670CC0}" @@ -1538,18 +1532,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 @@ -2066,30 +2048,6 @@ Global {2FADCED2-8563-4261-9AA8-514E5888075F}.Release|Mixed Platforms.Build.0 = Release|Any CPU {2FADCED2-8563-4261-9AA8-514E5888075F}.Release|x86.ActiveCfg = Release|Any CPU {2FADCED2-8563-4261-9AA8-514E5888075F}.Release|x86.Build.0 = Release|Any CPU - {BD636016-AC13-45BA-85CB-2A077568EDFA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {BD636016-AC13-45BA-85CB-2A077568EDFA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {BD636016-AC13-45BA-85CB-2A077568EDFA}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {BD636016-AC13-45BA-85CB-2A077568EDFA}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {BD636016-AC13-45BA-85CB-2A077568EDFA}.Debug|x86.ActiveCfg = Debug|Any CPU - {BD636016-AC13-45BA-85CB-2A077568EDFA}.Debug|x86.Build.0 = Debug|Any CPU - {BD636016-AC13-45BA-85CB-2A077568EDFA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {BD636016-AC13-45BA-85CB-2A077568EDFA}.Release|Any CPU.Build.0 = Release|Any CPU - {BD636016-AC13-45BA-85CB-2A077568EDFA}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {BD636016-AC13-45BA-85CB-2A077568EDFA}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {BD636016-AC13-45BA-85CB-2A077568EDFA}.Release|x86.ActiveCfg = Release|Any CPU - {BD636016-AC13-45BA-85CB-2A077568EDFA}.Release|x86.Build.0 = Release|Any CPU - {838E3E45-7546-4EC2-B4C5-0CB239A3BD58}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {838E3E45-7546-4EC2-B4C5-0CB239A3BD58}.Debug|Any CPU.Build.0 = Debug|Any CPU - {838E3E45-7546-4EC2-B4C5-0CB239A3BD58}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {838E3E45-7546-4EC2-B4C5-0CB239A3BD58}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {838E3E45-7546-4EC2-B4C5-0CB239A3BD58}.Debug|x86.ActiveCfg = Debug|Any CPU - {838E3E45-7546-4EC2-B4C5-0CB239A3BD58}.Debug|x86.Build.0 = Debug|Any CPU - {838E3E45-7546-4EC2-B4C5-0CB239A3BD58}.Release|Any CPU.ActiveCfg = Release|Any CPU - {838E3E45-7546-4EC2-B4C5-0CB239A3BD58}.Release|Any CPU.Build.0 = Release|Any CPU - {838E3E45-7546-4EC2-B4C5-0CB239A3BD58}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {838E3E45-7546-4EC2-B4C5-0CB239A3BD58}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {838E3E45-7546-4EC2-B4C5-0CB239A3BD58}.Release|x86.ActiveCfg = Release|Any CPU - {838E3E45-7546-4EC2-B4C5-0CB239A3BD58}.Release|x86.Build.0 = Release|Any CPU {B64979D7-5FAD-4C5E-BCFC-B7FFA9C962C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B64979D7-5FAD-4C5E-BCFC-B7FFA9C962C4}.Debug|Any CPU.Build.0 = Debug|Any CPU {B64979D7-5FAD-4C5E-BCFC-B7FFA9C962C4}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU @@ -2193,7 +2151,6 @@ 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} @@ -2240,8 +2197,6 @@ Global {B50A44CE-5F54-4559-90E0-1AF81655E944} = {36612AF5-23DB-46C0-9EB5-0F21911741F3} {E66CA4D7-8339-4674-B102-BC7CA890B7C5} = {36612AF5-23DB-46C0-9EB5-0F21911741F3} {2FADCED2-8563-4261-9AA8-514E5888075F} = {36612AF5-23DB-46C0-9EB5-0F21911741F3} - {BD636016-AC13-45BA-85CB-2A077568EDFA} = {202BA107-89D5-4868-AC5A-3527114C0109} - {838E3E45-7546-4EC2-B4C5-0CB239A3BD58} = {202BA107-89D5-4868-AC5A-3527114C0109} {B64979D7-5FAD-4C5E-BCFC-B7FFA9C962C4} = {36612AF5-23DB-46C0-9EB5-0F21911741F3} {2B11781D-13B0-4602-A6B4-96881F670CC0} = {36612AF5-23DB-46C0-9EB5-0F21911741F3} EndGlobalSection diff --git a/samples/WebAPI_Dapper/GreetingsPorts/Handlers/AddPersonHandlerAsync.cs b/samples/WebAPI_Dapper/GreetingsPorts/Handlers/AddPersonHandlerAsync.cs index d3361e0ee3..294f2302f5 100644 --- a/samples/WebAPI_Dapper/GreetingsPorts/Handlers/AddPersonHandlerAsync.cs +++ b/samples/WebAPI_Dapper/GreetingsPorts/Handlers/AddPersonHandlerAsync.cs @@ -13,20 +13,18 @@ public class AddPersonHandlerAsync : RequestHandlerAsync { private readonly IAmARelationalDbConnectionProvider _relationalDbConnectionProvider; - public AddPersonHandlerAsync(IAmARelationalDbConnectionProvider transactionConnectionProvider) + public AddPersonHandlerAsync(IAmARelationalDbConnectionProvider relationalDbConnectionProvider) { - _relationalDbConnectionProvider = transactionConnectionProvider; + _relationalDbConnectionProvider = relationalDbConnectionProvider; } [RequestLoggingAsync(0, HandlerTiming.Before)] [UsePolicyAsync(step:1, policy: Policies.Retry.EXPONENTIAL_RETRYPOLICYASYNC)] public override async Task HandleAsync(AddPerson addPerson, CancellationToken cancellationToken = default) { - using (var connection = await _relationalDbConnectionProvider.GetConnectionAsync(cancellationToken)) - { - await connection.InsertAsync(new Person(addPerson.Name)); - return await base.HandleAsync(addPerson, cancellationToken); - } + await using var connection = await _relationalDbConnectionProvider.GetConnectionAsync(cancellationToken); + await connection.InsertAsync(new Person(addPerson.Name)); + return await base.HandleAsync(addPerson, cancellationToken); } } } diff --git a/samples/WebAPI_Dapper/GreetingsPorts/Handlers/FIndGreetingsForPersonHandlerAsync.cs b/samples/WebAPI_Dapper/GreetingsPorts/Handlers/FIndGreetingsForPersonHandlerAsync.cs index 84a63f883c..542f2a9dbe 100644 --- a/samples/WebAPI_Dapper/GreetingsPorts/Handlers/FIndGreetingsForPersonHandlerAsync.cs +++ b/samples/WebAPI_Dapper/GreetingsPorts/Handlers/FIndGreetingsForPersonHandlerAsync.cs @@ -33,28 +33,26 @@ public FIndGreetingsForPersonHandlerAsync(IAmATransactionConnectionProvider tran var sql = @"select p.Id, p.Name, g.Id, g.Message from Person p inner join Greeting g on g.Recipient_Id = p.Id"; - using (var connection = await _transactionConnectionProvider.GetConnectionAsync(cancellationToken)) + await using var connection = await _transactionConnectionProvider.GetConnectionAsync(cancellationToken); + var people = await connection.QueryAsync(sql, (person, greeting) => { - var people = await connection.QueryAsync(sql, (person, greeting) => - { - person.Greetings.Add(greeting); - return person; - }, splitOn: "Id"); + person.Greetings.Add(greeting); + return person; + }, splitOn: "Id"); - var peopleGreetings = people.GroupBy(p => p.Id).Select(grp => - { - var groupedPerson = grp.First(); - groupedPerson.Greetings = grp.Select(p => p.Greetings.Single()).ToList(); - return groupedPerson; - }); + var peopleGreetings = people.GroupBy(p => p.Id).Select(grp => + { + var groupedPerson = grp.First(); + groupedPerson.Greetings = grp.Select(p => p.Greetings.Single()).ToList(); + return groupedPerson; + }); - var person = peopleGreetings.Single(); + var person = peopleGreetings.Single(); - return new FindPersonsGreetings - { - Name = person.Name, Greetings = person.Greetings.Select(g => new Salutation(g.Greet())) - }; - } + return new FindPersonsGreetings + { + 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 91fde6fae0..c4dfaad565 100644 --- a/samples/WebAPI_Dapper/GreetingsPorts/Handlers/FindPersonByNameHandlerAsync.cs +++ b/samples/WebAPI_Dapper/GreetingsPorts/Handlers/FindPersonByNameHandlerAsync.cs @@ -27,14 +27,12 @@ public FindPersonByNameHandlerAsync(IAmARelationalDbConnectionProvider relationa [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); - using (var connection = await _relationalDbConnectionProvider .GetConnectionAsync(cancellationToken)) - { - var people = await connection.GetListAsync(searchbyName); - var person = people.Single(); + var searchByName = Predicates.Field(p => p.Name, Operator.Eq, query.Name); + await using var connection = await _relationalDbConnectionProvider .GetConnectionAsync(cancellationToken); + var people = await connection.GetListAsync(searchByName); + var person = people.Single(); - return new FindPersonResult(person); - } + return new FindPersonResult(person); } } } diff --git a/samples/WebAPI_Dapper/GreetingsWeb/Database/OutboxExtensions.cs b/samples/WebAPI_Dapper/GreetingsWeb/Database/OutboxExtensions.cs index ea94c7383f..74466c75a7 100644 --- a/samples/WebAPI_Dapper/GreetingsWeb/Database/OutboxExtensions.cs +++ b/samples/WebAPI_Dapper/GreetingsWeb/Database/OutboxExtensions.cs @@ -5,9 +5,13 @@ using Paramore.Brighter; using Paramore.Brighter.Extensions.DependencyInjection; using Paramore.Brighter.Extensions.Hosting; +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 GreetingsWeb.Database @@ -27,19 +31,29 @@ public static (IAmAnOutbox, Type) MakeOutbox( } else { - switch (databaseType) + outbox = databaseType switch { - case DatabaseType.MySql: - outbox = MakeMySqlOutbox(configuration); - break; - default: - throw new InvalidOperationException("Unknown Db type for Outbox configuration"); - } + DatabaseType.MySql => MakeMySqlOutbox(configuration), + DatabaseType.MsSql => MakeMsSqlOutbox(configuration), + DatabaseType.Postgres => MakePostgresSqlOutbox(configuration), + DatabaseType.Sqlite => MakeSqliteOutBox(configuration), + _ => throw new InvalidOperationException("Unknown Db type for Outbox configuration") + }; } return outbox; } + private static (IAmAnOutbox, Type) MakePostgresSqlOutbox(RelationalDatabaseConfiguration configuration) + { + return (new PostgreSqlOutbox(configuration), typeof(NpgsqlUnitOfWork)); + } + + private static (IAmAnOutbox, Type) MakeMsSqlOutbox(RelationalDatabaseConfiguration configuration) + { + return new(new MsSqlOutbox(configuration), typeof(MsSqlUnitOfWork)); + } + private static (IAmAnOutbox, Type) MakeMySqlOutbox(RelationalDatabaseConfiguration configuration) { return (new MySqlOutbox(configuration), typeof(MySqlUnitOfWork)); diff --git a/samples/WebAPI_Dapper/GreetingsWeb/Database/SchemaCreation.cs b/samples/WebAPI_Dapper/GreetingsWeb/Database/SchemaCreation.cs index 9ced14b6d6..b28d6c6fc6 100644 --- a/samples/WebAPI_Dapper/GreetingsWeb/Database/SchemaCreation.cs +++ b/samples/WebAPI_Dapper/GreetingsWeb/Database/SchemaCreation.cs @@ -36,7 +36,7 @@ public static IHost CheckDbIsUp(this IHost webHost) if (env.IsDevelopment()) return webHost; WaitToConnect(dbType, connectionString); - CreateDatabaseIfNotExists(GetDbConnection(dbType, connectionString)); + CreateDatabaseIfNotExists(dbType, GetDbConnection(dbType, connectionString)); return webHost; } @@ -66,25 +66,42 @@ public static IHost MigrateDatabase(this IHost 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(); + using var scope = webHost.Services.CreateScope(); + var services = scope.ServiceProvider; + var env = services.GetService(); + var config = services.GetService(); - CreateOutbox(config, env); - } + 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(); + if (databaseType != DatabaseType.Postgres) + command.CommandText = "CREATE DATABASE IF NOT EXISTS Greetings"; + else + command.CommandText = "CREATE DATABASE Greetings"; + + 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; + } } @@ -99,6 +116,15 @@ private static void CreateOutbox(IConfiguration config, IWebHostEnvironment env) else CreateOutboxProduction(GetDatabaseType(config), connectionString); } + 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}"); diff --git a/samples/WebAPI_Dapper/GreetingsWeb/GreetingsWeb.csproj b/samples/WebAPI_Dapper/GreetingsWeb/GreetingsWeb.csproj index 9d96ae2d7e..c193e5d5b2 100644 --- a/samples/WebAPI_Dapper/GreetingsWeb/GreetingsWeb.csproj +++ b/samples/WebAPI_Dapper/GreetingsWeb/GreetingsWeb.csproj @@ -27,10 +27,7 @@ - - - - + diff --git a/samples/WebAPI_Dapper/GreetingsWeb/Properties/launchSettings.json b/samples/WebAPI_Dapper/GreetingsWeb/Properties/launchSettings.json index 57c58ccaa2..6ddca8d915 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, @@ -40,17 +40,17 @@ "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Production", "BRIGHTER_GREETINGS_DATABASE": "PostgresSQL" - }, - "ProductionMsSql": { - "commandName": "Project", - "dotnetRunMessages": "true", - "launchBrowser": true, - "launchUrl": "swagger", - "applicationUrl": "https://localhost:5001;http://localhost:5000", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Production", - "BRIGHTER_GREETINGS_DATABASE": "MySQL" - } + } + }, + "ProductionMsSql": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Production", + "BRIGHTER_GREETINGS_DATABASE": "MsSQL" } } } diff --git a/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs b/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs index 9111337743..de4abc8daa 100644 --- a/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs +++ b/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs @@ -3,8 +3,6 @@ using DapperExtensions.Sql; using FluentMigrator.Runner; using Greetings_MySqlMigrations.Migrations; -using Greetings_PostgreSqlMigrations.Migrations; -using Greetings_SqliteMigrations.Migrations; using GreetingsPorts.EntityMappers; using GreetingsPorts.Handlers; using GreetingsPorts.Policies; @@ -124,7 +122,7 @@ private void ConfigureMsSql(IServiceCollection services) .AddFluentMigratorCore() .ConfigureRunner(c => c.AddSqlServer() .WithGlobalConnectionString(DbConnectionString()) - .ScanIn(typeof(MsSqlInitialCreate).Assembly).For.Migrations() + .ScanIn(typeof(SqlInitialCreate).Assembly).For.Migrations() ); } @@ -134,7 +132,7 @@ private void ConfigureMySql(IServiceCollection services) .AddFluentMigratorCore() .ConfigureRunner(c => c.AddMySql5() .WithGlobalConnectionString(DbConnectionString()) - .ScanIn(typeof(MySqlInitialCreate).Assembly).For.Migrations() + .ScanIn(typeof(SqlInitialCreate).Assembly).For.Migrations() ); } @@ -143,9 +141,9 @@ private void ConfigurePostgreSql(IServiceCollection services) //TODO: add Postgres migrations services .AddFluentMigratorCore() - .ConfigureRunner(c => c.AddMySql5() + .ConfigureRunner(c => c.AddPostgres() .WithGlobalConnectionString(DbConnectionString()) - .ScanIn(typeof(PostgreSqlInitialCreate).Assembly).For.Migrations() + .ScanIn(typeof(SqlInitialCreate).Assembly).For.Migrations() ); } @@ -153,12 +151,10 @@ private void ConfigureSqlite(IServiceCollection services) { services .AddFluentMigratorCore() - .ConfigureRunner(c => - { - c.AddSQLite() + .ConfigureRunner(c => c.AddSQLite() .WithGlobalConnectionString(DbConnectionString()) - .ScanIn(typeof(SqlliteInitialCreate).Assembly).For.Migrations(); - }); + .ScanIn(typeof(SqlInitialCreate).Assembly).For.Migrations() + ); } private void ConfigureDapper() diff --git a/samples/WebAPI_Dapper/GreetingsWeb/appsettings.Production.json b/samples/WebAPI_Dapper/GreetingsWeb/appsettings.Production.json index 3ad297d5da..2a64a02529 100644 --- a/samples/WebAPI_Dapper/GreetingsWeb/appsettings.Production.json +++ b/samples/WebAPI_Dapper/GreetingsWeb/appsettings.Production.json @@ -9,8 +9,8 @@ "ConnectionStrings": { "GreetingsMySql": "server=localhost; port=3306; uid=root; pwd=root; database=Greetings", "MySqlDb": "server=localhost; port=3306; uid=root; pwd=root", - "GreetingsPostgreSql": "Host=localhost;Port=5432;Username=postgres;Password=password;Database=Greetings", - "PostgreSqlDb": "Host=localhost;Port=5432;Username=postgres;Password=password", + "GreetingsPostgreSql": "Server=localhost; Port=5432; Database=greetings; Username=postgres; Password=password;", + "PostgreSqlDb": "Server=localhost; Port=5432; Username=postgres; Password=password", "GreetingsMsSql": "Server=localhost,1433;Username=sa;Password=Password123!;Database=Greetings;", "MsSqlDb": "Server=localhost,1433;Username=sa;Password=Password123!" } 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 95% rename from samples/WebAPI_Dapper/Greetings_MySqlMigrations/Migrations/20220527_InitialCreate.cs rename to samples/WebAPI_Dapper/Greetings_Migrations/Migrations/20220527_InitialCreate.cs index 9fa274705a..7e6ee55a3e 100644 --- a/samples/WebAPI_Dapper/Greetings_MySqlMigrations/Migrations/20220527_InitialCreate.cs +++ b/samples/WebAPI_Dapper/Greetings_Migrations/Migrations/20220527_InitialCreate.cs @@ -3,7 +3,7 @@ namespace Greetings_MySqlMigrations.Migrations; [Migration(1)] -public class MySqlInitialCreate : Migration +public class SqlInitialCreate : Migration { public override void Up() { 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/WebApiWithWorkerAndSweeper/Orders.API/Program.cs b/samples/WebApiWithWorkerAndSweeper/Orders.API/Program.cs index a0dd1fc195..549609d28a 100644 --- a/samples/WebApiWithWorkerAndSweeper/Orders.API/Program.cs +++ b/samples/WebApiWithWorkerAndSweeper/Orders.API/Program.cs @@ -51,7 +51,7 @@ { configure.ProducerRegistry = producerRegistry; configure.Outbox = new MsSqlOutbox(outboxConfig); - configure.TransactionProvider = typeof(MsSqlSqlAuthUnitOfWork); + configure.TransactionProvider = typeof(MsSqlUnitOfWork); } ) .AutoFromAssemblies(Assembly.GetAssembly(typeof(NewOrderVersionEvent))); diff --git a/src/Paramore.Brighter.MsSql/MsSqlSqlAuthUnitOfWork.cs b/src/Paramore.Brighter.MsSql/MsSqlUnitOfWork.cs similarity index 94% rename from src/Paramore.Brighter.MsSql/MsSqlSqlAuthUnitOfWork.cs rename to src/Paramore.Brighter.MsSql/MsSqlUnitOfWork.cs index 3244c98e47..6a831f07ab 100644 --- a/src/Paramore.Brighter.MsSql/MsSqlSqlAuthUnitOfWork.cs +++ b/src/Paramore.Brighter.MsSql/MsSqlUnitOfWork.cs @@ -10,7 +10,7 @@ namespace Paramore.Brighter.MsSql /// /// A connection provider for Sqlite /// - public class MsSqlSqlAuthUnitOfWork : RelationalDbTransactionProvider + public class MsSqlUnitOfWork : RelationalDbTransactionProvider { private readonly string _connectionString; @@ -18,7 +18,7 @@ public class MsSqlSqlAuthUnitOfWork : RelationalDbTransactionProvider /// Create a connection provider for MSSQL using a connection string for Db access /// /// The configuration for this database - public MsSqlSqlAuthUnitOfWork(IAmARelationalDatabaseConfiguration configuration) + public MsSqlUnitOfWork(IAmARelationalDatabaseConfiguration configuration) { if (string.IsNullOrWhiteSpace(configuration?.ConnectionString)) throw new ArgumentNullException(nameof(configuration.ConnectionString)); diff --git a/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutbox.cs b/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutbox.cs index 82b334d259..55ea3aa015 100644 --- a/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutbox.cs +++ b/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutbox.cs @@ -201,14 +201,15 @@ protected override DbCommand CreateCommand( return command; } - protected override IDbDataParameter[] CreatePagedOutstandingParameters(double milliSecondsSinceAdded, - int pageSize, int pageNumber) + protected override IDbDataParameter[] CreatePagedOutstandingParameters( + double milliSecondsSinceAdded, + int pageSize, + int pageNumber) { - var offset = (pageNumber - 1) * pageSize; var parameters = new IDbDataParameter[3]; - parameters[0] = CreateSqlParameter("OffsetValue", offset); + parameters[0] = CreateSqlParameter("OutstandingSince", milliSecondsSinceAdded); parameters[1] = CreateSqlParameter("PageSize", pageSize); - parameters[2] = CreateSqlParameter("OutstandingSince", milliSecondsSinceAdded); + parameters[2] = CreateSqlParameter("PageNumber", pageNumber); return parameters; } diff --git a/src/Paramore.Brighter.PostgreSql/PostgreSqlNpgsqlUnitOfWork.cs b/src/Paramore.Brighter.PostgreSql/NpgsqlUnitOfWork.cs similarity index 95% rename from src/Paramore.Brighter.PostgreSql/PostgreSqlNpgsqlUnitOfWork.cs rename to src/Paramore.Brighter.PostgreSql/NpgsqlUnitOfWork.cs index ac0c5b374e..0e39423e4e 100644 --- a/src/Paramore.Brighter.PostgreSql/PostgreSqlNpgsqlUnitOfWork.cs +++ b/src/Paramore.Brighter.PostgreSql/NpgsqlUnitOfWork.cs @@ -10,7 +10,7 @@ namespace Paramore.Brighter.PostgreSql /// /// A connection provider that uses the connection string to create a connection /// - public class PostgreSqlNpgsqlUnitOfWork : RelationalDbTransactionProvider + public class NpgsqlUnitOfWork : RelationalDbTransactionProvider { private readonly string _connectionString; @@ -18,7 +18,7 @@ public class PostgreSqlNpgsqlUnitOfWork : RelationalDbTransactionProvider /// Initialise a new instance of PostgreSQl Connection provider from a connection string /// /// PostgreSQL Configuration - public PostgreSqlNpgsqlUnitOfWork(IAmARelationalDatabaseConfiguration configuration) + public NpgsqlUnitOfWork(IAmARelationalDatabaseConfiguration configuration) { if (string.IsNullOrWhiteSpace(configuration?.ConnectionString)) throw new ArgumentNullException(nameof(configuration.ConnectionString)); From 5b4dbdbbf99d984e3d01ebd224607f71b5506e03 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Mon, 12 Jun 2023 09:47:58 +0100 Subject: [PATCH 66/89] remove post overloads --- src/Paramore.Brighter/CommandProcessor.cs | 55 +------------------ src/Paramore.Brighter/IAmACommandProcessor.cs | 29 ---------- 2 files changed, 3 insertions(+), 81 deletions(-) diff --git a/src/Paramore.Brighter/CommandProcessor.cs b/src/Paramore.Brighter/CommandProcessor.cs index 2ed9dbc4ed..332bc4e52a 100644 --- a/src/Paramore.Brighter/CommandProcessor.cs +++ b/src/Paramore.Brighter/CommandProcessor.cs @@ -440,30 +440,10 @@ public async Task PublishAsync(T @event, bool continueOnCapturedContext = fal /// public void Post(TRequest request) where TRequest : class, IRequest { - Post(request, null); + ClearOutbox(DepositPost(request, (IAmABoxTransactionProvider)null)); } /// - /// 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 - /// Paramore.Brighter.ServiceActivator provides an endpoint for use in a windows service that reads from a queue - /// and then Sends or Publishes the message to a within that service. The decision to or is based on the - /// mapper. Your mapper can map to a with either a , which results in a or a - /// which results in a - /// 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 transaction provider - /// The type of request - /// The type of transaction used by the Outbox - /// - public void Post(TRequest request, IAmABoxTransactionProvider transactionProvider) where TRequest : class, IRequest - { - ClearOutbox(DepositPost(request, transactionProvider)); - } - - /// /// Posts the specified request with async/await support. 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 /// Paramore.Brighter.ServiceActivator provides an endpoint for use in a windows service that reads from a queue @@ -485,40 +465,11 @@ public async Task PostAsync( CancellationToken cancellationToken = default ) where TRequest : class, IRequest - { - await PostAsync(request, null, continueOnCapturedContext, cancellationToken); - } - - /// - /// Posts the specified request with async/await support. 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 - /// Paramore.Brighter.ServiceActivator provides an endpoint for use in a windows service that reads from a queue - /// and then Sends or Publishes the message to a within that service. The decision to or is based on the - /// mapper. Your mapper can map to a with either a , which results in a or a - /// which results in a - /// 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. - /// The transaction provider used to share a transaction with an Outbox - /// 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 - /// The type of a transaction i.e. DbTransaction etc - /// - /// awaitable . - public async Task PostAsync( - TRequest request, - IAmABoxTransactionProvider transactionProvider = null, - bool continueOnCapturedContext = false, - CancellationToken cancellationToken = default - ) - where TRequest : class, IRequest { - var messageId = await DepositPostAsync(request, transactionProvider, 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 diff --git a/src/Paramore.Brighter/IAmACommandProcessor.cs b/src/Paramore.Brighter/IAmACommandProcessor.cs index 58648bbc6a..4a59850ae6 100644 --- a/src/Paramore.Brighter/IAmACommandProcessor.cs +++ b/src/Paramore.Brighter/IAmACommandProcessor.cs @@ -84,18 +84,6 @@ Task PublishAsync( /// The request. void Post(TRequest request) where TRequest : class, IRequest; - /// - /// Posts the specified request. - /// - /// The type of the request - /// The type of any transaction used by the request - /// The request. - /// If you wish to use an outbox with this request, the transaction provider for that outbox - void Post( - TRequest request, - IAmABoxTransactionProvider transactionProvider - ) where TRequest : class, IRequest; - /// /// Posts the specified request with async/await support. /// @@ -110,23 +98,6 @@ Task PostAsync( CancellationToken cancellationToken = default ) where TRequest : class, IRequest; - /// - /// Posts the specified request with async/await support. - /// - /// The type of the request - /// GThe type of transaction used by any outbox - /// The request. - /// If you wish to use an outbox with this request, the transaction provider for that outbox - /// 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( - TRequest request, - IAmABoxTransactionProvider transactionProvider = null, - 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 From c81e944a5061f40e783e6bb3a5de5793ce1e30d4 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Tue, 13 Jun 2023 09:35:23 +0100 Subject: [PATCH 67/89] safety dance --- .../.idea/httpRequests/http-requests-log.http | 130 +++++++----------- .../Handlers/DeletePersonHandlerAsync.cs | 2 +- .../FIndGreetingsForPersonHandlerAsync.cs | 8 +- .../GreetingsWeb/Database/OutboxExtensions.cs | 20 +-- samples/WebAPI_Dapper/GreetingsWeb/Startup.cs | 36 +++-- .../Migrations/20220527_InitialCreate.cs | 36 +++-- .../Migrations/MigrationConfiguration.cs | 11 ++ .../Greetings_MsSqlMigrations.csproj | 13 -- .../Migrations/202306041332_InitialCreate.cs | 30 ---- .../Greetings_PostgreSqlMigrations.csproj | 13 -- .../Migrations/202306031734_InitialCreate.cs | 30 ---- .../GreetingsWeb/Database/OutboxExtensions.cs | 40 ++++-- .../GreetingsWeb/Startup.cs | 3 +- .../ServiceCollectionExtensions.cs | 17 ++- .../PostgreSqlOutbox.cs | 2 +- ...Provider.cs => NpgsqConnectionProvider.cs} | 4 +- .../ExternalBusConfiguration.cs | 8 ++ 17 files changed, 174 insertions(+), 229 deletions(-) create mode 100644 samples/WebAPI_Dapper/Greetings_Migrations/Migrations/MigrationConfiguration.cs delete mode 100644 samples/WebAPI_Dapper/Greetings_MsSqlMigrations/Greetings_MsSqlMigrations.csproj delete mode 100644 samples/WebAPI_Dapper/Greetings_MsSqlMigrations/Migrations/202306041332_InitialCreate.cs delete mode 100644 samples/WebAPI_Dapper/Greetings_PostgreSqlMigrations/Greetings_PostgreSqlMigrations.csproj delete mode 100644 samples/WebAPI_Dapper/Greetings_PostgreSqlMigrations/Migrations/202306031734_InitialCreate.cs rename src/Paramore.Brighter.PostgreSql/{PostgreSqlNpgsqlConnectionProvider.cs => NpgsqConnectionProvider.cs} (91%) diff --git a/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http b/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http index 0a54c4581c..49e50dcd7c 100644 --- a/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http +++ b/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http @@ -1,3 +1,51 @@ +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 + +### + +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 + +<> 2023-06-12T184025.500.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-06-12T165500.500.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-06-12T141625.500.json + +### + POST http://localhost:5000/People/new Content-Type: application/json Content-Length: 23 @@ -556,85 +604,3 @@ Accept-Encoding: br,deflate,gzip,x-gzip ### -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" -} - -### - -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-05-14T203228.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-05-14T203141.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-05-14T203136.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-05-11T110559.200.json - -### - -POST http://localhost:5000/People/new -Content-Type: application/json -Content-Length: 26 -Connection: Keep-Alive -User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) -Accept-Encoding: br,deflate,gzip,x-gzip - -{ - "Name" : "Ned Stark" -} - -<> 2023-05-11T110539.200.json - -### - diff --git a/samples/WebAPI_Dapper/GreetingsPorts/Handlers/DeletePersonHandlerAsync.cs b/samples/WebAPI_Dapper/GreetingsPorts/Handlers/DeletePersonHandlerAsync.cs index 5c484a6bce..ba280dba14 100644 --- a/samples/WebAPI_Dapper/GreetingsPorts/Handlers/DeletePersonHandlerAsync.cs +++ b/samples/WebAPI_Dapper/GreetingsPorts/Handlers/DeletePersonHandlerAsync.cs @@ -16,7 +16,7 @@ public class DeletePersonHandlerAsync : RequestHandlerAsync { private readonly IAmARelationalDbConnectionProvider _relationalDbConnectionProvider; - public DeletePersonHandlerAsync(IAmATransactionConnectionProvider relationalDbConnectionProvider) + public DeletePersonHandlerAsync(IAmARelationalDbConnectionProvider relationalDbConnectionProvider) { _relationalDbConnectionProvider = relationalDbConnectionProvider; } diff --git a/samples/WebAPI_Dapper/GreetingsPorts/Handlers/FIndGreetingsForPersonHandlerAsync.cs b/samples/WebAPI_Dapper/GreetingsPorts/Handlers/FIndGreetingsForPersonHandlerAsync.cs index 542f2a9dbe..b274354018 100644 --- a/samples/WebAPI_Dapper/GreetingsPorts/Handlers/FIndGreetingsForPersonHandlerAsync.cs +++ b/samples/WebAPI_Dapper/GreetingsPorts/Handlers/FIndGreetingsForPersonHandlerAsync.cs @@ -15,11 +15,11 @@ namespace GreetingsPorts.Handlers { public class FIndGreetingsForPersonHandlerAsync : QueryHandlerAsync { - private readonly IAmATransactionConnectionProvider _transactionConnectionProvider; + private readonly IAmARelationalDbConnectionProvider _relationalDbConnectionProvider; - public FIndGreetingsForPersonHandlerAsync(IAmATransactionConnectionProvider transactionConnectionProvider) + public FIndGreetingsForPersonHandlerAsync(IAmARelationalDbConnectionProvider relationalDbConnectionProvider) { - _transactionConnectionProvider = transactionConnectionProvider; + _relationalDbConnectionProvider = relationalDbConnectionProvider; } [QueryLogging(0)] @@ -33,7 +33,7 @@ public FIndGreetingsForPersonHandlerAsync(IAmATransactionConnectionProvider tran var sql = @"select p.Id, p.Name, g.Id, g.Message from Person p inner join Greeting g on g.Recipient_Id = p.Id"; - await using var connection = await _transactionConnectionProvider.GetConnectionAsync(cancellationToken); + await using var connection = await _relationalDbConnectionProvider.GetConnectionAsync(cancellationToken); var people = await connection.QueryAsync(sql, (person, greeting) => { person.Greetings.Add(greeting); diff --git a/samples/WebAPI_Dapper/GreetingsWeb/Database/OutboxExtensions.cs b/samples/WebAPI_Dapper/GreetingsWeb/Database/OutboxExtensions.cs index 74466c75a7..8454364051 100644 --- a/samples/WebAPI_Dapper/GreetingsWeb/Database/OutboxExtensions.cs +++ b/samples/WebAPI_Dapper/GreetingsWeb/Database/OutboxExtensions.cs @@ -19,12 +19,12 @@ namespace GreetingsWeb.Database public class OutboxExtensions { - public static (IAmAnOutbox, Type) MakeOutbox( + public static (IAmAnOutbox, Type, Type) MakeOutbox( IWebHostEnvironment env, DatabaseType databaseType, RelationalDatabaseConfiguration configuration) { - (IAmAnOutbox, Type) outbox; + (IAmAnOutbox, Type, Type) outbox; if (env.IsDevelopment()) { outbox = MakeSqliteOutBox(configuration); @@ -44,24 +44,24 @@ public static (IAmAnOutbox, Type) MakeOutbox( return outbox; } - private static (IAmAnOutbox, Type) MakePostgresSqlOutbox(RelationalDatabaseConfiguration configuration) + private static (IAmAnOutbox, Type, Type) MakePostgresSqlOutbox(RelationalDatabaseConfiguration configuration) { - return (new PostgreSqlOutbox(configuration), typeof(NpgsqlUnitOfWork)); + return (new PostgreSqlOutbox(configuration), typeof(NpgsqConnectionProvider), typeof(NpgsqlUnitOfWork)); } - private static (IAmAnOutbox, Type) MakeMsSqlOutbox(RelationalDatabaseConfiguration configuration) + private static (IAmAnOutbox, Type, Type) MakeMsSqlOutbox(RelationalDatabaseConfiguration configuration) { - return new(new MsSqlOutbox(configuration), typeof(MsSqlUnitOfWork)); + return new(new MsSqlOutbox(configuration), typeof(MsSqlAuthConnectionProvider), typeof(MsSqlUnitOfWork)); } - private static (IAmAnOutbox, Type) MakeMySqlOutbox(RelationalDatabaseConfiguration configuration) + private static (IAmAnOutbox, Type, Type) MakeMySqlOutbox(RelationalDatabaseConfiguration configuration) { - return (new MySqlOutbox(configuration), typeof(MySqlUnitOfWork)); + return (new MySqlOutbox(configuration), typeof (MySqlConnectionProvider), typeof(MySqlUnitOfWork)); } - private static (IAmAnOutbox, Type) MakeSqliteOutBox(RelationalDatabaseConfiguration configuration) + private static (IAmAnOutbox, Type, Type) MakeSqliteOutBox(RelationalDatabaseConfiguration configuration) { - return (new SqliteOutbox(configuration), typeof(SqliteUnitOfWork)); + return (new SqliteOutbox(configuration), typeof(SqliteConnectionProvider), typeof(SqliteUnitOfWork)); } } } diff --git a/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs b/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs index de4abc8daa..a6cb4c7c2c 100644 --- a/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs +++ b/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs @@ -1,4 +1,5 @@ using System; +using Dapper; using DapperExtensions; using DapperExtensions.Sql; using FluentMigrator.Runner; @@ -123,7 +124,8 @@ private void ConfigureMsSql(IServiceCollection services) .ConfigureRunner(c => c.AddSqlServer() .WithGlobalConnectionString(DbConnectionString()) .ScanIn(typeof(SqlInitialCreate).Assembly).For.Migrations() - ); + ) + .AddSingleton(new MigrationConfiguration(){DbType = DatabaseType.MsSql.ToString()}); } private void ConfigureMySql(IServiceCollection services) @@ -133,7 +135,8 @@ private void ConfigureMySql(IServiceCollection services) .ConfigureRunner(c => c.AddMySql5() .WithGlobalConnectionString(DbConnectionString()) .ScanIn(typeof(SqlInitialCreate).Assembly).For.Migrations() - ); + ) + .AddSingleton(new MigrationConfiguration(){DbType = DatabaseType.MySql.ToString()}); } private void ConfigurePostgreSql(IServiceCollection services) @@ -144,7 +147,8 @@ private void ConfigurePostgreSql(IServiceCollection services) .ConfigureRunner(c => c.AddPostgres() .WithGlobalConnectionString(DbConnectionString()) .ScanIn(typeof(SqlInitialCreate).Assembly).For.Migrations() - ); + ) + .AddSingleton(new MigrationConfiguration(){DbType = DatabaseType.Postgres.ToString()}); } private void ConfigureSqlite(IServiceCollection services) @@ -154,7 +158,8 @@ private void ConfigureSqlite(IServiceCollection services) .ConfigureRunner(c => c.AddSQLite() .WithGlobalConnectionString(DbConnectionString()) .ScanIn(typeof(SqlInitialCreate).Assembly).For.Migrations() - ); + ) + .AddSingleton(new MigrationConfiguration(){DbType = DatabaseType.Sqlite.ToString()}); } private void ConfigureDapper() @@ -188,26 +193,30 @@ private static void ConfigureDapperByHost(DatabaseType databaseType) private static void ConfigureDapperMsSql() { - DapperExtensions.DapperExtensions.SqlDialect = new SqlServerDialect(); - DapperAsyncExtensions.SqlDialect = new SqlServerDialect(); + var sqlDialect = new SqlServerDialect(); + DapperExtensions.DapperExtensions.SqlDialect = sqlDialect; + DapperAsyncExtensions.SqlDialect = sqlDialect; } private static void ConfigureDapperSqlite() { - DapperExtensions.DapperExtensions.SqlDialect = new SqliteDialect(); - DapperAsyncExtensions.SqlDialect = new SqliteDialect(); + var sqlDialect = new SqliteDialect(); + DapperExtensions.DapperExtensions.SqlDialect = sqlDialect; + DapperAsyncExtensions.SqlDialect = sqlDialect; } private static void ConfigureDapperMySql() { - DapperExtensions.DapperExtensions.SqlDialect = new MySqlDialect(); - DapperAsyncExtensions.SqlDialect = new MySqlDialect(); + var sqlDialect = new MySqlDialect(); + DapperExtensions.DapperExtensions.SqlDialect = sqlDialect; + DapperAsyncExtensions.SqlDialect = sqlDialect; } private static void ConfigureDapperPostgreSql() { - DapperExtensions.DapperExtensions.SqlDialect = new PostgreSqlDialect(); - DapperAsyncExtensions.SqlDialect = new PostgreSqlDialect(); + var sqlDialect = new PostgreSqlDialect(); + DapperExtensions.DapperExtensions.SqlDialect = sqlDialect; + DapperAsyncExtensions.SqlDialect = sqlDialect; } private void ConfigureBrighter(IServiceCollection services) @@ -237,7 +246,7 @@ private void ConfigureBrighter(IServiceCollection services) } ).Create(); - (IAmAnOutbox outbox, Type transactionProvider) makeOutbox = + (IAmAnOutbox outbox, Type connectionProvider, Type transactionProvider) makeOutbox = OutboxExtensions.MakeOutbox(_env, GetDatabaseType(), outboxConfiguration); services.AddBrighter(options => @@ -253,6 +262,7 @@ private void ConfigureBrighter(IServiceCollection services) configure.ProducerRegistry = producerRegistry; configure.Outbox = makeOutbox.outbox; configure.TransactionProvider = makeOutbox.transactionProvider; + configure.ConnectionProvider = makeOutbox.connectionProvider; }) .UseOutboxSweeper(options => { options.TimerInterval = 5; diff --git a/samples/WebAPI_Dapper/Greetings_Migrations/Migrations/20220527_InitialCreate.cs b/samples/WebAPI_Dapper/Greetings_Migrations/Migrations/20220527_InitialCreate.cs index 7e6ee55a3e..4c57c92f81 100644 --- a/samples/WebAPI_Dapper/Greetings_Migrations/Migrations/20220527_InitialCreate.cs +++ b/samples/WebAPI_Dapper/Greetings_Migrations/Migrations/20220527_InitialCreate.cs @@ -5,21 +5,35 @@ namespace Greetings_MySqlMigrations.Migrations; [Migration(1)] public class SqlInitialCreate : Migration { + private readonly IAmAMigrationConfiguration _configuration; + + public SqlInitialCreate(IAmAMigrationConfiguration configuration) + { + _configuration = configuration; + } + public override void Up() { - Create.Table("Person") - .WithColumn("Id").AsInt32().NotNullable().PrimaryKey().Identity() - .WithColumn("Name").AsString().Unique() - .WithColumn("TimeStamp").AsDateTime().Nullable().WithDefault(SystemMethods.CurrentDateTime); - - Create.Table("Greeting") - .WithColumn("Id").AsInt32().NotNullable().PrimaryKey().Identity() - .WithColumn("Message").AsString() - .WithColumn("Recipient_Id").AsInt32(); + var personTableName = _configuration.DbType == "Postgres" ? "person" : "Person"; + var idColumn = _configuration.DbType == "Postgres" ? "id": "Id"; + var nameColumn = _configuration.DbType == "Postgres" ? "name" : "Name"; + var timestampColumn = _configuration.DbType == "Postgres" ? "timeStamp" : "TimeStamp"; + var person = Create.Table(personTableName) + .WithColumn(idColumn).AsInt32().NotNullable().PrimaryKey().Identity() + .WithColumn(nameColumn).AsString().Unique() + .WithColumn(timestampColumn).AsDateTime().Nullable().WithDefault(SystemMethods.CurrentDateTime); + + var greetingTableName = _configuration.DbType == "Postgres" ? "greeting" : "Greeting"; + var recipientIdColumn = _configuration.DbType == "Postgres" ? "recipient_id": "Recipient_Id"; + var messageColumn = _configuration.DbType == "Postgres" ? "message": "Message"; + var greeting = Create.Table(greetingTableName) + .WithColumn(idColumn).AsInt32().NotNullable().PrimaryKey().Identity() + .WithColumn(messageColumn).AsString() + .WithColumn(recipientIdColumn).AsInt32(); Create.ForeignKey() - .FromTable("Greeting").ForeignColumn("Recipient_Id") - .ToTable("Person").PrimaryColumn("Id"); + .FromTable(greetingTableName).ForeignColumn(recipientIdColumn) + .ToTable(personTableName).PrimaryColumn(idColumn); } public override void Down() 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_MsSqlMigrations/Greetings_MsSqlMigrations.csproj b/samples/WebAPI_Dapper/Greetings_MsSqlMigrations/Greetings_MsSqlMigrations.csproj deleted file mode 100644 index 9107f11c61..0000000000 --- a/samples/WebAPI_Dapper/Greetings_MsSqlMigrations/Greetings_MsSqlMigrations.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - - net6.0 - enable - enable - - - - - - - diff --git a/samples/WebAPI_Dapper/Greetings_MsSqlMigrations/Migrations/202306041332_InitialCreate.cs b/samples/WebAPI_Dapper/Greetings_MsSqlMigrations/Migrations/202306041332_InitialCreate.cs deleted file mode 100644 index e6db18eccc..0000000000 --- a/samples/WebAPI_Dapper/Greetings_MsSqlMigrations/Migrations/202306041332_InitialCreate.cs +++ /dev/null @@ -1,30 +0,0 @@ -using FluentMigrator; - -namespace Greetings_PostgreSqlMigrations.Migrations; - -[Migration(1)] -public class MsSqlInitialCreate : 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/Greetings_PostgreSqlMigrations/Greetings_PostgreSqlMigrations.csproj b/samples/WebAPI_Dapper/Greetings_PostgreSqlMigrations/Greetings_PostgreSqlMigrations.csproj deleted file mode 100644 index 78d464b3d5..0000000000 --- a/samples/WebAPI_Dapper/Greetings_PostgreSqlMigrations/Greetings_PostgreSqlMigrations.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - - net6.0 - enable - enable - - - - - - - diff --git a/samples/WebAPI_Dapper/Greetings_PostgreSqlMigrations/Migrations/202306031734_InitialCreate.cs b/samples/WebAPI_Dapper/Greetings_PostgreSqlMigrations/Migrations/202306031734_InitialCreate.cs deleted file mode 100644 index 7ada805d2b..0000000000 --- a/samples/WebAPI_Dapper/Greetings_PostgreSqlMigrations/Migrations/202306031734_InitialCreate.cs +++ /dev/null @@ -1,30 +0,0 @@ -using FluentMigrator; - -namespace Greetings_PostgreSqlMigrations.Migrations; - -[Migration(1)] -public class PostgreSqlInitialCreate : 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_Kafka/GreetingsWeb/Database/OutboxExtensions.cs b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/OutboxExtensions.cs index ea94c7383f..8454364051 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/OutboxExtensions.cs +++ b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/OutboxExtensions.cs @@ -5,9 +5,13 @@ using Paramore.Brighter; using Paramore.Brighter.Extensions.DependencyInjection; using Paramore.Brighter.Extensions.Hosting; +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 GreetingsWeb.Database @@ -15,39 +19,49 @@ namespace GreetingsWeb.Database public class OutboxExtensions { - public static (IAmAnOutbox, Type) MakeOutbox( + public static (IAmAnOutbox, Type, Type) MakeOutbox( IWebHostEnvironment env, DatabaseType databaseType, RelationalDatabaseConfiguration configuration) { - (IAmAnOutbox, Type) outbox; + (IAmAnOutbox, Type, Type) outbox; if (env.IsDevelopment()) { outbox = MakeSqliteOutBox(configuration); } else { - switch (databaseType) + outbox = databaseType switch { - case DatabaseType.MySql: - outbox = MakeMySqlOutbox(configuration); - break; - default: - throw new InvalidOperationException("Unknown Db type for Outbox configuration"); - } + DatabaseType.MySql => MakeMySqlOutbox(configuration), + DatabaseType.MsSql => MakeMsSqlOutbox(configuration), + DatabaseType.Postgres => MakePostgresSqlOutbox(configuration), + DatabaseType.Sqlite => MakeSqliteOutBox(configuration), + _ => throw new InvalidOperationException("Unknown Db type for Outbox configuration") + }; } return outbox; } - private static (IAmAnOutbox, Type) MakeMySqlOutbox(RelationalDatabaseConfiguration configuration) + private static (IAmAnOutbox, Type, Type) MakePostgresSqlOutbox(RelationalDatabaseConfiguration configuration) { - return (new MySqlOutbox(configuration), typeof(MySqlUnitOfWork)); + return (new PostgreSqlOutbox(configuration), typeof(NpgsqConnectionProvider), typeof(NpgsqlUnitOfWork)); } - private static (IAmAnOutbox, Type) MakeSqliteOutBox(RelationalDatabaseConfiguration configuration) + private static (IAmAnOutbox, Type, Type) MakeMsSqlOutbox(RelationalDatabaseConfiguration configuration) { - return (new SqliteOutbox(configuration), typeof(SqliteUnitOfWork)); + return new(new MsSqlOutbox(configuration), typeof(MsSqlAuthConnectionProvider), 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_Kafka/GreetingsWeb/Startup.cs b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs index 4aa1f7ed06..1d0c349d5d 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs +++ b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs @@ -248,7 +248,7 @@ private void ConfigureBrighter(IServiceCollection services) }) .Create(); - (IAmAnOutbox outbox, Type transactionProvider) makeOutbox = + (IAmAnOutbox outbox, Type connectionProvider, Type transactionProvider) makeOutbox = OutboxExtensions.MakeOutbox(_env, GetDatabaseType(), outboxConfiguration); services.AddBrighter(options => @@ -263,6 +263,7 @@ private void ConfigureBrighter(IServiceCollection services) { configure.ProducerRegistry = producerRegistry; configure.Outbox = makeOutbox.outbox; + configure.ConnectionProvider = makeOutbox.connectionProvider; configure.TransactionProvider = makeOutbox.transactionProvider; }) .UseOutboxSweeper(options => diff --git a/src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionExtensions.cs b/src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionExtensions.cs index 6fe341e88b..3f1362e821 100644 --- a/src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionExtensions.cs @@ -167,7 +167,7 @@ public static IBrighterBuilder UseExternalBus( brighterBuilder.Services.Add(new ServiceDescriptor(boxProviderType, transactionProvider, serviceLifetime)); - RegisterRelationalProviderServicesMaybe(brighterBuilder, transactionProvider, serviceLifetime); + RegisterRelationalProviderServicesMaybe(brighterBuilder, busConfiguration.ConnectionProvider, transactionProvider, serviceLifetime); return ExternalBusBuilder(brighterBuilder, busConfiguration, transactionType); } @@ -363,14 +363,21 @@ public static MessageMapperRegistry MessageMapperRegistry(IServiceProvider provi private static void RegisterRelationalProviderServicesMaybe( IBrighterBuilder brighterBuilder, - Type transactionProvider, ServiceLifetime serviceLifetime) + Type connectionProvider, + Type transactionProvider, + ServiceLifetime serviceLifetime + ) { //not all box transaction providers are also relational connection providers - if (typeof(IAmARelationalDbConnectionProvider).IsAssignableFrom(transactionProvider)) + if (connectionProvider != null) { brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmARelationalDbConnectionProvider), - transactionProvider, serviceLifetime)); - + connectionProvider, serviceLifetime)); + } + + //not all box transaction providers are also relational connection providers + if (typeof(IAmARelationalDbConnectionProvider).IsAssignableFrom(transactionProvider)) + { //register the combined interface just in case brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmATransactionConnectionProvider), transactionProvider, serviceLifetime)); diff --git a/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutbox.cs b/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutbox.cs index 55ea3aa015..4cfe09df95 100644 --- a/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutbox.cs +++ b/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutbox.cs @@ -56,7 +56,7 @@ public PostgreSqlOutbox( } public PostgreSqlOutbox(IAmARelationalDatabaseConfiguration configuration) - : this(configuration, new PostgreSqlNpgsqlConnectionProvider(configuration)) + : this(configuration, new NpgsqConnectionProvider(configuration)) { } protected override void WriteToStore( diff --git a/src/Paramore.Brighter.PostgreSql/PostgreSqlNpgsqlConnectionProvider.cs b/src/Paramore.Brighter.PostgreSql/NpgsqConnectionProvider.cs similarity index 91% rename from src/Paramore.Brighter.PostgreSql/PostgreSqlNpgsqlConnectionProvider.cs rename to src/Paramore.Brighter.PostgreSql/NpgsqConnectionProvider.cs index 3c285ab4b3..a468c92769 100644 --- a/src/Paramore.Brighter.PostgreSql/PostgreSqlNpgsqlConnectionProvider.cs +++ b/src/Paramore.Brighter.PostgreSql/NpgsqConnectionProvider.cs @@ -9,7 +9,7 @@ namespace Paramore.Brighter.PostgreSql /// /// A connection provider that uses the connection string to create a connection /// - public class PostgreSqlNpgsqlConnectionProvider : RelationalDbConnectionProvider + public class NpgsqConnectionProvider : RelationalDbConnectionProvider { private readonly string _connectionString; @@ -17,7 +17,7 @@ public class PostgreSqlNpgsqlConnectionProvider : RelationalDbConnectionProvider /// Initialise a new instance of PostgreSQl Connection provider from a connection string /// /// PostgreSQL Configuration - public PostgreSqlNpgsqlConnectionProvider(IAmARelationalDatabaseConfiguration configuration) + public NpgsqConnectionProvider(IAmARelationalDatabaseConfiguration configuration) { if (string.IsNullOrWhiteSpace(configuration?.ConnectionString)) throw new ArgumentNullException(nameof(configuration.ConnectionString)); diff --git a/src/Paramore.Brighter/ExternalBusConfiguration.cs b/src/Paramore.Brighter/ExternalBusConfiguration.cs index b014ee4fda..09036b4f30 100644 --- a/src/Paramore.Brighter/ExternalBusConfiguration.cs +++ b/src/Paramore.Brighter/ExternalBusConfiguration.cs @@ -91,6 +91,12 @@ public interface IAmExternalBusConfiguration /// 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 /// @@ -138,6 +144,7 @@ public class ExternalBusConfiguration : IAmExternalBusConfiguration /// /// The transaction provider for the outbox + /// NOTE: Must implement IAmABoxTransactionProvider< > /// public Type TransactionProvider { get; set; } @@ -146,6 +153,7 @@ public class ExternalBusConfiguration : IAmExternalBusConfiguration /// public bool UseRpc { get; set; } + /// /// Initializes a new instance of the class. /// From 2af0442a04d0ee8d594d9a9455f9d873a2ab58b2 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Tue, 13 Jun 2023 17:18:36 +0100 Subject: [PATCH 68/89] Fix issues with PostgresSql generated via Dapper.Extensions and schema created by FluentMigrator --- .../.idea/httpRequests/http-requests-log.http | 76 ++++++++++--------- .../Handlers/FindPersonByNameHandlerAsync.cs | 2 +- .../Responses/FindPersonResult.cs | 4 +- .../Controllers/PeopleController.cs | 2 +- samples/WebAPI_Dapper/GreetingsWeb/Startup.cs | 2 +- .../Migrations/20220527_InitialCreate.cs | 28 +++---- 6 files changed, 56 insertions(+), 58 deletions(-) diff --git a/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http b/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http index 49e50dcd7c..5edc12d6e5 100644 --- a/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http +++ b/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http @@ -1,3 +1,42 @@ +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" +} + +<> 2023-06-13T171434.500.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" +} + +<> 2023-06-13T171423.500.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-06-13T171320.404.json + +### + GET http://localhost:5000/People/Tyrion Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) @@ -567,40 +606,3 @@ 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 - -<> 2023-05-14T210524.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" -} - -<> 2023-05-14T210520.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" -} - -### - diff --git a/samples/WebAPI_Dapper/GreetingsPorts/Handlers/FindPersonByNameHandlerAsync.cs b/samples/WebAPI_Dapper/GreetingsPorts/Handlers/FindPersonByNameHandlerAsync.cs index c4dfaad565..09d3503f55 100644 --- a/samples/WebAPI_Dapper/GreetingsPorts/Handlers/FindPersonByNameHandlerAsync.cs +++ b/samples/WebAPI_Dapper/GreetingsPorts/Handlers/FindPersonByNameHandlerAsync.cs @@ -30,7 +30,7 @@ public FindPersonByNameHandlerAsync(IAmARelationalDbConnectionProvider relationa var searchByName = Predicates.Field(p => p.Name, Operator.Eq, query.Name); await using var connection = await _relationalDbConnectionProvider .GetConnectionAsync(cancellationToken); var people = await connection.GetListAsync(searchByName); - var person = people.Single(); + 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/Startup.cs b/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs index a6cb4c7c2c..f7a8732a1f 100644 --- a/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs +++ b/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs @@ -141,10 +141,10 @@ private void ConfigureMySql(IServiceCollection services) private void ConfigurePostgreSql(IServiceCollection services) { - //TODO: add Postgres migrations services .AddFluentMigratorCore() .ConfigureRunner(c => c.AddPostgres() + .ConfigureGlobalProcessorOptions(opt => opt.ProviderSwitches = "Force Quote=false") .WithGlobalConnectionString(DbConnectionString()) .ScanIn(typeof(SqlInitialCreate).Assembly).For.Migrations() ) diff --git a/samples/WebAPI_Dapper/Greetings_Migrations/Migrations/20220527_InitialCreate.cs b/samples/WebAPI_Dapper/Greetings_Migrations/Migrations/20220527_InitialCreate.cs index 4c57c92f81..b8e6656cd4 100644 --- a/samples/WebAPI_Dapper/Greetings_Migrations/Migrations/20220527_InitialCreate.cs +++ b/samples/WebAPI_Dapper/Greetings_Migrations/Migrations/20220527_InitialCreate.cs @@ -14,26 +14,22 @@ public SqlInitialCreate(IAmAMigrationConfiguration configuration) public override void Up() { - var personTableName = _configuration.DbType == "Postgres" ? "person" : "Person"; - var idColumn = _configuration.DbType == "Postgres" ? "id": "Id"; - var nameColumn = _configuration.DbType == "Postgres" ? "name" : "Name"; + /* var timestampColumn = _configuration.DbType == "Postgres" ? "timeStamp" : "TimeStamp"; - var person = Create.Table(personTableName) - .WithColumn(idColumn).AsInt32().NotNullable().PrimaryKey().Identity() - .WithColumn(nameColumn).AsString().Unique() - .WithColumn(timestampColumn).AsDateTime().Nullable().WithDefault(SystemMethods.CurrentDateTime); + */ + var person = Create.Table("Person") + .WithColumn("Id").AsInt32().NotNullable().PrimaryKey().Identity() + .WithColumn("Name").AsString().Unique() + .WithColumn("TimeStamp").AsDateTime().Nullable().WithDefault(SystemMethods.CurrentDateTime); - var greetingTableName = _configuration.DbType == "Postgres" ? "greeting" : "Greeting"; - var recipientIdColumn = _configuration.DbType == "Postgres" ? "recipient_id": "Recipient_Id"; - var messageColumn = _configuration.DbType == "Postgres" ? "message": "Message"; - var greeting = Create.Table(greetingTableName) - .WithColumn(idColumn).AsInt32().NotNullable().PrimaryKey().Identity() - .WithColumn(messageColumn).AsString() - .WithColumn(recipientIdColumn).AsInt32(); + var greeting = Create.Table("Greeting") + .WithColumn("Id").AsInt32().NotNullable().PrimaryKey().Identity() + .WithColumn("Message").AsString() + .WithColumn("Recipient_Id").AsInt32(); Create.ForeignKey() - .FromTable(greetingTableName).ForeignColumn(recipientIdColumn) - .ToTable(personTableName).PrimaryColumn(idColumn); + .FromTable("Greeting").ForeignColumn("Recipient_Id") + .ToTable("Person").PrimaryColumn("Id"); } public override void Down() From 2a1fbf3f4a37340ce787238d561e13c9641c1302 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Fri, 16 Jun 2023 19:53:37 +0100 Subject: [PATCH 69/89] Safety Dance prior to debug changes --- .../.idea/httpRequests/http-requests-log.http | 300 +++++++++--------- .../Handlers/AddPersonHandlerAsync.cs | 6 +- .../GreetingsWeb/Database/OutboxExtensions.cs | 24 +- samples/WebAPI_Dapper/GreetingsWeb/Startup.cs | 8 +- .../Migrations/20220527_InitialCreate.cs | 5 +- .../GreetingsWeb/Database/OutboxExtensions.cs | 13 +- .../MsSqlOutbox.cs | 6 +- .../MySqlOutbox.cs | 11 +- .../PostgreSqlOutbox.cs | 21 +- .../NpgsqConnectionProvider.cs | 40 ++- .../NpgsqlUnitOfWork.cs | 37 ++- 11 files changed, 303 insertions(+), 168 deletions(-) diff --git a/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http b/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http index 5edc12d6e5..2d6a39b57d 100644 --- a/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http +++ b/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http @@ -9,7 +9,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Name" : "Tyrion" } -<> 2023-06-13T171434.500.json +<> 2023-06-15T095915.500.json ### @@ -24,64 +24,63 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Name" : "Tyrion" } -<> 2023-06-13T171423.500.json +<> 2023-06-15T091508.500.json ### -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 -<> 2023-06-13T171320.404.json +{ + "Greeting" : "I drink, and I know things" +} ### -GET http://localhost:5000/People/Tyrion +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 -### - -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 +{ + "Name" : "Tyrion" +} ### -GET http://localhost:5000/People/Tyrion +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 -### - -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 +{ + "Name" : "Tyrion" +} -<> 2023-06-12T184025.500.json +<> 2023-06-14T183740.500.json ### -GET http://localhost:5000/People/Tyrion +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 -<> 2023-06-12T165500.500.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 +{ + "Name" : "Tyrion" +} -<> 2023-06-12T141625.500.json +<> 2023-06-14T183618.500.json ### @@ -96,16 +95,22 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Name" : "Tyrion" } -<> 2023-06-09T164917.500.json +<> 2023-06-14T183505.500.json ### -GET http://localhost:5000/People/Tyrion +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 -<> 2023-06-09T164624.500.json +{ + "Name" : "Tyrion" +} + +<> 2023-06-14T182436.500.json ### @@ -120,7 +125,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Name" : "Tyrion" } -<> 2023-06-09T164620.500.json +<> 2023-06-14T100715.500.json ### @@ -129,7 +134,7 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-06-09T154849.500.json +<> 2023-06-14T090138.500.json ### @@ -144,7 +149,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Name" : "Tyrion" } -<> 2023-06-09T154836.500.json +<> 2023-06-13T205200.500.json ### @@ -159,7 +164,16 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Name" : "Tyrion" } -<> 2023-06-09T154833.500.json +<> 2023-06-13T205002.500.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-06-13T204955.404.json ### @@ -174,22 +188,22 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Name" : "Tyrion" } -<> 2023-06-09T154824.500.json +<> 2023-06-13T171434.500.json ### -POST http://localhost:5000/Greetings/Tyrion/new +POST http://localhost:5000/People/new Content-Type: application/json -Content-Length: 47 +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" } -<> 2023-06-08T222134.200.json +<> 2023-06-13T171423.500.json ### @@ -198,108 +212,94 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-06-08T222121.200.json +<> 2023-06-13T171320.404.json ### -POST http://localhost:5000/Greetings/Tyrion/new -Content-Type: application/json -Content-Length: 47 +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-06-02T190642.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 ### -POST http://localhost:5000/Greetings/Tyrion/new -Content-Type: application/json -Content-Length: 47 +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-06-02T190609.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-06-12T184025.500.json ### -POST http://localhost:5000/Greetings/Tyrion/new -Content-Type: application/json -Content-Length: 47 +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-06-12T165500.500.json ### -POST http://localhost:5000/Greetings/Tyrion/new -Content-Type: application/json -Content-Length: 47 +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-06-12T141625.500.json ### -POST http://localhost:5000/Greetings/Tyrion/new +POST http://localhost:5000/People/new Content-Type: application/json -Content-Length: 47 +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" } -<> 2023-06-02T184827.200.json +<> 2023-06-09T164917.500.json ### -POST http://localhost:5000/Greetings/Tyrion/new -Content-Type: application/json -Content-Length: 47 +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-06-02T182443.200.json +<> 2023-06-09T164624.500.json ### -POST http://localhost:5000/Greetings/Tyrion/new +POST http://localhost:5000/People/new Content-Type: application/json -Content-Length: 47 +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" } -<> 2023-06-02T164746.500.json +<> 2023-06-09T164620.500.json ### @@ -308,55 +308,52 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-06-02T164740.200.json +<> 2023-06-09T154849.500.json ### -POST http://localhost:5000/Greetings/Tyrion/new +POST http://localhost:5000/People/new Content-Type: application/json -Content-Length: 47 +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" } -<> 2023-06-02T164556.200.json +<> 2023-06-09T154836.500.json ### -POST http://localhost:5000/Greetings/Tyrion/new +POST http://localhost:5000/People/new Content-Type: application/json -Content-Length: 47 +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" } -<> 2023-06-02T164104.200.json +<> 2023-06-09T154833.500.json ### -GET http://localhost:5000/People/Tyrion +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 -<> 2023-06-02T164101.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 +{ + "Name" : "Tyrion" +} -<> 2023-06-02T164056.200.json +<> 2023-06-09T154824.500.json ### @@ -371,7 +368,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-05-28T155036.200.json +<> 2023-06-08T222134.200.json ### @@ -380,6 +377,8 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip +<> 2023-06-08T222121.200.json + ### POST http://localhost:5000/Greetings/Tyrion/new @@ -393,6 +392,8 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } +<> 2023-06-02T190642.200.json + ### POST http://localhost:5000/Greetings/Tyrion/new @@ -406,32 +407,33 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -### - -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-05-28T153646.200.json +<> 2023-06-02T190609.200.json ### -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 -<> 2023-05-28T153237.200.json +{ + "Greeting" : "I drink, and I know things" +} ### -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 -<> 2023-05-28T153234.200.json +{ + "Greeting" : "I drink, and I know things" +} ### @@ -446,7 +448,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-05-16T084354.200.json +<> 2023-06-02T184827.200.json ### @@ -461,7 +463,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-05-16T083851.200.json +<> 2023-06-02T182443.200.json ### @@ -476,7 +478,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-05-16T081219.200.json +<> 2023-06-02T164746.500.json ### @@ -485,7 +487,7 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-05-16T081215.200.json +<> 2023-06-02T164740.200.json ### @@ -500,7 +502,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-05-15T110426.200.json +<> 2023-06-02T164556.200.json ### @@ -515,22 +517,16 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-05-15T110422.200.json +<> 2023-06-02T164104.200.json ### -POST http://localhost:5000/Greetings/Tyrion/new -Content-Type: application/json -Content-Length: 47 +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-05-15T102620.200.json +<> 2023-06-02T164101.200.json ### @@ -539,7 +535,7 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-05-15T102555.200.json +<> 2023-06-02T164056.200.json ### @@ -554,7 +550,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-05-15T101101.200.json +<> 2023-05-28T155036.200.json ### @@ -563,22 +559,31 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-05-15T101027.200.json - ### -POST http://localhost:5000/People/new +POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json -Content-Length: 23 +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" } -<> 2023-05-15T101025.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" +} ### @@ -587,22 +592,25 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-05-15T101020.500.json +<> 2023-05-28T153646.200.json ### -POST http://localhost:5000/Greetings/Tyrion/new -Content-Type: application/json -Content-Length: 47 +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-05-28T153237.200.json -<> 2023-05-14T210531.500.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-05-28T153234.200.json ### diff --git a/samples/WebAPI_Dapper/GreetingsPorts/Handlers/AddPersonHandlerAsync.cs b/samples/WebAPI_Dapper/GreetingsPorts/Handlers/AddPersonHandlerAsync.cs index 294f2302f5..ef1cf1b157 100644 --- a/samples/WebAPI_Dapper/GreetingsPorts/Handlers/AddPersonHandlerAsync.cs +++ b/samples/WebAPI_Dapper/GreetingsPorts/Handlers/AddPersonHandlerAsync.cs @@ -22,8 +22,10 @@ public AddPersonHandlerAsync(IAmARelationalDbConnectionProvider relationalDbConn [UsePolicyAsync(step:1, policy: Policies.Retry.EXPONENTIAL_RETRYPOLICYASYNC)] public override async Task HandleAsync(AddPerson addPerson, CancellationToken cancellationToken = default) { - await using var connection = await _relationalDbConnectionProvider.GetConnectionAsync(cancellationToken); - await connection.InsertAsync(new Person(addPerson.Name)); + //await using var connection = await _relationalDbConnectionProvider.GetConnectionAsync(cancellationToken); + using var connection = _relationalDbConnectionProvider.GetConnection(); + //await connection.InsertAsync(new Person(addPerson.Name)); + connection.Insert(new Person(addPerson.Name)); return await base.HandleAsync(addPerson, cancellationToken); } } diff --git a/samples/WebAPI_Dapper/GreetingsWeb/Database/OutboxExtensions.cs b/samples/WebAPI_Dapper/GreetingsWeb/Database/OutboxExtensions.cs index 8454364051..be007eeaca 100644 --- a/samples/WebAPI_Dapper/GreetingsWeb/Database/OutboxExtensions.cs +++ b/samples/WebAPI_Dapper/GreetingsWeb/Database/OutboxExtensions.cs @@ -1,7 +1,10 @@ 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; @@ -22,7 +25,8 @@ public class OutboxExtensions public static (IAmAnOutbox, Type, Type) MakeOutbox( IWebHostEnvironment env, DatabaseType databaseType, - RelationalDatabaseConfiguration configuration) + RelationalDatabaseConfiguration configuration, + IServiceCollection services) { (IAmAnOutbox, Type, Type) outbox; if (env.IsDevelopment()) @@ -35,7 +39,7 @@ public static (IAmAnOutbox, Type, Type) MakeOutbox( { DatabaseType.MySql => MakeMySqlOutbox(configuration), DatabaseType.MsSql => MakeMsSqlOutbox(configuration), - DatabaseType.Postgres => MakePostgresSqlOutbox(configuration), + DatabaseType.Postgres => MakePostgresSqlOutbox(configuration, services), DatabaseType.Sqlite => MakeSqliteOutBox(configuration), _ => throw new InvalidOperationException("Unknown Db type for Outbox configuration") }; @@ -44,8 +48,22 @@ public static (IAmAnOutbox, Type, Type) MakeOutbox( return outbox; } - private static (IAmAnOutbox, Type, Type) MakePostgresSqlOutbox(RelationalDatabaseConfiguration configuration) + private static (IAmAnOutbox, Type, Type) MakePostgresSqlOutbox( + RelationalDatabaseConfiguration configuration, + IServiceCollection services) { + //if we want to use our IAmARelationalDatabaseConnectionProvider or IAmAABoxTransactionProvider + //from the Outbox in our handlers, then we need to construct an NpgsqlDataSource and register the composite types + //then pass that to the Outbox constructor so that connections created by the Outbox will be aware of + //those composite types + //var dataSourceBuilder = new NpgsqlDataSourceBuilder(configuration.ConnectionString); + //dataSourceBuilder.DefaultNameTranslator = new NpgsqlNullNameTranslator(); + //dataSourceBuilder.MapComposite(); + //dataSourceBuilder.MapComposite(); + //var dataSource = dataSourceBuilder.Build(); + + //services.AddSingleton(dataSource); + return (new PostgreSqlOutbox(configuration), typeof(NpgsqConnectionProvider), typeof(NpgsqlUnitOfWork)); } diff --git a/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs b/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs index f7a8732a1f..61c1d17e79 100644 --- a/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs +++ b/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs @@ -4,6 +4,7 @@ using DapperExtensions.Sql; using FluentMigrator.Runner; using Greetings_MySqlMigrations.Migrations; +using GreetingsEntities; using GreetingsPorts.EntityMappers; using GreetingsPorts.Handlers; using GreetingsPorts.Policies; @@ -15,6 +16,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.OpenApi.Models; +using Npgsql; using OpenTelemetry.Metrics; using OpenTelemetry.Trace; using Paramore.Brighter; @@ -81,8 +83,8 @@ public void ConfigureServices(IServiceCollection services) .AddAspNetCoreInstrumentation() .AddConsoleExporter()); - ConfigureMigration(services); ConfigureDapper(); + ConfigureMigration(services); ConfigureBrighter(services); ConfigureDarker(services); } @@ -217,7 +219,7 @@ private static void ConfigureDapperPostgreSql() var sqlDialect = new PostgreSqlDialect(); DapperExtensions.DapperExtensions.SqlDialect = sqlDialect; DapperAsyncExtensions.SqlDialect = sqlDialect; - } + } private void ConfigureBrighter(IServiceCollection services) { @@ -247,7 +249,7 @@ private void ConfigureBrighter(IServiceCollection services) ).Create(); (IAmAnOutbox outbox, Type connectionProvider, Type transactionProvider) makeOutbox = - OutboxExtensions.MakeOutbox(_env, GetDatabaseType(), outboxConfiguration); + OutboxExtensions.MakeOutbox(_env, GetDatabaseType(), outboxConfiguration, services); services.AddBrighter(options => { diff --git a/samples/WebAPI_Dapper/Greetings_Migrations/Migrations/20220527_InitialCreate.cs b/samples/WebAPI_Dapper/Greetings_Migrations/Migrations/20220527_InitialCreate.cs index b8e6656cd4..2957c97da3 100644 --- a/samples/WebAPI_Dapper/Greetings_Migrations/Migrations/20220527_InitialCreate.cs +++ b/samples/WebAPI_Dapper/Greetings_Migrations/Migrations/20220527_InitialCreate.cs @@ -14,13 +14,12 @@ public SqlInitialCreate(IAmAMigrationConfiguration configuration) public override void Up() { - /* 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); + .WithColumn(timestampColumn).AsDateTime().Nullable().WithDefault(SystemMethods.CurrentDateTime); var greeting = Create.Table("Greeting") .WithColumn("Id").AsInt32().NotNullable().PrimaryKey().Identity() diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/OutboxExtensions.cs b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/OutboxExtensions.cs index 8454364051..3a763c9021 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/OutboxExtensions.cs +++ b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/OutboxExtensions.cs @@ -1,7 +1,9 @@ using System; +using GreetingsEntities; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Npgsql; using Paramore.Brighter; using Paramore.Brighter.Extensions.DependencyInjection; using Paramore.Brighter.Extensions.Hosting; @@ -46,7 +48,16 @@ public static (IAmAnOutbox, Type, Type) MakeOutbox( private static (IAmAnOutbox, Type, Type) MakePostgresSqlOutbox(RelationalDatabaseConfiguration configuration) { - return (new PostgreSqlOutbox(configuration), typeof(NpgsqConnectionProvider), typeof(NpgsqlUnitOfWork)); + //if we want to use our IAmARelationalDatabaseConnectionProvider or IAmAABoxTransactionProvider + //from the Outbox in our handlers, then we need to construct an NpgsqlDataSource and register the composite types + //then pass that to the Outbox constructor so that connections created by the Outbox will be aware of + //those composite types + var dataSourceBuilder = new NpgsqlDataSourceBuilder(configuration.ConnectionString); + dataSourceBuilder.MapComposite(); + dataSourceBuilder.MapComposite(); + var dataSource = dataSourceBuilder.Build(); + + return (new PostgreSqlOutbox(configuration, dataSource), typeof(NpgsqConnectionProvider), typeof(NpgsqlUnitOfWork)); } private static (IAmAnOutbox, Type, Type) MakeMsSqlOutbox(RelationalDatabaseConfiguration configuration) diff --git a/src/Paramore.Brighter.Outbox.MsSql/MsSqlOutbox.cs b/src/Paramore.Brighter.Outbox.MsSql/MsSqlOutbox.cs index 20cbcb9f91..49eb52b15a 100644 --- a/src/Paramore.Brighter.Outbox.MsSql/MsSqlOutbox.cs +++ b/src/Paramore.Brighter.Outbox.MsSql/MsSqlOutbox.cs @@ -38,7 +38,7 @@ THE SOFTWARE. */ namespace Paramore.Brighter.Outbox.MsSql { /// - /// Class MsSqlOutbox. + /// Implements an Outbox using MSSQL as a backing store /// public class MsSqlOutbox : RelationDatabaseOutbox { @@ -48,7 +48,7 @@ public class MsSqlOutbox : RelationDatabaseOutbox private readonly IAmARelationalDbConnectionProvider _connectionProvider; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The configuration. /// The connection factory. @@ -61,7 +61,7 @@ public MsSqlOutbox(IAmARelationalDatabaseConfiguration configuration, IAmARelati } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The configuration. public MsSqlOutbox(IAmARelationalDatabaseConfiguration configuration) : this(configuration, diff --git a/src/Paramore.Brighter.Outbox.MySql/MySqlOutbox.cs b/src/Paramore.Brighter.Outbox.MySql/MySqlOutbox.cs index c8e94057d4..d2c92d5807 100644 --- a/src/Paramore.Brighter.Outbox.MySql/MySqlOutbox.cs +++ b/src/Paramore.Brighter.Outbox.MySql/MySqlOutbox.cs @@ -39,7 +39,7 @@ THE SOFTWARE. */ namespace Paramore.Brighter.Outbox.MySql { /// - /// Class MySqlOutbox. + /// Implements an outbox using Sqlite as a backing store /// public class MySqlOutbox : RelationDatabaseOutbox { @@ -49,6 +49,11 @@ public class MySqlOutbox : RelationDatabaseOutbox 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()) { @@ -57,6 +62,10 @@ public MySqlOutbox(IAmARelationalDatabaseConfiguration configuration, IAmARelati ContinueOnCapturedContext = false; } + /// + /// Initializes a new instance of the class. + /// + /// The configuration to connect to this data store public MySqlOutbox(IAmARelationalDatabaseConfiguration configuration) : this(configuration, new MySqlConnectionProvider(configuration)) { diff --git a/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutbox.cs b/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutbox.cs index 4cfe09df95..22f9f42631 100644 --- a/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutbox.cs +++ b/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutbox.cs @@ -38,6 +38,9 @@ THE SOFTWARE. */ 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(); @@ -45,6 +48,11 @@ public class PostgreSqlOutbox : RelationDatabaseOutbox 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, @@ -55,8 +63,17 @@ public PostgreSqlOutbox( _connectionProvider = connectionProvider; } - public PostgreSqlOutbox(IAmARelationalDatabaseConfiguration configuration) - : this(configuration, new NpgsqConnectionProvider(configuration)) + /// + /// 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 NpgsqConnectionProvider(configuration, dataSource)) { } protected override void WriteToStore( diff --git a/src/Paramore.Brighter.PostgreSql/NpgsqConnectionProvider.cs b/src/Paramore.Brighter.PostgreSql/NpgsqConnectionProvider.cs index a468c92769..2c9ec36c1d 100644 --- a/src/Paramore.Brighter.PostgreSql/NpgsqConnectionProvider.cs +++ b/src/Paramore.Brighter.PostgreSql/NpgsqConnectionProvider.cs @@ -11,18 +11,35 @@ namespace Paramore.Brighter.PostgreSql /// public class NpgsqConnectionProvider : RelationalDbConnectionProvider { + private NpgsqlDataSource _dataSource; private readonly string _connectionString; /// /// Initialise a new instance of PostgreSQl Connection provider from a connection string /// /// PostgreSQL Configuration - public NpgsqConnectionProvider(IAmARelationalDatabaseConfiguration 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 NpgsqConnectionProvider( + 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(); } /// @@ -32,11 +49,17 @@ public NpgsqConnectionProvider(IAmARelationalDatabaseConfiguration configuration /// 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. @@ -44,9 +67,22 @@ public override DbConnection GetConnection() /// A database connection public override async Task GetConnectionAsync(CancellationToken cancellationToken = default) { - var connection = new NpgsqlConnection(_connectionString); + 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/NpgsqlUnitOfWork.cs b/src/Paramore.Brighter.PostgreSql/NpgsqlUnitOfWork.cs index 0e39423e4e..55f9e56806 100644 --- a/src/Paramore.Brighter.PostgreSql/NpgsqlUnitOfWork.cs +++ b/src/Paramore.Brighter.PostgreSql/NpgsqlUnitOfWork.cs @@ -12,20 +12,37 @@ namespace Paramore.Brighter.PostgreSql /// public class NpgsqlUnitOfWork : RelationalDbTransactionProvider { + private NpgsqlDataSource _dataSource; private readonly string _connectionString; /// /// Initialise a new instance of PostgreSQl Connection provider from a connection string /// /// PostgreSQL Configuration - public NpgsqlUnitOfWork(IAmARelationalDatabaseConfiguration 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 NpgsqlUnitOfWork( + IAmARelationalDatabaseConfiguration configuration, + NpgsqlDataSource dataSource) { 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 /// @@ -48,6 +65,8 @@ public override Task CommitAsync(CancellationToken cancellationToken) /// 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; @@ -60,6 +79,8 @@ public override DbConnection GetConnection() /// 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; @@ -90,5 +111,17 @@ public override async Task GetTransactionAsync(CancellationToken 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; + } } } From 4b4415491cb65a0c77417adcff0be5a3b18da992 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Sat, 17 Jun 2023 12:45:26 +0100 Subject: [PATCH 70/89] Drop Dapper Extensions, simpler debugging --- .../.idea/httpRequests/http-requests-log.http | 336 ++++++++---------- .../WebAPI_Dapper/GreetingsEntities/Person.cs | 6 +- .../EntityMappers/GreetingsMapper.cs | 18 - .../EntityMappers/PersonMapper.cs | 17 - .../GreetingsPorts/GreetingsPorts.csproj | 1 - .../Handlers/AddGreetingHandlerAsync.cs | 38 +- .../Handlers/AddPersonHandlerAsync.cs | 9 +- .../Handlers/DeletePersonHandlerAsync.cs | 24 +- .../Handlers/FindPersonByNameHandlerAsync.cs | 6 +- .../GreetingsWeb/GreetingsWeb.csproj | 7 +- samples/WebAPI_Dapper/GreetingsWeb/Startup.cs | 67 +--- .../GreetingsWeb/appsettings.Production.json | 6 +- .../SalutationAnalytics/Program.cs | 11 +- .../EntityMappers/SalutationMapper.cs | 15 - .../Handlers/GreetingMadeHandler.cs | 38 +- .../SalutationPorts/Policies/Retry.cs | 12 +- .../Policies/SalutationPolicy.cs | 4 +- .../SalutationPorts/SalutationPorts.csproj | 1 - .../EntityMappers/GreetingsMapper.cs | 18 - .../EntityMappers/PersonMapper.cs | 17 - .../GreetingsPorts/GreetingsPorts.csproj | 1 - .../Handlers/AddGreetingHandlerAsync.cs | 61 ++-- .../Handlers/AddPersonHandlerAsync.cs | 16 +- .../Handlers/DeletePersonHandlerAsync.cs | 30 +- .../FIndGreetingsForPersonHandlerAsync.cs | 42 +-- .../Handlers/FindPersonByNameHandlerAsync.cs | 15 +- .../GreetingsWeb/GreetingsWeb.csproj | 7 +- .../GreetingsWeb/Startup.cs | 57 --- .../SalutationAnalytics/Program.cs | 9 - .../EntityMappers/SalutationMapper.cs | 15 - .../Handlers/GreetingMadeHandler.cs | 31 +- .../SalutationPorts/SalutationPorts.csproj | 1 - .../Paramore.Brighter.Inbox.Postgres.csproj | 9 +- ...Paramore.Brighter.Outbox.PostgreSql.csproj | 8 +- ...hter.PostgreSql.EntityFrameworkCore.csproj | 8 +- .../Paramore.Brighter.PostgreSql.csproj | 9 +- ...Paramore.Brighter.PostgresSQL.Tests.csproj | 6 + 37 files changed, 366 insertions(+), 610 deletions(-) delete mode 100644 samples/WebAPI_Dapper/GreetingsPorts/EntityMappers/GreetingsMapper.cs delete mode 100644 samples/WebAPI_Dapper/GreetingsPorts/EntityMappers/PersonMapper.cs delete mode 100644 samples/WebAPI_Dapper/SalutationPorts/EntityMappers/SalutationMapper.cs delete mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsPorts/EntityMappers/GreetingsMapper.cs delete mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsPorts/EntityMappers/PersonMapper.cs delete mode 100644 samples/WebAPI_Dapper_Kafka/SalutationPorts/EntityMappers/SalutationMapper.cs diff --git a/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http b/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http index 2d6a39b57d..084d7211cf 100644 --- a/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http +++ b/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http @@ -1,30 +1,30 @@ -POST http://localhost:5000/People/new +POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json -Content-Length: 23 +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" } -<> 2023-06-15T095915.500.json +<> 2023-06-17T124321.200.json ### -POST http://localhost:5000/People/new +POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json -Content-Length: 23 +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" } -<> 2023-06-15T091508.500.json +<> 2023-06-17T124315.200.json ### @@ -39,48 +39,34 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } +<> 2023-06-17T123334.500.json + ### -POST http://localhost:5000/People/new -Content-Type: application/json -Content-Length: 23 +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 -{ - "Name" : "Tyrion" -} +<> 2023-06-17T123324.200.json ### -POST http://localhost:5000/People/new -Content-Type: application/json -Content-Length: 23 +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 -{ - "Name" : "Tyrion" -} - -<> 2023-06-14T183740.500.json +<> 2023-06-17T120039.200.json ### -POST http://localhost:5000/People/new -Content-Type: application/json -Content-Length: 23 +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 -{ - "Name" : "Tyrion" -} - -<> 2023-06-14T183618.500.json +<> 2023-06-17T115435.200.json ### @@ -95,37 +81,25 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Name" : "Tyrion" } -<> 2023-06-14T183505.500.json +<> 2023-06-17T115417.200.json ### -POST http://localhost:5000/People/new -Content-Type: application/json -Content-Length: 23 +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 -{ - "Name" : "Tyrion" -} - -<> 2023-06-14T182436.500.json +<> 2023-06-17T112750.200.json ### -POST http://localhost:5000/People/new -Content-Type: application/json -Content-Length: 23 +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 -{ - "Name" : "Tyrion" -} - -<> 2023-06-14T100715.500.json +<> 2023-06-17T112627.200.json ### @@ -134,37 +108,31 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-06-14T090138.500.json +<> 2023-06-17T112146.500.json ### -POST http://localhost:5000/People/new -Content-Type: application/json -Content-Length: 23 +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 -{ - "Name" : "Tyrion" -} - -<> 2023-06-13T205200.500.json +<> 2023-06-17T095354.404.json ### -POST http://localhost:5000/People/new +POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json -Content-Length: 23 +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" } -<> 2023-06-13T205002.500.json +<> 2023-06-17T093904.200.json ### @@ -173,37 +141,23 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-06-13T204955.404.json +<> 2023-06-17T093854.200.json ### -POST http://localhost:5000/People/new -Content-Type: application/json -Content-Length: 23 +DELETE 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 -{ - "Name" : "Tyrion" -} - -<> 2023-06-13T171434.500.json - ### -POST http://localhost:5000/People/new -Content-Type: application/json -Content-Length: 23 +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 -{ - "Name" : "Tyrion" -} - -<> 2023-06-13T171423.500.json +<> 2023-06-17T093837.200.json ### @@ -212,18 +166,26 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-06-13T171320.404.json +<> 2023-06-17T093809.200.json ### -GET http://localhost:5000/People/Tyrion +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" +} + +<> 2023-06-17T093716.500.json + ### -GET http://localhost:5000/People/Tyrion +DELETE 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 @@ -235,32 +197,52 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip +<> 2023-06-17T093700.200.json + ### -GET http://localhost:5000/People/Tyrion +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 -<> 2023-06-12T184025.500.json +{ + "Name" : "Tyrion" +} + +<> 2023-06-16T215201.500.json ### -GET http://localhost:5000/People/Tyrion +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 -<> 2023-06-12T165500.500.json +{ + "Name" : "Tyrion" +} + +<> 2023-06-16T203958.500.json ### -GET http://localhost:5000/People/Tyrion +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 -<> 2023-06-12T141625.500.json +{ + "Name" : "Tyrion" +} + +<> 2023-06-15T095915.500.json ### @@ -275,16 +257,20 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Name" : "Tyrion" } -<> 2023-06-09T164917.500.json +<> 2023-06-15T091508.500.json ### -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 -<> 2023-06-09T164624.500.json +{ + "Greeting" : "I drink, and I know things" +} ### @@ -299,16 +285,20 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Name" : "Tyrion" } -<> 2023-06-09T164620.500.json - ### -GET http://localhost:5000/People/Tyrion +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 -<> 2023-06-09T154849.500.json +{ + "Name" : "Tyrion" +} + +<> 2023-06-14T183740.500.json ### @@ -323,7 +313,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Name" : "Tyrion" } -<> 2023-06-09T154836.500.json +<> 2023-06-14T183618.500.json ### @@ -338,7 +328,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Name" : "Tyrion" } -<> 2023-06-09T154833.500.json +<> 2023-06-14T183505.500.json ### @@ -353,22 +343,22 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Name" : "Tyrion" } -<> 2023-06-09T154824.500.json +<> 2023-06-14T182436.500.json ### -POST http://localhost:5000/Greetings/Tyrion/new +POST http://localhost:5000/People/new Content-Type: application/json -Content-Length: 47 +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" } -<> 2023-06-08T222134.200.json +<> 2023-06-14T100715.500.json ### @@ -377,109 +367,93 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-06-08T222121.200.json +<> 2023-06-14T090138.500.json ### -POST http://localhost:5000/Greetings/Tyrion/new +POST http://localhost:5000/People/new Content-Type: application/json -Content-Length: 47 +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" } -<> 2023-06-02T190642.200.json +<> 2023-06-13T205200.500.json ### -POST http://localhost:5000/Greetings/Tyrion/new +POST http://localhost:5000/People/new Content-Type: application/json -Content-Length: 47 +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" } -<> 2023-06-02T190609.200.json +<> 2023-06-13T205002.500.json ### -POST http://localhost:5000/Greetings/Tyrion/new -Content-Type: application/json -Content-Length: 47 +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-06-13T204955.404.json ### -POST http://localhost:5000/Greetings/Tyrion/new +POST http://localhost:5000/People/new Content-Type: application/json -Content-Length: 47 +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" } +<> 2023-06-13T171434.500.json + ### -POST http://localhost:5000/Greetings/Tyrion/new +POST http://localhost:5000/People/new Content-Type: application/json -Content-Length: 47 +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" } -<> 2023-06-02T184827.200.json +<> 2023-06-13T171423.500.json ### -POST http://localhost:5000/Greetings/Tyrion/new -Content-Type: application/json -Content-Length: 47 +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-06-02T182443.200.json +<> 2023-06-13T171320.404.json ### -POST http://localhost:5000/Greetings/Tyrion/new -Content-Type: application/json -Content-Length: 47 +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-06-02T164746.500.json - ### GET http://localhost:5000/People/Tyrion @@ -487,37 +461,21 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-06-02T164740.200.json - ### -POST http://localhost:5000/Greetings/Tyrion/new -Content-Type: application/json -Content-Length: 47 +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-06-02T164556.200.json - ### -POST http://localhost:5000/Greetings/Tyrion/new -Content-Type: application/json -Content-Length: 47 +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-06-02T164104.200.json +<> 2023-06-12T184025.500.json ### @@ -526,7 +484,7 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-06-02T164101.200.json +<> 2023-06-12T165500.500.json ### @@ -535,22 +493,22 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-06-02T164056.200.json +<> 2023-06-12T141625.500.json ### -POST http://localhost:5000/Greetings/Tyrion/new +POST http://localhost:5000/People/new Content-Type: application/json -Content-Length: 47 +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" } -<> 2023-05-28T155036.200.json +<> 2023-06-09T164917.500.json ### @@ -559,58 +517,76 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip +<> 2023-06-09T164624.500.json + ### -POST http://localhost:5000/Greetings/Tyrion/new +POST http://localhost:5000/People/new Content-Type: application/json -Content-Length: 47 +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" } +<> 2023-06-09T164620.500.json + ### -POST http://localhost:5000/Greetings/Tyrion/new -Content-Type: application/json -Content-Length: 47 +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-06-09T154849.500.json ### -GET http://localhost:5000/People/Tyrion +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 -<> 2023-05-28T153646.200.json +{ + "Name" : "Tyrion" +} + +<> 2023-06-09T154836.500.json ### -GET http://localhost:5000/People/Tyrion +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 -<> 2023-05-28T153237.200.json +{ + "Name" : "Tyrion" +} + +<> 2023-06-09T154833.500.json ### -GET http://localhost:5000/People/Tyrion +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 -<> 2023-05-28T153234.200.json +{ + "Name" : "Tyrion" +} + +<> 2023-06-09T154824.500.json ### 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 564807e0b5..864e81571b 100644 --- a/samples/WebAPI_Dapper/GreetingsPorts/GreetingsPorts.csproj +++ b/samples/WebAPI_Dapper/GreetingsPorts/GreetingsPorts.csproj @@ -11,7 +11,6 @@ - diff --git a/samples/WebAPI_Dapper/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs b/samples/WebAPI_Dapper/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs index d00cd0c318..88e87c9c07 100644 --- a/samples/WebAPI_Dapper/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs +++ b/samples/WebAPI_Dapper/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs @@ -3,8 +3,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -using DapperExtensions; -using DapperExtensions.Predicate; +using Dapper; using Paramore.Brighter; using GreetingsEntities; using GreetingsPorts.Requests; @@ -41,23 +40,32 @@ public override async Task HandleAsync(AddGreeting addGreeting, Can 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 people = await conn.QueryAsync( + "select * from Person where name = @name", + new {name = addGreeting.Name}, + tx + ); + var person = people.SingleOrDefault(); - var greeting = new Greeting(addGreeting.Greeting, person); + if (person != null) + { + var greeting = new Greeting(addGreeting.Greeting, person); - //write the added child entity to the Db - await conn.InsertAsync(greeting, tx); + //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)); + //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); + //commit both new greeting and outgoing message + await _transactionProvider.CommitAsync(cancellationToken); + } } catch (Exception e) { diff --git a/samples/WebAPI_Dapper/GreetingsPorts/Handlers/AddPersonHandlerAsync.cs b/samples/WebAPI_Dapper/GreetingsPorts/Handlers/AddPersonHandlerAsync.cs index ef1cf1b157..f5e0d276d9 100644 --- a/samples/WebAPI_Dapper/GreetingsPorts/Handlers/AddPersonHandlerAsync.cs +++ b/samples/WebAPI_Dapper/GreetingsPorts/Handlers/AddPersonHandlerAsync.cs @@ -1,7 +1,6 @@ using System.Threading; using System.Threading.Tasks; -using DapperExtensions; -using GreetingsEntities; +using Dapper; using GreetingsPorts.Requests; using Paramore.Brighter; using Paramore.Brighter.Logging.Attributes; @@ -22,10 +21,8 @@ public AddPersonHandlerAsync(IAmARelationalDbConnectionProvider relationalDbConn [UsePolicyAsync(step:1, policy: Policies.Retry.EXPONENTIAL_RETRYPOLICYASYNC)] public override async Task HandleAsync(AddPerson addPerson, CancellationToken cancellationToken = default) { - //await using var connection = await _relationalDbConnectionProvider.GetConnectionAsync(cancellationToken); - using var connection = _relationalDbConnectionProvider.GetConnection(); - //await connection.InsertAsync(new Person(addPerson.Name)); - connection.Insert(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 ba280dba14..16b71a1ad0 100644 --- a/samples/WebAPI_Dapper/GreetingsPorts/Handlers/DeletePersonHandlerAsync.cs +++ b/samples/WebAPI_Dapper/GreetingsPorts/Handlers/DeletePersonHandlerAsync.cs @@ -2,8 +2,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -using DapperExtensions; -using DapperExtensions.Predicate; +using Dapper; using GreetingsEntities; using GreetingsPorts.Requests; using Paramore.Brighter; @@ -29,16 +28,21 @@ public override async Task HandleAsync(DeletePerson deletePerson, var tx = await connection.BeginTransactionAsync(cancellationToken); try { + var people = await connection.QueryAsync( + "select * from Person where name = @name", + new {name = deletePerson.Name} + ); + var person = people.SingleOrDefault(); - var searchbyName = Predicates.Field(p => p.Name, Operator.Eq, deletePerson.Name); - var people = await connection - .GetListAsync(searchbyName, transaction: tx); - var person = people.Single(); + if (person != null) + { + await connection.ExecuteAsync( + "delete from Greeting where PersonId = @PersonId", + new { PersonId = person.Id }, + tx); - var deleteById = Predicates.Field(g => g.RecipientId, Operator.Eq, person.Id); - await connection.DeleteAsync(deleteById, tx); - - await tx.CommitAsync(cancellationToken); + await tx.CommitAsync(cancellationToken); + } } catch (Exception) { diff --git a/samples/WebAPI_Dapper/GreetingsPorts/Handlers/FindPersonByNameHandlerAsync.cs b/samples/WebAPI_Dapper/GreetingsPorts/Handlers/FindPersonByNameHandlerAsync.cs index 09d3503f55..7ada5290dc 100644 --- a/samples/WebAPI_Dapper/GreetingsPorts/Handlers/FindPersonByNameHandlerAsync.cs +++ b/samples/WebAPI_Dapper/GreetingsPorts/Handlers/FindPersonByNameHandlerAsync.cs @@ -1,8 +1,7 @@ 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; @@ -27,9 +26,8 @@ public FindPersonByNameHandlerAsync(IAmARelationalDbConnectionProvider relationa [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); await using var connection = await _relationalDbConnectionProvider .GetConnectionAsync(cancellationToken); - var people = await connection.GetListAsync(searchByName); + 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/GreetingsWeb/GreetingsWeb.csproj b/samples/WebAPI_Dapper/GreetingsWeb/GreetingsWeb.csproj index c193e5d5b2..9f3771e6dc 100644 --- a/samples/WebAPI_Dapper/GreetingsWeb/GreetingsWeb.csproj +++ b/samples/WebAPI_Dapper/GreetingsWeb/GreetingsWeb.csproj @@ -8,7 +8,6 @@ - @@ -55,4 +54,10 @@ <_ContentIncludedByDefault Remove="out\GreetingsAdapters.runtimeconfig.json" /> + + + ..\..\..\libs\Npgsql\net6.0\Npgsql.dll + + + diff --git a/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs b/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs index 61c1d17e79..98e58234b9 100644 --- a/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs +++ b/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs @@ -1,11 +1,6 @@ using System; -using Dapper; -using DapperExtensions; -using DapperExtensions.Sql; using FluentMigrator.Runner; using Greetings_MySqlMigrations.Migrations; -using GreetingsEntities; -using GreetingsPorts.EntityMappers; using GreetingsPorts.Handlers; using GreetingsPorts.Policies; using GreetingsWeb.Database; @@ -16,12 +11,10 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.OpenApi.Models; -using Npgsql; using OpenTelemetry.Metrics; using OpenTelemetry.Trace; using Paramore.Brighter; using Paramore.Brighter.Extensions.DependencyInjection; -using Paramore.Brighter.Extensions.Hosting; using Paramore.Brighter.MessagingGateway.RMQ; using Paramore.Darker.AspNetCore; using Paramore.Darker.Policies; @@ -83,7 +76,6 @@ public void ConfigureServices(IServiceCollection services) .AddAspNetCoreInstrumentation() .AddConsoleExporter()); - ConfigureDapper(); ConfigureMigration(services); ConfigureBrighter(services); ConfigureDarker(services); @@ -164,63 +156,6 @@ private void ConfigureSqlite(IServiceCollection services) .AddSingleton(new MigrationConfiguration(){DbType = DatabaseType.Sqlite.ToString()}); } - private void ConfigureDapper() - { - ConfigureDapperByHost(GetDatabaseType()); - - DapperExtensions.DapperExtensions.SetMappingAssemblies(new[] { typeof(PersonMapper).Assembly }); - DapperAsyncExtensions.SetMappingAssemblies(new[] { typeof(PersonMapper).Assembly }); - } - - private static void ConfigureDapperByHost(DatabaseType databaseType) - { - switch (databaseType) - { - case DatabaseType.Sqlite: - ConfigureDapperSqlite(); - break; - case DatabaseType.MySql: - ConfigureDapperMySql(); - break; - case DatabaseType.MsSql: - ConfigureDapperMsSql(); - break; - case DatabaseType.Postgres: - ConfigureDapperPostgreSql(); - break; - default: - throw new ArgumentOutOfRangeException(nameof(databaseType), "Database type is not supported"); - } - } - - private static void ConfigureDapperMsSql() - { - var sqlDialect = new SqlServerDialect(); - DapperExtensions.DapperExtensions.SqlDialect = sqlDialect; - DapperAsyncExtensions.SqlDialect = sqlDialect; - } - - private static void ConfigureDapperSqlite() - { - var sqlDialect = new SqliteDialect(); - DapperExtensions.DapperExtensions.SqlDialect = sqlDialect; - DapperAsyncExtensions.SqlDialect = sqlDialect; - } - - private static void ConfigureDapperMySql() - { - var sqlDialect = new MySqlDialect(); - DapperExtensions.DapperExtensions.SqlDialect = sqlDialect; - DapperAsyncExtensions.SqlDialect = sqlDialect; - } - - private static void ConfigureDapperPostgreSql() - { - var sqlDialect = new PostgreSqlDialect(); - DapperExtensions.DapperExtensions.SqlDialect = sqlDialect; - DapperAsyncExtensions.SqlDialect = sqlDialect; - } - private void ConfigureBrighter(IServiceCollection services) { var outboxConfiguration = new RelationalDatabaseConfiguration( @@ -266,10 +201,12 @@ private void ConfigureBrighter(IServiceCollection services) configure.TransactionProvider = makeOutbox.transactionProvider; configure.ConnectionProvider = makeOutbox.connectionProvider; }) + /* .UseOutboxSweeper(options => { options.TimerInterval = 5; options.MinimumMessageAge = 5000; }) + */ .AutoFromAssemblies(typeof(AddPersonHandlerAsync).Assembly); } diff --git a/samples/WebAPI_Dapper/GreetingsWeb/appsettings.Production.json b/samples/WebAPI_Dapper/GreetingsWeb/appsettings.Production.json index 2a64a02529..e26000a3d4 100644 --- a/samples/WebAPI_Dapper/GreetingsWeb/appsettings.Production.json +++ b/samples/WebAPI_Dapper/GreetingsWeb/appsettings.Production.json @@ -7,11 +7,11 @@ } }, "ConnectionStrings": { - "GreetingsMySql": "server=localhost; port=3306; uid=root; pwd=root; database=Greetings", + "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;", + "GreetingsPostgreSql": "Server=localhost; Port=5432; Database=greetings; Username=postgres; Password=password", "PostgreSqlDb": "Server=localhost; Port=5432; Username=postgres; Password=password", - "GreetingsMsSql": "Server=localhost,1433;Username=sa;Password=Password123!;Database=Greetings;", + "GreetingsMsSql": "Server=localhost,1433;Username=sa;Password=Password123!;Database=Greetings", "MsSqlDb": "Server=localhost,1433;Username=sa;Password=Password123!" } } \ No newline at end of file diff --git a/samples/WebAPI_Dapper/SalutationAnalytics/Program.cs b/samples/WebAPI_Dapper/SalutationAnalytics/Program.cs index aa8f4be54b..d87e6559dc 100644 --- a/samples/WebAPI_Dapper/SalutationAnalytics/Program.cs +++ b/samples/WebAPI_Dapper/SalutationAnalytics/Program.cs @@ -1,8 +1,6 @@ using System; using System.IO; using System.Threading.Tasks; -using DapperExtensions; -using DapperExtensions.Sql; using FluentMigrator.Runner; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -19,7 +17,6 @@ using Paramore.Brighter.ServiceActivator.Extensions.Hosting; using Paramore.Brighter.Sqlite; using SalutationAnalytics.Database; -using SalutationPorts.EntityMappers; using SalutationPorts.Policies; using SalutationPorts.Requests; @@ -69,7 +66,7 @@ private static void ConfigureBrighter(HostBuilderContext hostContext, IServiceCo 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 @@ -159,8 +156,6 @@ private static void ConfigureMigration(HostBuilderContext hostBuilderContext, IS private static void ConfigureDapper(HostBuilderContext hostBuilderContext, IServiceCollection services) { 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) @@ -180,16 +175,12 @@ 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(); } private static void ConfigureDapperMySql(IServiceCollection services) { - DapperExtensions.DapperExtensions.SqlDialect = new MySqlDialect(); - DapperAsyncExtensions.SqlDialect = new MySqlDialect(); services.AddScoped(); services.AddScoped(); } 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 ad567bb463..eb4a0ff3b6 100644 --- a/samples/WebAPI_Dapper/SalutationPorts/Handlers/GreetingMadeHandler.cs +++ b/samples/WebAPI_Dapper/SalutationPorts/Handlers/GreetingMadeHandler.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using DapperExtensions; +using Dapper; using Microsoft.Extensions.Logging; using Paramore.Brighter; using Paramore.Brighter.Logging.Attributes; @@ -12,13 +10,13 @@ namespace SalutationPorts.Handlers { - public class GreetingMadeHandlerAsync : RequestHandlerAsync + public class GreetingMadeHandler : RequestHandler { private readonly IAmATransactionConnectionProvider _transactionConnectionProvider; private readonly IAmACommandProcessor _postBox; - private readonly ILogger _logger; + private readonly ILogger _logger; - public GreetingMadeHandlerAsync(IAmATransactionConnectionProvider transactionConnectionProvider, IAmACommandProcessor postBox, ILogger logger) + public GreetingMadeHandler(IAmATransactionConnectionProvider transactionConnectionProvider, IAmACommandProcessor postBox, ILogger logger) { _transactionConnectionProvider = transactionConnectionProvider; _postBox = postBox; @@ -26,39 +24,41 @@ public GreetingMadeHandlerAsync(IAmATransactionConnectionProvider transactionCon } //[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) + [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 _transactionConnectionProvider.GetTransactionAsync(cancellationToken); + var tx = _transactionConnectionProvider.GetTransaction(); try { var salutation = new Salutation(@event.Greeting); - await _transactionConnectionProvider.GetConnection().InsertAsync(salutation, tx); + _transactionConnectionProvider.GetConnection().Execute( + "insert into Salutation (greeting) values (@greeting)", + new {greeting = salutation.Greeting}, + tx); - posts.Add(await _postBox.DepositPostAsync( + posts.Add(_postBox.DepositPost( new SalutationReceived(DateTimeOffset.Now), - _transactionConnectionProvider, - cancellationToken: cancellationToken)); + _transactionConnectionProvider)); - await tx.CommitAsync(cancellationToken); + tx.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); + tx.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 4658d4de3b..9dbe4c27ca 100644 --- a/samples/WebAPI_Dapper/SalutationPorts/SalutationPorts.csproj +++ b/samples/WebAPI_Dapper/SalutationPorts/SalutationPorts.csproj @@ -11,7 +11,6 @@ - diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/EntityMappers/GreetingsMapper.cs b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/EntityMappers/GreetingsMapper.cs deleted file mode 100644 index ede3108fab..0000000000 --- a/samples/WebAPI_Dapper_Kafka/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_Kafka/GreetingsPorts/EntityMappers/PersonMapper.cs b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/EntityMappers/PersonMapper.cs deleted file mode 100644 index 7a29337bbb..0000000000 --- a/samples/WebAPI_Dapper_Kafka/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_Kafka/GreetingsPorts/GreetingsPorts.csproj b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/GreetingsPorts.csproj index 564807e0b5..864e81571b 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/GreetingsPorts.csproj +++ b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/GreetingsPorts.csproj @@ -11,7 +11,6 @@ - diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs index d87aca5968..88e87c9c07 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs +++ b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs @@ -3,8 +3,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -using DapperExtensions; -using DapperExtensions.Predicate; +using Dapper; using Paramore.Brighter; using GreetingsEntities; using GreetingsPorts.Requests; @@ -18,12 +17,12 @@ public class AddGreetingHandlerAsync: RequestHandlerAsync { private readonly IAmACommandProcessor _postBox; private readonly ILogger _logger; - private readonly IAmATransactionConnectionProvider _transactionConnectionProvider; + private readonly IAmATransactionConnectionProvider _transactionProvider; - public AddGreetingHandlerAsync(IAmATransactionConnectionProvider transactionConnectionProvider, IAmACommandProcessor postBox, ILogger logger) + public AddGreetingHandlerAsync(IAmATransactionConnectionProvider transactionProvider, IAmACommandProcessor postBox, ILogger logger) { - _transactionConnectionProvider = transactionConnectionProvider; //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; } @@ -33,45 +32,51 @@ public AddGreetingHandlerAsync(IAmATransactionConnectionProvider transactionConn public override async Task HandleAsync(AddGreeting addGreeting, CancellationToken cancellationToken = default) { var posts = new List(); + + //We use the unit of work to grab connection and transaction, because Outbox needs + //to share them 'behind the scenes' - //NOTE: We are using the transaction connection provider to get a connection, so we do not own the connection - //and we should use the transaction connection provider to manage it - var conn = await _transactionConnectionProvider.GetConnectionAsync(cancellationToken); - //NOTE: We are using a transaction, but as we are using the Outbox, we ask the transaction connection provider for a transaction - //and allow it to manage the transaction for us. This is because we want to use the same transaction for the outgoing message - var tx = await _transactionConnectionProvider.GetTransactionAsync(cancellationToken); + 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 people = await conn.QueryAsync( + "select * from Person where name = @name", + new {name = addGreeting.Name}, + tx + ); + var person = people.SingleOrDefault(); - var greeting = new Greeting(addGreeting.Greeting, person); + if (person != null) + { + var greeting = new Greeting(addGreeting.Greeting, person); - //write the added child entity to the Db - await conn.InsertAsync(greeting, tx); + //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()), - _transactionConnectionProvider, - cancellationToken: cancellationToken) - ); + //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 _transactionConnectionProvider.CommitAsync(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 _transactionConnectionProvider.RollbackAsync(cancellationToken); + await _transactionProvider.RollbackAsync(cancellationToken); return await base.HandleAsync(addGreeting, cancellationToken); } finally { - //in the finally block, tell the transaction provider that we are done. - _transactionConnectionProvider.Close(); + _transactionProvider.Close(); } //Send this message via a transport. We need the ids to send just the messages here, not all outstanding ones. diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/AddPersonHandlerAsync.cs b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/AddPersonHandlerAsync.cs index 9385f37556..f5e0d276d9 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/AddPersonHandlerAsync.cs +++ b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/AddPersonHandlerAsync.cs @@ -1,7 +1,6 @@ using System.Threading; using System.Threading.Tasks; -using DapperExtensions; -using GreetingsEntities; +using Dapper; using GreetingsPorts.Requests; using Paramore.Brighter; using Paramore.Brighter.Logging.Attributes; @@ -11,22 +10,19 @@ namespace GreetingsPorts.Handlers { public class AddPersonHandlerAsync : RequestHandlerAsync { - private readonly IAmARelationalDbConnectionProvider _connectionProvider; + private readonly IAmARelationalDbConnectionProvider _relationalDbConnectionProvider; - public AddPersonHandlerAsync(IAmARelationalDbConnectionProvider connectionProvider) + public AddPersonHandlerAsync(IAmARelationalDbConnectionProvider relationalDbConnectionProvider) { - _connectionProvider = connectionProvider; + _relationalDbConnectionProvider = relationalDbConnectionProvider; } [RequestLoggingAsync(0, HandlerTiming.Before)] [UsePolicyAsync(step:1, policy: Policies.Retry.EXPONENTIAL_RETRYPOLICYASYNC)] public override async Task HandleAsync(AddPerson addPerson, CancellationToken cancellationToken = default) { - using (var connection = await _connectionProvider.GetConnectionAsync(cancellationToken)) - { - await connection.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_Kafka/GreetingsPorts/Handlers/DeletePersonHandlerAsync.cs b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/DeletePersonHandlerAsync.cs index 24f7b3055c..16b71a1ad0 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/DeletePersonHandlerAsync.cs +++ b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/DeletePersonHandlerAsync.cs @@ -2,8 +2,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -using DapperExtensions; -using DapperExtensions.Predicate; +using Dapper; using GreetingsEntities; using GreetingsPorts.Requests; using Paramore.Brighter; @@ -14,7 +13,7 @@ namespace GreetingsPorts.Handlers { public class DeletePersonHandlerAsync : RequestHandlerAsync { - private readonly IAmARelationalDbConnectionProvider _relationalDbConnectionProvider; + private readonly IAmARelationalDbConnectionProvider _relationalDbConnectionProvider; public DeletePersonHandlerAsync(IAmARelationalDbConnectionProvider relationalDbConnectionProvider) { @@ -26,22 +25,24 @@ public DeletePersonHandlerAsync(IAmARelationalDbConnectionProvider relationalDbC public override async Task HandleAsync(DeletePerson deletePerson, CancellationToken cancellationToken = default) { var connection = await _relationalDbConnectionProvider.GetConnectionAsync(cancellationToken); - await connection.OpenAsync(cancellationToken); - - //NOTE: we are using a transaction, but a connection provider will not manage one for us, so we need to do it ourselves var tx = await connection.BeginTransactionAsync(cancellationToken); try { + var people = await connection.QueryAsync( + "select * from Person where name = @name", + new {name = deletePerson.Name} + ); + var person = people.SingleOrDefault(); - var searchbyName = Predicates.Field(p => p.Name, Operator.Eq, deletePerson.Name); - var people = await _relationalDbConnectionProvider.GetConnection() - .GetListAsync(searchbyName, transaction: tx); - var person = people.Single(); + if (person != null) + { + await connection.ExecuteAsync( + "delete from Greeting where PersonId = @PersonId", + new { PersonId = person.Id }, + tx); - var deleteById = Predicates.Field(g => g.RecipientId, Operator.Eq, person.Id); - await _relationalDbConnectionProvider.GetConnection().DeleteAsync(deleteById, tx); - - await tx.CommitAsync(cancellationToken); + await tx.CommitAsync(cancellationToken); + } } catch (Exception) { @@ -53,6 +54,7 @@ public override async Task HandleAsync(DeletePerson deletePerson, { await connection.DisposeAsync(); await tx.DisposeAsync(); + } return await base.HandleAsync(deletePerson, cancellationToken); diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/FIndGreetingsForPersonHandlerAsync.cs b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/FIndGreetingsForPersonHandlerAsync.cs index 79f165d20a..b274354018 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/FIndGreetingsForPersonHandlerAsync.cs +++ b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/FIndGreetingsForPersonHandlerAsync.cs @@ -1,5 +1,4 @@ -using System.Data.Common; -using System.Linq; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Dapper; @@ -16,9 +15,9 @@ namespace GreetingsPorts.Handlers { public class FIndGreetingsForPersonHandlerAsync : QueryHandlerAsync { - private readonly IAmARelationalDbConnectionProvider _relationalDbConnectionProvider; + private readonly IAmARelationalDbConnectionProvider _relationalDbConnectionProvider; - public FIndGreetingsForPersonHandlerAsync(IAmARelationalDbConnectionProvider relationalDbConnectionProvider) + public FIndGreetingsForPersonHandlerAsync(IAmARelationalDbConnectionProvider relationalDbConnectionProvider) { _relationalDbConnectionProvider = relationalDbConnectionProvider; } @@ -34,29 +33,26 @@ public FIndGreetingsForPersonHandlerAsync(IAmARelationalDbConnectionProvider re var sql = @"select p.Id, p.Name, g.Id, g.Message from Person p inner join Greeting g on g.Recipient_Id = p.Id"; - - using (var connection = await _relationalDbConnectionProvider.GetConnectionAsync(cancellationToken)) + await using var connection = await _relationalDbConnectionProvider.GetConnectionAsync(cancellationToken); + var people = await connection.QueryAsync(sql, (person, greeting) => { - var people = await connection.QueryAsync(sql, (person, greeting) => - { - person.Greetings.Add(greeting); - return person; - }, splitOn: "Id"); + person.Greetings.Add(greeting); + return person; + }, splitOn: "Id"); - var peopleGreetings = people.GroupBy(p => p.Id).Select(grp => - { - var groupedPerson = grp.First(); - groupedPerson.Greetings = grp.Select(p => p.Greetings.Single()).ToList(); - return groupedPerson; - }); + var peopleGreetings = people.GroupBy(p => p.Id).Select(grp => + { + var groupedPerson = grp.First(); + groupedPerson.Greetings = grp.Select(p => p.Greetings.Single()).ToList(); + return groupedPerson; + }); - var person = peopleGreetings.Single(); + var person = peopleGreetings.Single(); - return new FindPersonsGreetings - { - Name = person.Name, Greetings = person.Greetings.Select(g => new Salutation(g.Greet())) - }; - } + return new FindPersonsGreetings + { + Name = person.Name, Greetings = person.Greetings.Select(g => new Salutation(g.Greet())) + }; } } diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/FindPersonByNameHandlerAsync.cs b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/FindPersonByNameHandlerAsync.cs index d10c65e23d..7ada5290dc 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/FindPersonByNameHandlerAsync.cs +++ b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/FindPersonByNameHandlerAsync.cs @@ -1,9 +1,7 @@ -using System.Data.Common; 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; @@ -28,14 +26,11 @@ public FindPersonByNameHandlerAsync(IAmARelationalDbConnectionProvider relationa [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); - using (var connection = await _relationalDbConnectionProvider .GetConnectionAsync(cancellationToken)) - { - var people = await connection.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); - } + return new FindPersonResult(person); } } } diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/GreetingsWeb.csproj b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/GreetingsWeb.csproj index 0eced8cda6..f60f5e7db4 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/GreetingsWeb.csproj +++ b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/GreetingsWeb.csproj @@ -8,7 +8,6 @@ - @@ -58,4 +57,10 @@ <_ContentIncludedByDefault Remove="out\GreetingsAdapters.runtimeconfig.json" /> + + + ..\..\..\libs\Npgsql\net6.0\Npgsql.dll + + + diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs index 1d0c349d5d..5431109a20 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs +++ b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs @@ -1,12 +1,9 @@ using System; using Confluent.SchemaRegistry; -using DapperExtensions; -using DapperExtensions.Sql; using FluentMigrator.Runner; using Greetings_MySqlMigrations.Migrations; using Greetings_PostgreSqlMigrations.Migrations; using Greetings_SqliteMigrations.Migrations; -using GreetingsPorts.EntityMappers; using GreetingsPorts.Handlers; using GreetingsPorts.Policies; using GreetingsWeb.Database; @@ -84,7 +81,6 @@ public void ConfigureServices(IServiceCollection services) .AddConsoleExporter()); ConfigureMigration(services); - ConfigureDapper(); ConfigureBrighter(services); ConfigureDarker(services); } @@ -162,59 +158,6 @@ private void ConfigureSqlite(IServiceCollection services) }); } - private void ConfigureDapper() - { - ConfigureDapperByHost(GetDatabaseType()); - - DapperExtensions.DapperExtensions.SetMappingAssemblies(new[] { typeof(PersonMapper).Assembly }); - DapperAsyncExtensions.SetMappingAssemblies(new[] { typeof(PersonMapper).Assembly }); - } - - private static void ConfigureDapperByHost(DatabaseType databaseType) - { - switch (databaseType) - { - case DatabaseType.Sqlite: - ConfigureDapperSqlite(); - break; - case DatabaseType.MySql: - ConfigureDapperMySql(); - break; - case DatabaseType.MsSql: - ConfigureDapperMsSql(); - break; - case DatabaseType.Postgres: - ConfigureDapperPostgreSql(); - break; - default: - throw new ArgumentOutOfRangeException(nameof(databaseType), "Database type is not supported"); - } - } - - private static void ConfigureDapperMsSql() - { - DapperExtensions.DapperExtensions.SqlDialect = new SqlServerDialect(); - DapperAsyncExtensions.SqlDialect = new SqlServerDialect(); - } - - private static void ConfigureDapperSqlite() - { - DapperExtensions.DapperExtensions.SqlDialect = new SqliteDialect(); - DapperAsyncExtensions.SqlDialect = new SqliteDialect(); - } - - private static void ConfigureDapperMySql() - { - DapperExtensions.DapperExtensions.SqlDialect = new MySqlDialect(); - DapperAsyncExtensions.SqlDialect = new MySqlDialect(); - } - - private static void ConfigureDapperPostgreSql() - { - DapperExtensions.DapperExtensions.SqlDialect = new PostgreSqlDialect(); - DapperAsyncExtensions.SqlDialect = new PostgreSqlDialect(); - } - private void ConfigureBrighter(IServiceCollection services) { var outboxConfiguration = new RelationalDatabaseConfiguration( diff --git a/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Program.cs b/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Program.cs index 8a48162ad4..ccb2904992 100644 --- a/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Program.cs +++ b/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Program.cs @@ -3,8 +3,6 @@ using System.Threading.Tasks; using Confluent.Kafka; using Confluent.SchemaRegistry; -using DapperExtensions; -using DapperExtensions.Sql; using FluentMigrator.Runner; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -21,7 +19,6 @@ using Paramore.Brighter.ServiceActivator.Extensions.Hosting; using Paramore.Brighter.Sqlite; using SalutationAnalytics.Database; -using SalutationPorts.EntityMappers; using SalutationPorts.Policies; using SalutationPorts.Requests; @@ -174,8 +171,6 @@ private static void ConfigureMigration(HostBuilderContext hostBuilderContext, IS private static void ConfigureDapper(HostBuilderContext hostBuilderContext, IServiceCollection services) { 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) @@ -195,16 +190,12 @@ 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(); } private static void ConfigureDapperMySql(IServiceCollection services) { - DapperExtensions.DapperExtensions.SqlDialect = new MySqlDialect(); - DapperAsyncExtensions.SqlDialect = new MySqlDialect(); services.AddScoped(); services.AddScoped(); } diff --git a/samples/WebAPI_Dapper_Kafka/SalutationPorts/EntityMappers/SalutationMapper.cs b/samples/WebAPI_Dapper_Kafka/SalutationPorts/EntityMappers/SalutationMapper.cs deleted file mode 100644 index aacce5f63e..0000000000 --- a/samples/WebAPI_Dapper_Kafka/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_Kafka/SalutationPorts/Handlers/GreetingMadeHandler.cs b/samples/WebAPI_Dapper_Kafka/SalutationPorts/Handlers/GreetingMadeHandler.cs index 4fdde9d753..eb4a0ff3b6 100644 --- a/samples/WebAPI_Dapper_Kafka/SalutationPorts/Handlers/GreetingMadeHandler.cs +++ b/samples/WebAPI_Dapper_Kafka/SalutationPorts/Handlers/GreetingMadeHandler.cs @@ -1,9 +1,8 @@ using System; using System.Collections.Generic; -using DapperExtensions; +using Dapper; using Microsoft.Extensions.Logging; using Paramore.Brighter; -using Paramore.Brighter.Inbox.Attributes; using Paramore.Brighter.Logging.Attributes; using Paramore.Brighter.Policies.Attributes; using SalutationEntities; @@ -13,19 +12,10 @@ namespace SalutationPorts.Handlers { public class GreetingMadeHandler : RequestHandler { - private readonly IAmATransactionConnectionProvider _transactionConnectionProvider; + private readonly IAmATransactionConnectionProvider _transactionConnectionProvider; private readonly IAmACommandProcessor _postBox; private readonly ILogger _logger; - /* - * KAFKA and ASYNC: Kafka is oriented around the idea of an ordered append log of events. You will lose that ordering - * if you use an async handler, because your handlers will not necessarily complete in order. Because a Brighter consumer - * is single-threaded we guarantee your ordering, provided you don't use async handlers. If you do, there are no - * guarantees about order. - * Generally, you should not use async handlers with Kafka, unless you are happy to lose ordering. - * Instead, rely on being able to partition your topic such that a single thread can handle the number of messages - * arriving on that thread with an acceptable latency. - */ public GreetingMadeHandler(IAmATransactionConnectionProvider transactionConnectionProvider, IAmACommandProcessor postBox, ILogger logger) { _transactionConnectionProvider = transactionConnectionProvider; @@ -33,32 +23,35 @@ public GreetingMadeHandler(IAmATransactionConnectionProvider transactionConnecti _logger = logger; } - [UseInbox(step:0, contextKey: typeof(GreetingMadeHandler), onceOnly: true )] + //[UseInboxAsync(step:0, contextKey: typeof(GreetingMadeHandlerAsync), onceOnly: true )] -- we are using a global inbox, so need to be explicit!! [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 = _transactionConnectionProvider.GetTransaction(); + + var tx = _transactionConnectionProvider.GetTransaction(); try { var salutation = new Salutation(@event.Greeting); - _transactionConnectionProvider.GetConnection().Insert(salutation, tx); + _transactionConnectionProvider.GetConnection().Execute( + "insert into Salutation (greeting) values (@greeting)", + new {greeting = salutation.Greeting}, + tx); posts.Add(_postBox.DepositPost( - new SalutationReceived(DateTimeOffset.Now), + new SalutationReceived(DateTimeOffset.Now), _transactionConnectionProvider)); - _transactionConnectionProvider.Commit(); + tx.Commit(); } catch (Exception e) { _logger.LogError(e, "Could not save salutation"); //if it went wrong rollback entity write and Outbox write - _transactionConnectionProvider.Rollback(); + tx.Rollback(); return base.Handle(@event); } diff --git a/samples/WebAPI_Dapper_Kafka/SalutationPorts/SalutationPorts.csproj b/samples/WebAPI_Dapper_Kafka/SalutationPorts/SalutationPorts.csproj index 4658d4de3b..9dbe4c27ca 100644 --- a/samples/WebAPI_Dapper_Kafka/SalutationPorts/SalutationPorts.csproj +++ b/samples/WebAPI_Dapper_Kafka/SalutationPorts/SalutationPorts.csproj @@ -11,7 +11,6 @@ - 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 1880b1e406..1ec6863a28 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,16 @@ - - - all runtime; build; native; contentfiles; analyzers; buildtransitive + + + ..\..\libs\Npgsql\netstandard2.0\Npgsql.dll + + + 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 1061b7456e..4cb662308d 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,15 @@ - - - all runtime; build; native; contentfiles; analyzers; buildtransitive + + + ..\..\libs\Npgsql\netstandard2.0\Npgsql.dll + + \ No newline at end of file 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 e882c38b30..688fc470f8 100644 --- a/src/Paramore.Brighter.PostgreSql.EntityFrameworkCore/Paramore.Brighter.PostgreSql.EntityFrameworkCore.csproj +++ b/src/Paramore.Brighter.PostgreSql.EntityFrameworkCore/Paramore.Brighter.PostgreSql.EntityFrameworkCore.csproj @@ -9,8 +9,6 @@ - - all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -36,5 +34,11 @@ + + + ..\..\libs\Npgsql\netstandard2.0\Npgsql.dll + + + diff --git a/src/Paramore.Brighter.PostgreSql/Paramore.Brighter.PostgreSql.csproj b/src/Paramore.Brighter.PostgreSql/Paramore.Brighter.PostgreSql.csproj index 38922eedd8..724fdbf0c0 100644 --- a/src/Paramore.Brighter.PostgreSql/Paramore.Brighter.PostgreSql.csproj +++ b/src/Paramore.Brighter.PostgreSql/Paramore.Brighter.PostgreSql.csproj @@ -8,9 +8,6 @@ - - - all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -21,4 +18,10 @@ + + + ..\..\libs\Npgsql\netstandard2.0\Npgsql.dll + + + 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 daa8f749d3..8655b606b2 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 + + + From db07b290bb705a7e7d39c4e4159d9381788a7d3b Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Sat, 17 Jun 2023 21:04:39 +0100 Subject: [PATCH 71/89] Move to Dapper; fix database access --- .../.idea/httpRequests/http-requests-log.http | 316 ++++++++---------- .../Handlers/DeletePersonHandlerAsync.cs | 19 +- .../FIndGreetingsForPersonHandlerAsync.cs | 8 +- .../GreetingsWeb/Database/SchemaCreation.cs | 14 +- .../GreetingsWeb/appsettings.Production.json | 4 +- .../Handlers/DeletePersonHandlerAsync.cs | 19 +- .../FIndGreetingsForPersonHandlerAsync.cs | 8 +- .../GreetingsWeb/Database/SchemaCreation.cs | 93 ++++-- .../Properties/launchSettings.json | 24 +- .../GreetingsWeb/appsettings.Production.json | 22 +- .../NpgsqlUnitOfWork.cs | 2 +- 11 files changed, 278 insertions(+), 251 deletions(-) diff --git a/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http b/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http index 084d7211cf..0e9531eaec 100644 --- a/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http +++ b/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http @@ -9,37 +9,31 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-17T124321.200.json +<> 2023-06-17T210322.200.json ### -POST http://localhost:5000/Greetings/Tyrion/new -Content-Type: application/json -Content-Length: 47 +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-06-17T124315.200.json +<> 2023-06-17T210320.200.json ### -POST http://localhost:5000/Greetings/Tyrion/new +POST http://localhost:5000/People/new Content-Type: application/json -Content-Length: 47 +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" } -<> 2023-06-17T123334.500.json +<> 2023-06-17T210317.200.json ### @@ -48,58 +42,62 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-06-17T123324.200.json +<> 2023-06-17T210315.404.json ### -GET http://localhost:5000/People/Tyrion +DELETE 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-06-17T120039.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 -<> 2023-06-17T115435.200.json +<> 2023-06-17T210308.200.json ### -POST http://localhost:5000/People/new +POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json -Content-Length: 23 +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" } -<> 2023-06-17T115417.200.json +<> 2023-06-17T210303.200.json ### -GET http://localhost:5000/Greetings/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 -<> 2023-06-17T112750.200.json +<> 2023-06-17T210258.200.json ### -GET http://localhost:5000/People/Tyrion +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 -<> 2023-06-17T112627.200.json +{ + "Name" : "Tyrion" +} + +<> 2023-06-17T210256.200.json ### @@ -108,7 +106,7 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-06-17T112146.500.json +<> 2023-06-17T210252.404.json ### @@ -117,7 +115,14 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-06-17T095354.404.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 + +<> 2023-06-17T200601.200.json ### @@ -132,41 +137,40 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-17T093904.200.json +<> 2023-06-17T200557.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 -<> 2023-06-17T093854.200.json +<> 2023-06-17T200554.200.json ### -DELETE 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 -### - -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-06-17T093837.200.json +<> 2023-06-17T200550.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 -<> 2023-06-17T093809.200.json +<> 2023-06-17T200547.200.json ### @@ -181,14 +185,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Name" : "Tyrion" } -<> 2023-06-17T093716.500.json - -### - -DELETE 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-06-17T200534.200.json ### @@ -197,67 +194,47 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-06-17T093700.200.json +<> 2023-06-17T200532.404.json ### -POST http://localhost:5000/People/new -Content-Type: application/json -Content-Length: 23 +DELETE 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 -{ - "Name" : "Tyrion" -} - -<> 2023-06-16T215201.500.json - ### -POST http://localhost:5000/People/new -Content-Type: application/json -Content-Length: 23 +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 -{ - "Name" : "Tyrion" -} - -<> 2023-06-16T203958.500.json +<> 2023-06-17T200450.200.json ### -POST http://localhost:5000/People/new +POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json -Content-Length: 23 +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" } -<> 2023-06-15T095915.500.json +<> 2023-06-17T200439.200.json ### -POST http://localhost:5000/People/new -Content-Type: application/json -Content-Length: 23 +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 -{ - "Name" : "Tyrion" -} - -<> 2023-06-15T091508.500.json +<> 2023-06-17T200431.200.json ### @@ -272,18 +249,16 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } +<> 2023-06-17T200419.200.json + ### -POST http://localhost:5000/People/new -Content-Type: application/json -Content-Length: 23 +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 -{ - "Name" : "Tyrion" -} +<> 2023-06-17T200256.200.json ### @@ -298,52 +273,40 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Name" : "Tyrion" } -<> 2023-06-14T183740.500.json +<> 2023-06-17T200250.200.json ### -POST http://localhost:5000/People/new -Content-Type: application/json -Content-Length: 23 +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 -{ - "Name" : "Tyrion" -} - -<> 2023-06-14T183618.500.json +<> 2023-06-17T200246.404.json ### -POST http://localhost:5000/People/new +POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json -Content-Length: 23 +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" } -<> 2023-06-14T183505.500.json +<> 2023-06-17T194351.500.json ### -POST http://localhost:5000/People/new -Content-Type: application/json -Content-Length: 23 +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 -{ - "Name" : "Tyrion" -} - -<> 2023-06-14T182436.500.json +<> 2023-06-17T194346.200.json ### @@ -358,7 +321,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Name" : "Tyrion" } -<> 2023-06-14T100715.500.json +<> 2023-06-17T194334.200.json ### @@ -367,100 +330,96 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-06-14T090138.500.json +<> 2023-06-17T194331.404.json ### -POST http://localhost:5000/People/new -Content-Type: application/json -Content-Length: 23 +DELETE 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 -{ - "Name" : "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 -<> 2023-06-13T205200.500.json +<> 2023-06-17T194318.200.json ### -POST http://localhost:5000/People/new +POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json -Content-Length: 23 +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" } -<> 2023-06-13T205002.500.json +<> 2023-06-17T194316.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 -<> 2023-06-13T204955.404.json +<> 2023-06-17T194313.200.json ### -POST http://localhost:5000/People/new +POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json -Content-Length: 23 +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" } -<> 2023-06-13T171434.500.json +<> 2023-06-17T194302.200.json ### -POST http://localhost:5000/People/new -Content-Type: application/json -Content-Length: 23 +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 -{ - "Name" : "Tyrion" -} - -<> 2023-06-13T171423.500.json +<> 2023-06-17T194226.200.json ### -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 -<> 2023-06-13T171320.404.json - -### +{ + "Greeting" : "I drink, and I know things" +} -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-06-17T194024.500.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 +<> 2023-06-17T194021.200.json + ### GET http://localhost:5000/People/Tyrion @@ -468,32 +427,40 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip +<> 2023-06-17T194016.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 -<> 2023-06-12T184025.500.json +<> 2023-06-17T191611.200.json ### -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 -<> 2023-06-12T165500.500.json +{ + "Greeting" : "I drink, and I know things" +} + +<> 2023-06-17T191609.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 -<> 2023-06-12T141625.500.json +<> 2023-06-17T191606.200.json ### @@ -508,31 +475,32 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Name" : "Tyrion" } -<> 2023-06-09T164917.500.json +<> 2023-06-17T191602.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 -<> 2023-06-09T164624.500.json +<> 2023-06-17T191556.200.json ### -POST http://localhost:5000/People/new -Content-Type: application/json -Content-Length: 23 +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 -{ - "Name" : "Tyrion" -} +<> 2023-06-17T191553.404.json -<> 2023-06-09T164620.500.json +### + +DELETE 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 ### @@ -541,52 +509,40 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-06-09T154849.500.json +<> 2023-06-17T191545.200.json ### -POST http://localhost:5000/People/new +POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json -Content-Length: 23 +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" } -<> 2023-06-09T154836.500.json +<> 2023-06-17T191309.200.json ### -POST http://localhost:5000/People/new -Content-Type: application/json -Content-Length: 23 +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 -{ - "Name" : "Tyrion" -} - -<> 2023-06-09T154833.500.json +<> 2023-06-17T191306.200.json ### -POST http://localhost:5000/People/new -Content-Type: application/json -Content-Length: 23 +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 -{ - "Name" : "Tyrion" -} - -<> 2023-06-09T154824.500.json +<> 2023-06-17T191304.200.json ### diff --git a/samples/WebAPI_Dapper/GreetingsPorts/Handlers/DeletePersonHandlerAsync.cs b/samples/WebAPI_Dapper/GreetingsPorts/Handlers/DeletePersonHandlerAsync.cs index 16b71a1ad0..fe2731a534 100644 --- a/samples/WebAPI_Dapper/GreetingsPorts/Handlers/DeletePersonHandlerAsync.cs +++ b/samples/WebAPI_Dapper/GreetingsPorts/Handlers/DeletePersonHandlerAsync.cs @@ -5,6 +5,7 @@ using Dapper; using GreetingsEntities; using GreetingsPorts.Requests; +using Microsoft.Extensions.Logging; using Paramore.Brighter; using Paramore.Brighter.Logging.Attributes; using Paramore.Brighter.Policies.Attributes; @@ -13,11 +14,13 @@ namespace GreetingsPorts.Handlers { public class DeletePersonHandlerAsync : RequestHandlerAsync { - private readonly IAmARelationalDbConnectionProvider _relationalDbConnectionProvider; + private readonly IAmARelationalDbConnectionProvider _relationalDbConnectionProvider; + private readonly ILogger _logger; - public DeletePersonHandlerAsync(IAmARelationalDbConnectionProvider relationalDbConnectionProvider) + public DeletePersonHandlerAsync(IAmARelationalDbConnectionProvider relationalDbConnectionProvider, ILogger logger) { _relationalDbConnectionProvider = relationalDbConnectionProvider; + _logger = logger; } [RequestLoggingAsync(0, HandlerTiming.Before)] @@ -30,22 +33,28 @@ public override async Task HandleAsync(DeletePerson deletePerson, { var people = await connection.QueryAsync( "select * from Person where name = @name", - new {name = deletePerson.Name} + new {name = deletePerson.Name}, + tx ); var person = people.SingleOrDefault(); if (person != null) { await connection.ExecuteAsync( - "delete from Greeting where PersonId = @PersonId", + "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); 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); diff --git a/samples/WebAPI_Dapper/GreetingsPorts/Handlers/FIndGreetingsForPersonHandlerAsync.cs b/samples/WebAPI_Dapper/GreetingsPorts/Handlers/FIndGreetingsForPersonHandlerAsync.cs index b274354018..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; @@ -39,6 +40,11 @@ from Person p 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 => { diff --git a/samples/WebAPI_Dapper/GreetingsWeb/Database/SchemaCreation.cs b/samples/WebAPI_Dapper/GreetingsWeb/Database/SchemaCreation.cs index b28d6c6fc6..870b8a869f 100644 --- a/samples/WebAPI_Dapper/GreetingsWeb/Database/SchemaCreation.cs +++ b/samples/WebAPI_Dapper/GreetingsWeb/Database/SchemaCreation.cs @@ -81,10 +81,16 @@ private static void CreateDatabaseIfNotExists(DatabaseType databaseType, DbConne //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(); - if (databaseType != DatabaseType.Postgres) - command.CommandText = "CREATE DATABASE IF NOT EXISTS Greetings"; - else - command.CommandText = "CREATE DATABASE Greetings"; + + 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 Outbox for unknown Db type") + }; try { diff --git a/samples/WebAPI_Dapper/GreetingsWeb/appsettings.Production.json b/samples/WebAPI_Dapper/GreetingsWeb/appsettings.Production.json index e26000a3d4..43ad056184 100644 --- a/samples/WebAPI_Dapper/GreetingsWeb/appsettings.Production.json +++ b/samples/WebAPI_Dapper/GreetingsWeb/appsettings.Production.json @@ -11,7 +11,7 @@ "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,1433;Username=sa;Password=Password123!;Database=Greetings", - "MsSqlDb": "Server=localhost,1433;Username=sa;Password=Password123!" + "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_Kafka/GreetingsPorts/Handlers/DeletePersonHandlerAsync.cs b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/DeletePersonHandlerAsync.cs index 16b71a1ad0..fe2731a534 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/DeletePersonHandlerAsync.cs +++ b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/DeletePersonHandlerAsync.cs @@ -5,6 +5,7 @@ using Dapper; using GreetingsEntities; using GreetingsPorts.Requests; +using Microsoft.Extensions.Logging; using Paramore.Brighter; using Paramore.Brighter.Logging.Attributes; using Paramore.Brighter.Policies.Attributes; @@ -13,11 +14,13 @@ namespace GreetingsPorts.Handlers { public class DeletePersonHandlerAsync : RequestHandlerAsync { - private readonly IAmARelationalDbConnectionProvider _relationalDbConnectionProvider; + private readonly IAmARelationalDbConnectionProvider _relationalDbConnectionProvider; + private readonly ILogger _logger; - public DeletePersonHandlerAsync(IAmARelationalDbConnectionProvider relationalDbConnectionProvider) + public DeletePersonHandlerAsync(IAmARelationalDbConnectionProvider relationalDbConnectionProvider, ILogger logger) { _relationalDbConnectionProvider = relationalDbConnectionProvider; + _logger = logger; } [RequestLoggingAsync(0, HandlerTiming.Before)] @@ -30,22 +33,28 @@ public override async Task HandleAsync(DeletePerson deletePerson, { var people = await connection.QueryAsync( "select * from Person where name = @name", - new {name = deletePerson.Name} + new {name = deletePerson.Name}, + tx ); var person = people.SingleOrDefault(); if (person != null) { await connection.ExecuteAsync( - "delete from Greeting where PersonId = @PersonId", + "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); 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); diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/FIndGreetingsForPersonHandlerAsync.cs b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/FIndGreetingsForPersonHandlerAsync.cs index b274354018..45752cff49 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/FIndGreetingsForPersonHandlerAsync.cs +++ b/samples/WebAPI_Dapper_Kafka/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; @@ -39,6 +40,11 @@ from Person p 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 => { diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/SchemaCreation.cs b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/SchemaCreation.cs index c979c115bc..870b8a869f 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/SchemaCreation.cs +++ b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/SchemaCreation.cs @@ -36,7 +36,7 @@ public static IHost CheckDbIsUp(this IHost webHost) if (env.IsDevelopment()) return webHost; WaitToConnect(dbType, connectionString); - CreateDatabaseIfNotExists(GetDbConnection(dbType, connectionString)); + CreateDatabaseIfNotExists(dbType, GetDbConnection(dbType, connectionString)); return webHost; } @@ -64,40 +64,72 @@ public static IHost MigrateDatabase(this IHost webHost) return webHost; } - public static IHost CreateOutbox(this IHost webHost, bool hasBinaryPayload = false) + 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(); + using var scope = webHost.Services.CreateScope(); + var services = scope.ServiceProvider; + var env = services.GetService(); + var config = services.GetService(); - CreateOutbox(config, env, hasBinaryPayload); - } + 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 Outbox 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, bool hasBinaryPayload) + private static void CreateOutbox(IConfiguration config, IWebHostEnvironment env) { try { var connectionString = DbConnectionString(config, env); if (env.IsDevelopment()) - CreateOutboxDevelopment(connectionString, hasBinaryPayload); + CreateOutboxDevelopment(connectionString); else - CreateOutboxProduction(GetDatabaseType(config), connectionString, hasBinaryPayload); + CreateOutboxProduction(GetDatabaseType(config), connectionString); + } + 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) { @@ -107,34 +139,33 @@ private static void CreateOutbox(IConfiguration config, IWebHostEnvironment env, } } - private static void CreateOutboxDevelopment(string connectionString, bool hasBinaryPayload) + private static void CreateOutboxDevelopment(string connectionString) { - CreateOutboxSqlite(connectionString, hasBinaryPayload); + CreateOutboxSqlite(connectionString); } - private static void CreateOutboxProduction(DatabaseType databaseType, string connectionString, - bool hasBinaryPayload) + private static void CreateOutboxProduction(DatabaseType databaseType, string connectionString) { switch (databaseType) { case DatabaseType.MySql: - CreateOutboxMySql(connectionString, hasBinaryPayload); + CreateOutboxMySql(connectionString); break; case DatabaseType.MsSql: - CreateOutboxMsSql(connectionString, hasBinaryPayload); + CreateOutboxMsSql(connectionString); break; case DatabaseType.Postgres: - CreateOutboxPostgres(connectionString, hasBinaryPayload); + CreateOutboxPostgres(connectionString); break; case DatabaseType.Sqlite: - CreateOutboxSqlite(connectionString, hasBinaryPayload); + CreateOutboxSqlite(connectionString); break; default: throw new InvalidOperationException("Could not create instance of Outbox for unknown Db type"); } } - private static void CreateOutboxMsSql(string connectionString, bool hasBinaryPayload) + private static void CreateOutboxMsSql(string connectionString) { using var sqlConnection = new SqlConnection(connectionString); sqlConnection.Open(); @@ -146,12 +177,12 @@ private static void CreateOutboxMsSql(string connectionString, bool hasBinaryPay if (exists) return; using var command = sqlConnection.CreateCommand(); - command.CommandText = SqlOutboxBuilder.GetDDL(OUTBOX_TABLE_NAME, hasBinaryPayload); + command.CommandText = SqlOutboxBuilder.GetDDL(OUTBOX_TABLE_NAME); command.ExecuteScalar(); } - private static void CreateOutboxMySql(string connectionString, bool hasBinaryPayload) + private static void CreateOutboxMySql(string connectionString) { using var sqlConnection = new MySqlConnection(connectionString); sqlConnection.Open(); @@ -163,11 +194,11 @@ private static void CreateOutboxMySql(string connectionString, bool hasBinaryPay if (exists) return; using var command = sqlConnection.CreateCommand(); - command.CommandText = MySqlOutboxBuilder.GetDDL(OUTBOX_TABLE_NAME, hasBinaryPayload); + command.CommandText = MySqlOutboxBuilder.GetDDL(OUTBOX_TABLE_NAME); command.ExecuteScalar(); } - private static void CreateOutboxPostgres(string connectionString, bool hasBinaryPayload) + private static void CreateOutboxPostgres(string connectionString) { using var sqlConnection = new NpgsqlConnection(connectionString); sqlConnection.Open(); @@ -179,11 +210,11 @@ private static void CreateOutboxPostgres(string connectionString, bool hasBinary if (exists) return; using var command = sqlConnection.CreateCommand(); - command.CommandText = PostgreSqlOutboxBulder.GetDDL(OUTBOX_TABLE_NAME, hasBinaryPayload); + command.CommandText = PostgreSqlOutboxBulder.GetDDL(OUTBOX_TABLE_NAME); command.ExecuteScalar(); } - private static void CreateOutboxSqlite(string connectionString, bool hasBinaryPayload) + private static void CreateOutboxSqlite(string connectionString) { using var sqlConnection = new SqliteConnection(connectionString); sqlConnection.Open(); @@ -195,7 +226,7 @@ private static void CreateOutboxSqlite(string connectionString, bool hasBinaryPa if (reader.HasRows) return; using var command = sqlConnection.CreateCommand(); - command.CommandText = SqliteOutboxBuilder.GetDDL(OUTBOX_TABLE_NAME, hasBinaryPayload); + command.CommandText = SqliteOutboxBuilder.GetDDL(OUTBOX_TABLE_NAME); command.ExecuteScalar(); } diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Properties/launchSettings.json b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Properties/launchSettings.json index 57c58ccaa2..6ddca8d915 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Properties/launchSettings.json +++ b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Properties/launchSettings.json @@ -9,7 +9,7 @@ } }, "profiles": { - "Development": { + "Development": { "commandName": "Project", "dotnetRunMessages": "true", "launchBrowser": true, @@ -40,17 +40,17 @@ "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Production", "BRIGHTER_GREETINGS_DATABASE": "PostgresSQL" - }, - "ProductionMsSql": { - "commandName": "Project", - "dotnetRunMessages": "true", - "launchBrowser": true, - "launchUrl": "swagger", - "applicationUrl": "https://localhost:5001;http://localhost:5000", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Production", - "BRIGHTER_GREETINGS_DATABASE": "MySQL" - } + } + }, + "ProductionMsSql": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Production", + "BRIGHTER_GREETINGS_DATABASE": "MsSQL" } } } diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/appsettings.Production.json b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/appsettings.Production.json index c02f13f346..5ad8dd3201 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/appsettings.Production.json +++ b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/appsettings.Production.json @@ -1,13 +1,17 @@ { - "Logging": { - "LogLevel": { - "Default": "Warning", - "System": "Warning", - "Microsoft": "Warning" - } - }, + "Logging": { + "LogLevel": { + "Default": "Warning", + "System": "Warning", + "Microsoft": "Warning" + } + }, "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/src/Paramore.Brighter.PostgreSql/NpgsqlUnitOfWork.cs b/src/Paramore.Brighter.PostgreSql/NpgsqlUnitOfWork.cs index 55f9e56806..47b930bd6d 100644 --- a/src/Paramore.Brighter.PostgreSql/NpgsqlUnitOfWork.cs +++ b/src/Paramore.Brighter.PostgreSql/NpgsqlUnitOfWork.cs @@ -24,7 +24,7 @@ public class NpgsqlUnitOfWork : RelationalDbTransactionProvider /// globally public NpgsqlUnitOfWork( IAmARelationalDatabaseConfiguration configuration, - NpgsqlDataSource dataSource) + NpgsqlDataSource dataSource = null) { if (string.IsNullOrWhiteSpace(configuration?.ConnectionString)) throw new ArgumentNullException(nameof(configuration.ConnectionString)); From 28ea16d708ea08e560770af535d3e845f61658d9 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Mon, 19 Jun 2023 20:36:51 +0100 Subject: [PATCH 72/89] Fixes to salutations for supporting all databases --- .../.idea/httpRequests/http-requests-log.http | 238 ++++++++++-------- .../GreetingsWeb/Database/OutboxExtensions.cs | 12 - samples/WebAPI_Dapper/GreetingsWeb/Startup.cs | 3 +- .../Database/OutboxExtensions.cs | 67 +++++ .../Database/SchemaCreation.cs | 2 +- .../SalutationAnalytics/Program.cs | 14 ++ .../Properties/launchSettings.json | 3 +- .../Handlers/GreetingMadeHandler.cs | 12 +- 8 files changed, 224 insertions(+), 127 deletions(-) create mode 100644 samples/WebAPI_Dapper/SalutationAnalytics/Database/OutboxExtensions.cs diff --git a/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http b/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http index 0e9531eaec..700324a7af 100644 --- a/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http +++ b/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http @@ -9,6 +9,138 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } +<> 2023-06-19T203414.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-06-19T203413.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-06-19T203412.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-06-19T203410.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-06-19T203409.200.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 + +<> 2023-06-19T203406.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-06-19T203359.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-06-19T191748.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-06-19T191743.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-06-19T191738.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-06-17T210322.200.json ### @@ -440,109 +572,3 @@ Accept-Encoding: br,deflate,gzip,x-gzip ### -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-06-17T191609.200.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 - -<> 2023-06-17T191606.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" -} - -<> 2023-06-17T191602.200.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 - -<> 2023-06-17T191556.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-06-17T191553.404.json - -### - -DELETE 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 - -<> 2023-06-17T191545.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-06-17T191309.200.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 - -<> 2023-06-17T191306.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-06-17T191304.200.json - -### - diff --git a/samples/WebAPI_Dapper/GreetingsWeb/Database/OutboxExtensions.cs b/samples/WebAPI_Dapper/GreetingsWeb/Database/OutboxExtensions.cs index be007eeaca..7c003b1a73 100644 --- a/samples/WebAPI_Dapper/GreetingsWeb/Database/OutboxExtensions.cs +++ b/samples/WebAPI_Dapper/GreetingsWeb/Database/OutboxExtensions.cs @@ -52,18 +52,6 @@ private static (IAmAnOutbox, Type, Type) MakePostgresSqlOutbox( RelationalDatabaseConfiguration configuration, IServiceCollection services) { - //if we want to use our IAmARelationalDatabaseConnectionProvider or IAmAABoxTransactionProvider - //from the Outbox in our handlers, then we need to construct an NpgsqlDataSource and register the composite types - //then pass that to the Outbox constructor so that connections created by the Outbox will be aware of - //those composite types - //var dataSourceBuilder = new NpgsqlDataSourceBuilder(configuration.ConnectionString); - //dataSourceBuilder.DefaultNameTranslator = new NpgsqlNullNameTranslator(); - //dataSourceBuilder.MapComposite(); - //dataSourceBuilder.MapComposite(); - //var dataSource = dataSourceBuilder.Build(); - - //services.AddSingleton(dataSource); - return (new PostgreSqlOutbox(configuration), typeof(NpgsqConnectionProvider), typeof(NpgsqlUnitOfWork)); } diff --git a/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs b/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs index 98e58234b9..6c40210d81 100644 --- a/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs +++ b/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs @@ -15,6 +15,7 @@ using OpenTelemetry.Trace; using Paramore.Brighter; using Paramore.Brighter.Extensions.DependencyInjection; +using Paramore.Brighter.Extensions.Hosting; using Paramore.Brighter.MessagingGateway.RMQ; using Paramore.Darker.AspNetCore; using Paramore.Darker.Policies; @@ -201,12 +202,10 @@ private void ConfigureBrighter(IServiceCollection services) configure.TransactionProvider = makeOutbox.transactionProvider; configure.ConnectionProvider = makeOutbox.connectionProvider; }) - /* .UseOutboxSweeper(options => { options.TimerInterval = 5; options.MinimumMessageAge = 5000; }) - */ .AutoFromAssemblies(typeof(AddPersonHandlerAsync).Assembly); } diff --git a/samples/WebAPI_Dapper/SalutationAnalytics/Database/OutboxExtensions.cs b/samples/WebAPI_Dapper/SalutationAnalytics/Database/OutboxExtensions.cs new file mode 100644 index 0000000000..688b8ae77c --- /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(NpgsqConnectionProvider), typeof(NpgsqlUnitOfWork)); + } + + private static (IAmAnOutbox, Type, Type) MakeMsSqlOutbox(RelationalDatabaseConfiguration configuration) + { + return new(new MsSqlOutbox(configuration), typeof(MsSqlAuthConnectionProvider), 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 dc715a67d1..36178a8283 100644 --- a/samples/WebAPI_Dapper/SalutationAnalytics/Database/SchemaCreation.cs +++ b/samples/WebAPI_Dapper/SalutationAnalytics/Database/SchemaCreation.cs @@ -371,7 +371,7 @@ private static (DatabaseType, string) DbServerConnectionString(IConfiguration co private static string GetDevConnectionString() { - return "Filename=Greetings.db;Cache=Shared"; + return "Filename=Salutations.db;Cache=Shared"; } private static DbConnection GetDbConnection(DatabaseType databaseType, string connectionString) diff --git a/samples/WebAPI_Dapper/SalutationAnalytics/Program.cs b/samples/WebAPI_Dapper/SalutationAnalytics/Program.cs index 1f5dbe9cb1..662d8ec884 100644 --- a/samples/WebAPI_Dapper/SalutationAnalytics/Program.cs +++ b/samples/WebAPI_Dapper/SalutationAnalytics/Program.cs @@ -24,6 +24,8 @@ namespace SalutationAnalytics { class Program { + private const string OUTBOX_TABLE_NAME = "Outbox"; + public static async Task Main(string[] args) { var host = CreateHostBuilder(args).Build(); @@ -97,6 +99,15 @@ private static void ConfigureBrighter(HostBuilderContext hostContext, IServiceCo } } ).Create(); + + var outboxConfiguration = new RelationalDatabaseConfiguration( + DbConnectionString(hostContext), + outBoxTableName:OUTBOX_TABLE_NAME + ); + services.AddSingleton(outboxConfiguration); + + (IAmAnOutbox outbox, Type connectionProvider, Type transactionProvider) makeOutbox = + OutboxExtensions.MakeOutbox(hostContext, GetDatabaseType(hostContext), outboxConfiguration, services); services.AddServiceActivator(options => { @@ -123,6 +134,9 @@ private static void ConfigureBrighter(HostBuilderContext hostContext, IServiceCo .UseExternalBus((config) => { config.ProducerRegistry = producerRegistry; + config.Outbox = makeOutbox.outbox; + config.ConnectionProvider = makeOutbox.connectionProvider; + config.TransactionProvider = makeOutbox.transactionProvider; }) .AutoFromAssemblies(); diff --git a/samples/WebAPI_Dapper/SalutationAnalytics/Properties/launchSettings.json b/samples/WebAPI_Dapper/SalutationAnalytics/Properties/launchSettings.json index 442c3ce437..7e2b9ed678 100644 --- a/samples/WebAPI_Dapper/SalutationAnalytics/Properties/launchSettings.json +++ b/samples/WebAPI_Dapper/SalutationAnalytics/Properties/launchSettings.json @@ -5,7 +5,7 @@ "commandName": "Project", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", - "BRIGHTER_GREETINGS_DATABASE" : "Sqlite" + "BRIGHTER_GREETINGS_DATABASE": "Sqlite" } }, "ProductionMySql": { @@ -40,5 +40,6 @@ "ASPNETCORE_ENVIRONMENT": "Production", "BRIGHTER_GREETINGS_DATABASE": "MsSQL" } + } } } diff --git a/samples/WebAPI_Dapper/SalutationPorts/Handlers/GreetingMadeHandler.cs b/samples/WebAPI_Dapper/SalutationPorts/Handlers/GreetingMadeHandler.cs index eb4a0ff3b6..fdf6434dc5 100644 --- a/samples/WebAPI_Dapper/SalutationPorts/Handlers/GreetingMadeHandler.cs +++ b/samples/WebAPI_Dapper/SalutationPorts/Handlers/GreetingMadeHandler.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Data.Common; using Dapper; using Microsoft.Extensions.Logging; using Paramore.Brighter; @@ -12,11 +13,11 @@ namespace SalutationPorts.Handlers { public class GreetingMadeHandler : RequestHandler { - private readonly IAmATransactionConnectionProvider _transactionConnectionProvider; + private readonly IAmABoxTransactionProvider _transactionConnectionProvider; private readonly IAmACommandProcessor _postBox; private readonly ILogger _logger; - public GreetingMadeHandler(IAmATransactionConnectionProvider transactionConnectionProvider, IAmACommandProcessor postBox, ILogger logger) + public GreetingMadeHandler(IAmABoxTransactionProvider transactionConnectionProvider, IAmACommandProcessor postBox, ILogger logger) { _transactionConnectionProvider = transactionConnectionProvider; _postBox = postBox; @@ -31,11 +32,12 @@ public override GreetingMade Handle(GreetingMade @event) var posts = new List(); var tx = _transactionConnectionProvider.GetTransaction(); + var conn = tx.Connection; try { var salutation = new Salutation(@event.Greeting); - _transactionConnectionProvider.GetConnection().Execute( + conn.Execute( "insert into Salutation (greeting) values (@greeting)", new {greeting = salutation.Greeting}, tx); @@ -44,14 +46,14 @@ public override GreetingMade Handle(GreetingMade @event) new SalutationReceived(DateTimeOffset.Now), _transactionConnectionProvider)); - tx.Commit(); + _transactionConnectionProvider.Commit(); } catch (Exception e) { _logger.LogError(e, "Could not save salutation"); //if it went wrong rollback entity write and Outbox write - tx.Rollback(); + _transactionConnectionProvider.Rollback(); return base.Handle(@event); } From 3f6bc3f7dc0549dc84467bafe8c31fb139bd91ff Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Sat, 24 Jun 2023 09:19:03 -0500 Subject: [PATCH 73/89] Fix naming issues for the Salutations project --- .../GreetingsWeb/Database/SchemaCreation.cs | 1 - .../Database/SchemaCreation.cs | 6 ++--- .../SalutationAnalytics/Program.cs | 25 ++++++++++++++++--- 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/samples/WebAPI_Dapper/GreetingsWeb/Database/SchemaCreation.cs b/samples/WebAPI_Dapper/GreetingsWeb/Database/SchemaCreation.cs index 924fcaea4a..5560757ef5 100644 --- a/samples/WebAPI_Dapper/GreetingsWeb/Database/SchemaCreation.cs +++ b/samples/WebAPI_Dapper/GreetingsWeb/Database/SchemaCreation.cs @@ -110,7 +110,6 @@ private static void CreateDatabaseIfNotExists(DatabaseType databaseType, DbConne } } - private static void CreateOutbox(IConfiguration config, IWebHostEnvironment env) { try diff --git a/samples/WebAPI_Dapper/SalutationAnalytics/Database/SchemaCreation.cs b/samples/WebAPI_Dapper/SalutationAnalytics/Database/SchemaCreation.cs index 36178a8283..62a89886cb 100644 --- a/samples/WebAPI_Dapper/SalutationAnalytics/Database/SchemaCreation.cs +++ b/samples/WebAPI_Dapper/SalutationAnalytics/Database/SchemaCreation.cs @@ -402,9 +402,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.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") }; diff --git a/samples/WebAPI_Dapper/SalutationAnalytics/Program.cs b/samples/WebAPI_Dapper/SalutationAnalytics/Program.cs index 662d8ec884..07e2c8d602 100644 --- a/samples/WebAPI_Dapper/SalutationAnalytics/Program.cs +++ b/samples/WebAPI_Dapper/SalutationAnalytics/Program.cs @@ -265,11 +265,11 @@ 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 @@ -287,5 +287,22 @@ 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"; + } + + } } From 5bee4d2742f18b6d062fdb4b31fd4456b32af2b1 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Sat, 24 Jun 2023 09:55:07 -0500 Subject: [PATCH 74/89] safewt dance --- .../.idea/httpRequests/http-requests-log.http | 234 +++++++++++------- .../GreetingsWeb/Database/OutboxExtensions.cs | 4 +- .../Database/OutboxExtensions.cs | 4 +- .../Database/SchemaCreation.cs | 4 +- .../SalutationAnalytics/Program.cs | 19 ++ .../SalutationEntities/Salutation.cs | 6 +- .../202205161812_SqliteMigrations.cs | 2 +- .../GreetingsWeb/Database/OutboxExtensions.cs | 4 +- .../Extensions/BrighterExtensions.cs | 2 +- .../MsSqlInbox.cs | 2 +- .../MsSqlMessageConsumer.cs | 2 +- .../MsSqlMessageProducer.cs | 2 +- ...Provider.cs => MsSqlConnectionProvider.cs} | 4 +- .../MsSqlOutbox.cs | 2 +- .../PostgreSqlOutbox.cs | 2 +- ...der.cs => PostgreSqlConnectionProvider.cs} | 4 +- ...lUnitOfWork.cs => PostgreSqlUnitOfWork.cs} | 4 +- .../MsSqlTestHelper.cs | 4 +- 18 files changed, 188 insertions(+), 117 deletions(-) rename src/Paramore.Brighter.MsSql/{MsSqlAuthConnectionProvider.cs => MsSqlConnectionProvider.cs} (91%) rename src/Paramore.Brighter.PostgreSql/{NpgsqConnectionProvider.cs => PostgreSqlConnectionProvider.cs} (96%) rename src/Paramore.Brighter.PostgreSql/{NpgsqlUnitOfWork.cs => PostgreSqlUnitOfWork.cs} (97%) diff --git a/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http b/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http index 700324a7af..fe12191b06 100644 --- a/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http +++ b/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http @@ -9,7 +9,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-19T203414.200.json +<> 2023-06-24T093617.200.json ### @@ -24,7 +24,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-19T203413.200.json +<> 2023-06-24T093616.200.json ### @@ -39,7 +39,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-19T203412.200.json +<> 2023-06-24T093614.200.json ### @@ -54,7 +54,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-19T203410.200.json +<> 2023-06-24T093611.500.json ### @@ -69,16 +69,22 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-19T203409.200.json +<> 2023-06-24T093606.200.json ### -GET http://localhost:5000/Greetings/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 -<> 2023-06-19T203406.200.json +{ + "Greeting" : "I drink, and I know things" +} + +<> 2023-06-24T093605.200.json ### @@ -87,7 +93,7 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-06-19T203359.200.json +<> 2023-06-24T093600.200.json ### @@ -102,7 +108,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-19T191748.200.json +<> 2023-06-24T091635-1.200.json ### @@ -117,16 +123,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-19T191743.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-06-19T191738.200.json +<> 2023-06-24T091635.200.json ### @@ -141,56 +138,70 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-17T210322.200.json +<> 2023-06-24T091634.200.json ### -GET http://localhost:5000/Greetings/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 -<> 2023-06-17T210320.200.json +{ + "Greeting" : "I drink, and I know things" +} + +<> 2023-06-24T091632.200.json ### -POST http://localhost:5000/People/new +POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json -Content-Length: 23 +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" } -<> 2023-06-17T210317.200.json +<> 2023-06-24T091631.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 -<> 2023-06-17T210315.404.json +<> 2023-06-24T091625.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 +<> 2023-06-24T091620.200.json + ### -GET http://localhost:5000/Greetings/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 -<> 2023-06-17T210308.200.json +{ + "Greeting" : "I drink, and I know things" +} + +<> 2023-06-24T091307.500.json ### @@ -205,56 +216,61 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-17T210303.200.json +<> 2023-06-24T091303.200.json ### -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 -<> 2023-06-17T210258.200.json +{ + "Greeting" : "I drink, and I know things" +} + +<> 2023-06-24T091302.200.json ### -POST http://localhost:5000/People/new +POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json -Content-Length: 23 +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" } -<> 2023-06-17T210256.200.json +<> 2023-06-24T091301.200.json ### -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 -<> 2023-06-17T210252.404.json - -### +{ + "Greeting" : "I drink, and I know things" +} -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-06-24T091259.200.json ### -GET http://localhost:5000/Greetings/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 -<> 2023-06-17T200601.200.json +<> 2023-06-24T091255.200.json ### @@ -269,16 +285,22 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-17T200557.200.json +<> 2023-06-19T203414.200.json ### -GET http://localhost:5000/Greetings/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 -<> 2023-06-17T200554.200.json +{ + "Greeting" : "I drink, and I know things" +} + +<> 2023-06-19T203413.200.json ### @@ -293,56 +315,70 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-17T200550.200.json +<> 2023-06-19T203412.200.json ### -GET http://localhost:5000/Greetings/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 -<> 2023-06-17T200547.200.json +{ + "Greeting" : "I drink, and I know things" +} + +<> 2023-06-19T203410.200.json ### -POST http://localhost:5000/People/new +POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json -Content-Length: 23 +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" } -<> 2023-06-17T200534.200.json +<> 2023-06-19T203409.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 -<> 2023-06-17T200532.404.json +<> 2023-06-19T203406.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 +<> 2023-06-19T203359.200.json + ### -GET http://localhost:5000/Greetings/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 -<> 2023-06-17T200450.200.json +{ + "Greeting" : "I drink, and I know things" +} + +<> 2023-06-19T191748.200.json ### @@ -357,16 +393,16 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-17T200439.200.json +<> 2023-06-19T191743.200.json ### -GET http://localhost:5000/Greetings/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 -<> 2023-06-17T200431.200.json +<> 2023-06-19T191738.200.json ### @@ -381,16 +417,16 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-17T200419.200.json +<> 2023-06-17T210322.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 -<> 2023-06-17T200256.200.json +<> 2023-06-17T210320.200.json ### @@ -405,7 +441,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Name" : "Tyrion" } -<> 2023-06-17T200250.200.json +<> 2023-06-17T210317.200.json ### @@ -414,7 +450,23 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-06-17T200246.404.json +<> 2023-06-17T210315.404.json + +### + +DELETE 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/Greetings/Tyrion +Connection: Keep-Alive +User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) +Accept-Encoding: br,deflate,gzip,x-gzip + +<> 2023-06-17T210308.200.json ### @@ -429,16 +481,16 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-17T194351.500.json +<> 2023-06-17T210303.200.json ### -GET http://localhost:5000/Greetings/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 -<> 2023-06-17T194346.200.json +<> 2023-06-17T210258.200.json ### @@ -453,7 +505,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Name" : "Tyrion" } -<> 2023-06-17T194334.200.json +<> 2023-06-17T210256.200.json ### @@ -462,11 +514,11 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-06-17T194331.404.json +<> 2023-06-17T210252.404.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 @@ -478,7 +530,7 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-06-17T194318.200.json +<> 2023-06-17T200601.200.json ### @@ -493,7 +545,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-17T194316.200.json +<> 2023-06-17T200557.200.json ### @@ -502,7 +554,7 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-06-17T194313.200.json +<> 2023-06-17T200554.200.json ### @@ -517,7 +569,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-17T194302.200.json +<> 2023-06-17T200550.200.json ### @@ -526,41 +578,39 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-06-17T194226.200.json +<> 2023-06-17T200547.200.json ### -POST http://localhost:5000/Greetings/Tyrion/new +POST http://localhost:5000/People/new Content-Type: application/json -Content-Length: 47 +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" } -<> 2023-06-17T194024.500.json +<> 2023-06-17T200534.200.json ### -GET http://localhost:5000/Greetings/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 -<> 2023-06-17T194021.200.json +<> 2023-06-17T200532.404.json ### -GET http://localhost:5000/People/Tyrion +DELETE 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-06-17T194016.200.json - ### GET http://localhost:5000/Greetings/Tyrion @@ -568,7 +618,7 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-06-17T191611.200.json +<> 2023-06-17T200450.200.json ### diff --git a/samples/WebAPI_Dapper/GreetingsWeb/Database/OutboxExtensions.cs b/samples/WebAPI_Dapper/GreetingsWeb/Database/OutboxExtensions.cs index 7c003b1a73..ddaae874fe 100644 --- a/samples/WebAPI_Dapper/GreetingsWeb/Database/OutboxExtensions.cs +++ b/samples/WebAPI_Dapper/GreetingsWeb/Database/OutboxExtensions.cs @@ -52,12 +52,12 @@ private static (IAmAnOutbox, Type, Type) MakePostgresSqlOutbox( RelationalDatabaseConfiguration configuration, IServiceCollection services) { - return (new PostgreSqlOutbox(configuration), typeof(NpgsqConnectionProvider), typeof(NpgsqlUnitOfWork)); + return (new PostgreSqlOutbox(configuration), typeof(PostgreSqlConnectionProvider), typeof(PostgreSqlUnitOfWork)); } private static (IAmAnOutbox, Type, Type) MakeMsSqlOutbox(RelationalDatabaseConfiguration configuration) { - return new(new MsSqlOutbox(configuration), typeof(MsSqlAuthConnectionProvider), typeof(MsSqlUnitOfWork)); + return new(new MsSqlOutbox(configuration), typeof(MsSqlConnectionProvider), typeof(MsSqlUnitOfWork)); } private static (IAmAnOutbox, Type, Type) MakeMySqlOutbox(RelationalDatabaseConfiguration configuration) diff --git a/samples/WebAPI_Dapper/SalutationAnalytics/Database/OutboxExtensions.cs b/samples/WebAPI_Dapper/SalutationAnalytics/Database/OutboxExtensions.cs index 688b8ae77c..428c3bfda7 100644 --- a/samples/WebAPI_Dapper/SalutationAnalytics/Database/OutboxExtensions.cs +++ b/samples/WebAPI_Dapper/SalutationAnalytics/Database/OutboxExtensions.cs @@ -46,12 +46,12 @@ private static (IAmAnOutbox, Type, Type) MakePostgresSqlOutbox( RelationalDatabaseConfiguration configuration, IServiceCollection services) { - return (new PostgreSqlOutbox(configuration), typeof(NpgsqConnectionProvider), typeof(NpgsqlUnitOfWork)); + return (new PostgreSqlOutbox(configuration), typeof(PostgreSqlConnectionProvider), typeof(PostgreSqlUnitOfWork)); } private static (IAmAnOutbox, Type, Type) MakeMsSqlOutbox(RelationalDatabaseConfiguration configuration) { - return new(new MsSqlOutbox(configuration), typeof(MsSqlAuthConnectionProvider), typeof(MsSqlUnitOfWork)); + return new(new MsSqlOutbox(configuration), typeof(MsSqlConnectionProvider), typeof(MsSqlUnitOfWork)); } private static (IAmAnOutbox, Type, Type) MakeMySqlOutbox(RelationalDatabaseConfiguration configuration) diff --git a/samples/WebAPI_Dapper/SalutationAnalytics/Database/SchemaCreation.cs b/samples/WebAPI_Dapper/SalutationAnalytics/Database/SchemaCreation.cs index 62a89886cb..3d3d3f68e0 100644 --- a/samples/WebAPI_Dapper/SalutationAnalytics/Database/SchemaCreation.cs +++ b/samples/WebAPI_Dapper/SalutationAnalytics/Database/SchemaCreation.cs @@ -207,7 +207,7 @@ private static void CreateInboxMySql(string connectionString) private static void CreateInboxMsSql(string connectionString) { - using var sqlConnection = new SqliteConnection(connectionString); + using var sqlConnection = new SqlConnection(connectionString); sqlConnection.Open(); using var exists = sqlConnection.CreateCommand(); @@ -223,7 +223,7 @@ private static void CreateInboxMsSql(string connectionString) private static void CreateInboxPostgres(string connectionString) { - using var sqlConnection = new SqliteConnection(connectionString); + using var sqlConnection = new NpgsqlConnection(connectionString); sqlConnection.Open(); using var exists = sqlConnection.CreateCommand(); diff --git a/samples/WebAPI_Dapper/SalutationAnalytics/Program.cs b/samples/WebAPI_Dapper/SalutationAnalytics/Program.cs index 07e2c8d602..1bb0f9cd6e 100644 --- a/samples/WebAPI_Dapper/SalutationAnalytics/Program.cs +++ b/samples/WebAPI_Dapper/SalutationAnalytics/Program.cs @@ -12,7 +12,9 @@ using Paramore.Brighter.Inbox.MySql; using Paramore.Brighter.Inbox.Sqlite; 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; @@ -233,6 +235,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"); } @@ -250,7 +258,18 @@ private static void ConfigureDapperMySql(IServiceCollection services) services.AddScoped(); } + private static void ConfigureDapperMsSql(IServiceCollection services) + { + services.AddScoped(); + services.AddScoped(); + } + private static void ConfigureDapperPostgreSql(IServiceCollection services) + { + services.AddScoped(); + services.AddScoped(); + } + private static IAmAnInbox CreateInbox(HostBuilderContext hostContext, IAmARelationalDatabaseConfiguration configuration) { if (hostContext.HostingEnvironment.IsDevelopment()) 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/Salutations_Migrations/Migrations/202205161812_SqliteMigrations.cs b/samples/WebAPI_Dapper/Salutations_Migrations/Migrations/202205161812_SqliteMigrations.cs index a2d7c654f6..a0f91cec2e 100644 --- a/samples/WebAPI_Dapper/Salutations_Migrations/Migrations/202205161812_SqliteMigrations.cs +++ b/samples/WebAPI_Dapper/Salutations_Migrations/Migrations/202205161812_SqliteMigrations.cs @@ -10,7 +10,7 @@ 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_Kafka/GreetingsWeb/Database/OutboxExtensions.cs b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/OutboxExtensions.cs index 3a763c9021..6c80d26793 100644 --- a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/OutboxExtensions.cs +++ b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/OutboxExtensions.cs @@ -57,12 +57,12 @@ private static (IAmAnOutbox, Type, Type) MakePostgresSqlOutbox(RelationalDatabas dataSourceBuilder.MapComposite(); var dataSource = dataSourceBuilder.Build(); - return (new PostgreSqlOutbox(configuration, dataSource), typeof(NpgsqConnectionProvider), typeof(NpgsqlUnitOfWork)); + return (new PostgreSqlOutbox(configuration, dataSource), typeof(PostgreSqlConnectionProvider), typeof(PostgreSqlUnitOfWork)); } private static (IAmAnOutbox, Type, Type) MakeMsSqlOutbox(RelationalDatabaseConfiguration configuration) { - return new(new MsSqlOutbox(configuration), typeof(MsSqlAuthConnectionProvider), typeof(MsSqlUnitOfWork)); + return new(new MsSqlOutbox(configuration), typeof(MsSqlConnectionProvider), typeof(MsSqlUnitOfWork)); } private static (IAmAnOutbox, Type, Type) MakeMySqlOutbox(RelationalDatabaseConfiguration configuration) diff --git a/samples/WebApiWithWorkerAndSweeper/Orders.Sweeper/Extensions/BrighterExtensions.cs b/samples/WebApiWithWorkerAndSweeper/Orders.Sweeper/Extensions/BrighterExtensions.cs index 9fc9e7fcbc..1c2669dd60 100644 --- a/samples/WebApiWithWorkerAndSweeper/Orders.Sweeper/Extensions/BrighterExtensions.cs +++ b/samples/WebApiWithWorkerAndSweeper/Orders.Sweeper/Extensions/BrighterExtensions.cs @@ -50,7 +50,7 @@ public static WebApplicationBuilder AddBrighter(this WebApplicationBuilder build } else { - transactionProviderType = typeof(MsSqlAuthConnectionProvider); + transactionProviderType = typeof(MsSqlConnectionProvider); } builder.Services.AddBrighter() diff --git a/src/Paramore.Brighter.Inbox.MsSql/MsSqlInbox.cs b/src/Paramore.Brighter.Inbox.MsSql/MsSqlInbox.cs index 5f583704ff..527fd9ba55 100644 --- a/src/Paramore.Brighter.Inbox.MsSql/MsSqlInbox.cs +++ b/src/Paramore.Brighter.Inbox.MsSql/MsSqlInbox.cs @@ -66,7 +66,7 @@ public MsSqlInbox(IAmARelationalDatabaseConfiguration configuration, IAmARelatio /// /// The configuration. public MsSqlInbox(IAmARelationalDatabaseConfiguration configuration) : this(configuration, - new MsSqlAuthConnectionProvider(configuration)) + new MsSqlConnectionProvider(configuration)) { } diff --git a/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlMessageConsumer.cs b/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlMessageConsumer.cs index c1be691e0a..f29611e605 100644 --- a/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlMessageConsumer.cs +++ b/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlMessageConsumer.cs @@ -25,7 +25,7 @@ RelationalDbConnectionProvider connectionProvider public MsSqlMessageConsumer( RelationalDatabaseConfiguration msSqlConfiguration, - string topic) :this(msSqlConfiguration, topic, new MsSqlAuthConnectionProvider(msSqlConfiguration)) + string topic) :this(msSqlConfiguration, topic, new MsSqlConnectionProvider(msSqlConfiguration)) { } diff --git a/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlMessageProducer.cs b/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlMessageProducer.cs index 4abe9f2588..5d8e1b4dc9 100644 --- a/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlMessageProducer.cs +++ b/src/Paramore.Brighter.MessagingGateway.MsSql/MsSqlMessageProducer.cs @@ -71,7 +71,7 @@ public MsSqlMessageProducer( public MsSqlMessageProducer( RelationalDatabaseConfiguration msSqlConfiguration, - Publication publication = null) : this(msSqlConfiguration, new MsSqlAuthConnectionProvider(msSqlConfiguration), publication) + Publication publication = null) : this(msSqlConfiguration, new MsSqlConnectionProvider(msSqlConfiguration), publication) { } diff --git a/src/Paramore.Brighter.MsSql/MsSqlAuthConnectionProvider.cs b/src/Paramore.Brighter.MsSql/MsSqlConnectionProvider.cs similarity index 91% rename from src/Paramore.Brighter.MsSql/MsSqlAuthConnectionProvider.cs rename to src/Paramore.Brighter.MsSql/MsSqlConnectionProvider.cs index f7c5027fb7..3c3fb0bc44 100644 --- a/src/Paramore.Brighter.MsSql/MsSqlAuthConnectionProvider.cs +++ b/src/Paramore.Brighter.MsSql/MsSqlConnectionProvider.cs @@ -9,7 +9,7 @@ namespace Paramore.Brighter.MsSql /// /// A connection provider for Sqlite /// - public class MsSqlAuthConnectionProvider : RelationalDbConnectionProvider + public class MsSqlConnectionProvider : RelationalDbConnectionProvider { private readonly string _connectionString; @@ -17,7 +17,7 @@ public class MsSqlAuthConnectionProvider : RelationalDbConnectionProvider /// Create a connection provider for MSSQL using a connection string for Db access /// /// The configuration for this database - public MsSqlAuthConnectionProvider(IAmARelationalDatabaseConfiguration configuration) + public MsSqlConnectionProvider(IAmARelationalDatabaseConfiguration configuration) { if (string.IsNullOrWhiteSpace(configuration?.ConnectionString)) throw new ArgumentNullException(nameof(configuration.ConnectionString)); diff --git a/src/Paramore.Brighter.Outbox.MsSql/MsSqlOutbox.cs b/src/Paramore.Brighter.Outbox.MsSql/MsSqlOutbox.cs index 49eb52b15a..8428039224 100644 --- a/src/Paramore.Brighter.Outbox.MsSql/MsSqlOutbox.cs +++ b/src/Paramore.Brighter.Outbox.MsSql/MsSqlOutbox.cs @@ -65,7 +65,7 @@ public MsSqlOutbox(IAmARelationalDatabaseConfiguration configuration, IAmARelati /// /// The configuration. public MsSqlOutbox(IAmARelationalDatabaseConfiguration configuration) : this(configuration, - new MsSqlAuthConnectionProvider(configuration)) + new MsSqlConnectionProvider(configuration)) { } diff --git a/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutbox.cs b/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutbox.cs index 22f9f42631..a71cd47b3d 100644 --- a/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutbox.cs +++ b/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutbox.cs @@ -73,7 +73,7 @@ public PostgreSqlOutbox( public PostgreSqlOutbox( IAmARelationalDatabaseConfiguration configuration, NpgsqlDataSource dataSource = null) - : this(configuration, new NpgsqConnectionProvider(configuration, dataSource)) + : this(configuration, new PostgreSqlConnectionProvider(configuration, dataSource)) { } protected override void WriteToStore( diff --git a/src/Paramore.Brighter.PostgreSql/NpgsqConnectionProvider.cs b/src/Paramore.Brighter.PostgreSql/PostgreSqlConnectionProvider.cs similarity index 96% rename from src/Paramore.Brighter.PostgreSql/NpgsqConnectionProvider.cs rename to src/Paramore.Brighter.PostgreSql/PostgreSqlConnectionProvider.cs index 2c9ec36c1d..4425565947 100644 --- a/src/Paramore.Brighter.PostgreSql/NpgsqConnectionProvider.cs +++ b/src/Paramore.Brighter.PostgreSql/PostgreSqlConnectionProvider.cs @@ -9,7 +9,7 @@ namespace Paramore.Brighter.PostgreSql /// /// A connection provider that uses the connection string to create a connection /// - public class NpgsqConnectionProvider : RelationalDbConnectionProvider + public class PostgreSqlConnectionProvider : RelationalDbConnectionProvider { private NpgsqlDataSource _dataSource; private readonly string _connectionString; @@ -21,7 +21,7 @@ public class NpgsqConnectionProvider : RelationalDbConnectionProvider /// 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 NpgsqConnectionProvider( + public PostgreSqlConnectionProvider( IAmARelationalDatabaseConfiguration configuration, NpgsqlDataSource dataSource = null) { diff --git a/src/Paramore.Brighter.PostgreSql/NpgsqlUnitOfWork.cs b/src/Paramore.Brighter.PostgreSql/PostgreSqlUnitOfWork.cs similarity index 97% rename from src/Paramore.Brighter.PostgreSql/NpgsqlUnitOfWork.cs rename to src/Paramore.Brighter.PostgreSql/PostgreSqlUnitOfWork.cs index 47b930bd6d..b0114dd6e9 100644 --- a/src/Paramore.Brighter.PostgreSql/NpgsqlUnitOfWork.cs +++ b/src/Paramore.Brighter.PostgreSql/PostgreSqlUnitOfWork.cs @@ -10,7 +10,7 @@ namespace Paramore.Brighter.PostgreSql /// /// A connection provider that uses the connection string to create a connection /// - public class NpgsqlUnitOfWork : RelationalDbTransactionProvider + public class PostgreSqlUnitOfWork : RelationalDbTransactionProvider { private NpgsqlDataSource _dataSource; private readonly string _connectionString; @@ -22,7 +22,7 @@ public class NpgsqlUnitOfWork : RelationalDbTransactionProvider /// 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 NpgsqlUnitOfWork( + public PostgreSqlUnitOfWork( IAmARelationalDatabaseConfiguration configuration, NpgsqlDataSource dataSource = null) { diff --git a/tests/Paramore.Brighter.MSSQL.Tests/MsSqlTestHelper.cs b/tests/Paramore.Brighter.MSSQL.Tests/MsSqlTestHelper.cs index 4f37c1480c..a461fb503b 100644 --- a/tests/Paramore.Brighter.MSSQL.Tests/MsSqlTestHelper.cs +++ b/tests/Paramore.Brighter.MSSQL.Tests/MsSqlTestHelper.cs @@ -59,9 +59,9 @@ public MsSqlTestHelper(bool binaryMessagePayload = false) _tableName = $"test_{Guid.NewGuid()}"; _connectionProvider = - new MsSqlAuthConnectionProvider(new RelationalDatabaseConfiguration(_sqlSettings.TestsBrighterConnectionString)); + new MsSqlConnectionProvider(new RelationalDatabaseConfiguration(_sqlSettings.TestsBrighterConnectionString)); _masterConnectionProvider = - new MsSqlAuthConnectionProvider(new RelationalDatabaseConfiguration(_sqlSettings.TestsMasterConnectionString)); + new MsSqlConnectionProvider(new RelationalDatabaseConfiguration(_sqlSettings.TestsMasterConnectionString)); } public void CreateDatabase() From 4494645923fa9e3249c0184bc5a352a1092639ef Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Sat, 24 Jun 2023 11:52:19 -0500 Subject: [PATCH 75/89] Fix DDL for Postgres --- src/Paramore.Brighter.Inbox.Postgres/PostgreSqlInboxBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Paramore.Brighter.Inbox.Postgres/PostgreSqlInboxBuilder.cs b/src/Paramore.Brighter.Inbox.Postgres/PostgreSqlInboxBuilder.cs index 6441abc022..4738df31da 100644 --- a/src/Paramore.Brighter.Inbox.Postgres/PostgreSqlInboxBuilder.cs +++ b/src/Paramore.Brighter.Inbox.Postgres/PostgreSqlInboxBuilder.cs @@ -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 From 8d4fe32730172f48a5438c3ff7e3590e572cd72b Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Sat, 24 Jun 2023 12:17:09 -0500 Subject: [PATCH 76/89] Fix exists SQL for inbox --- .../.idea/httpRequests/http-requests-log.http | 210 +++++++++++------- .../SqlInboxBuilder.cs | 2 +- 2 files changed, 126 insertions(+), 86 deletions(-) diff --git a/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http b/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http index fe12191b06..382e3a71f4 100644 --- a/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http +++ b/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http @@ -9,7 +9,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-24T093617.200.json +<> 2023-06-24T121016.200.json ### @@ -24,7 +24,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-24T093616.200.json +<> 2023-06-24T121015.200.json ### @@ -39,7 +39,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-24T093614.200.json +<> 2023-06-24T121014-1.200.json ### @@ -54,7 +54,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-24T093611.500.json +<> 2023-06-24T121014.200.json ### @@ -69,7 +69,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-24T093606.200.json +<> 2023-06-24T121013.200.json ### @@ -84,16 +84,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-24T093605.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-06-24T093600.200.json +<> 2023-06-24T121012.200.json ### @@ -108,7 +99,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-24T091635-1.200.json +<> 2023-06-24T121011.200.json ### @@ -123,7 +114,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-24T091635.200.json +<> 2023-06-24T121010-1.200.json ### @@ -138,7 +129,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-24T091634.200.json +<> 2023-06-24T121010.200.json ### @@ -153,40 +144,40 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-24T091632.200.json +<> 2023-06-24T121008.200.json ### -POST http://localhost:5000/Greetings/Tyrion/new -Content-Type: application/json -Content-Length: 47 +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-06-24T091631.200.json +<> 2023-06-24T121005.200.json ### -GET http://localhost:5000/Greetings/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 -<> 2023-06-24T091625.200.json +<> 2023-06-24T121002.200.json ### -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 -<> 2023-06-24T091620.200.json +{ + "Greeting" : "I drink, and I know things" +} + +<> 2023-06-24T093617.200.json ### @@ -201,7 +192,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-24T091307.500.json +<> 2023-06-24T093616.200.json ### @@ -216,7 +207,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-24T091303.200.json +<> 2023-06-24T093614.200.json ### @@ -231,7 +222,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-24T091302.200.json +<> 2023-06-24T093611.500.json ### @@ -246,7 +237,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-24T091301.200.json +<> 2023-06-24T093606.200.json ### @@ -261,7 +252,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-24T091259.200.json +<> 2023-06-24T093605.200.json ### @@ -270,7 +261,7 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-06-24T091255.200.json +<> 2023-06-24T093600.200.json ### @@ -285,7 +276,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-19T203414.200.json +<> 2023-06-24T091635-1.200.json ### @@ -300,7 +291,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-19T203413.200.json +<> 2023-06-24T091635.200.json ### @@ -315,7 +306,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-19T203412.200.json +<> 2023-06-24T091634.200.json ### @@ -330,7 +321,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-19T203410.200.json +<> 2023-06-24T091632.200.json ### @@ -345,7 +336,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-19T203409.200.json +<> 2023-06-24T091631.200.json ### @@ -354,7 +345,7 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-06-19T203406.200.json +<> 2023-06-24T091625.200.json ### @@ -363,7 +354,7 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-06-19T203359.200.json +<> 2023-06-24T091620.200.json ### @@ -378,7 +369,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-19T191748.200.json +<> 2023-06-24T091307.500.json ### @@ -393,16 +384,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-19T191743.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-06-19T191738.200.json +<> 2023-06-24T091303.200.json ### @@ -417,31 +399,37 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-17T210322.200.json +<> 2023-06-24T091302.200.json ### -GET http://localhost:5000/Greetings/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 -<> 2023-06-17T210320.200.json +{ + "Greeting" : "I drink, and I know things" +} + +<> 2023-06-24T091301.200.json ### -POST http://localhost:5000/People/new +POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json -Content-Length: 23 +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" } -<> 2023-06-17T210317.200.json +<> 2023-06-24T091259.200.json ### @@ -450,23 +438,37 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-06-17T210315.404.json +<> 2023-06-24T091255.200.json ### -DELETE 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 +{ + "Greeting" : "I drink, and I know things" +} + +<> 2023-06-19T203414.200.json + ### -GET http://localhost:5000/Greetings/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 -<> 2023-06-17T210308.200.json +{ + "Greeting" : "I drink, and I know things" +} + +<> 2023-06-19T203413.200.json ### @@ -481,40 +483,46 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-17T210303.200.json +<> 2023-06-19T203412.200.json ### -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 -<> 2023-06-17T210258.200.json +{ + "Greeting" : "I drink, and I know things" +} + +<> 2023-06-19T203410.200.json ### -POST http://localhost:5000/People/new +POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json -Content-Length: 23 +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" } -<> 2023-06-17T210256.200.json +<> 2023-06-19T203409.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 -<> 2023-06-17T210252.404.json +<> 2023-06-19T203406.200.json ### @@ -523,14 +531,22 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip +<> 2023-06-19T203359.200.json + ### -GET http://localhost:5000/Greetings/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 -<> 2023-06-17T200601.200.json +{ + "Greeting" : "I drink, and I know things" +} + +<> 2023-06-19T191748.200.json ### @@ -545,16 +561,16 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-17T200557.200.json +<> 2023-06-19T191743.200.json ### -GET http://localhost:5000/Greetings/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 -<> 2023-06-17T200554.200.json +<> 2023-06-19T191738.200.json ### @@ -569,7 +585,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-17T200550.200.json +<> 2023-06-17T210322.200.json ### @@ -578,7 +594,7 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-06-17T200547.200.json +<> 2023-06-17T210320.200.json ### @@ -593,7 +609,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Name" : "Tyrion" } -<> 2023-06-17T200534.200.json +<> 2023-06-17T210317.200.json ### @@ -602,7 +618,7 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-06-17T200532.404.json +<> 2023-06-17T210315.404.json ### @@ -618,7 +634,31 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-06-17T200450.200.json +<> 2023-06-17T210308.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-06-17T210303.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-06-17T210258.200.json ### diff --git a/src/Paramore.Brighter.Inbox.MsSql/SqlInboxBuilder.cs b/src/Paramore.Brighter.Inbox.MsSql/SqlInboxBuilder.cs index 4e4343dc06..f34b1272e4 100644 --- a/src/Paramore.Brighter.Inbox.MsSql/SqlInboxBuilder.cs +++ b/src/Paramore.Brighter.Inbox.MsSql/SqlInboxBuilder.cs @@ -41,7 +41,7 @@ [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 From 540409a2b90e0c846df883414855cb513cb67e9d Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Sat, 24 Jun 2023 12:38:59 -0500 Subject: [PATCH 77/89] Remove old DDL statements; improved exists checks --- .../DDL Scripts/MSSQL/Inbox.sql | 28 ----------- .../Paramore.Brighter.Inbox.MsSql.csproj | 3 -- .../MySqlInboxBuilder.cs | 3 +- .../DDL Scripts/SQLite/CommandStore.sql | 13 ------ .../DDL_SCRIPTS/MSSQL/Outbox.sql | 46 ------------------- .../Paramore.Brighter.Outbox.MsSql.csproj | 3 -- .../SqlOutboxBuilder.cs | 2 +- .../MySqlOutboxBuilder.cs | 2 +- .../DDL_SCRIPTS/POSTGRESQL/Outbox.sql | 15 ------ .../PostgreSqlOutboxBulder.cs | 2 +- .../DDL_SCRIPTS/SQLite/Outbox.sql | 27 ----------- 11 files changed, 4 insertions(+), 140 deletions(-) delete mode 100644 src/Paramore.Brighter.Inbox.MsSql/DDL Scripts/MSSQL/Inbox.sql delete mode 100644 src/Paramore.Brighter.Inbox.Sqlite/DDL Scripts/SQLite/CommandStore.sql delete mode 100644 src/Paramore.Brighter.Outbox.MsSql/DDL_SCRIPTS/MSSQL/Outbox.sql delete mode 100644 src/Paramore.Brighter.Outbox.PostgreSql/DDL_SCRIPTS/POSTGRESQL/Outbox.sql delete mode 100644 src/Paramore.Brighter.Outbox.Sqlite/DDL_SCRIPTS/SQLite/Outbox.sql 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/Paramore.Brighter.Inbox.MsSql.csproj b/src/Paramore.Brighter.Inbox.MsSql/Paramore.Brighter.Inbox.MsSql.csproj index 55ab4be5be..b0fee9b061 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.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.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.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/Paramore.Brighter.Outbox.MsSql.csproj b/src/Paramore.Brighter.Outbox.MsSql/Paramore.Brighter.Outbox.MsSql.csproj index e33a061702..5919a8ae69 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/SqlOutboxBuilder.cs b/src/Paramore.Brighter.Outbox.MsSql/SqlOutboxBuilder.cs index 5e189e7896..47e8656d81 100644 --- a/src/Paramore.Brighter.Outbox.MsSql/SqlOutboxBuilder.cs +++ b/src/Paramore.Brighter.Outbox.MsSql/SqlOutboxBuilder.cs @@ -70,7 +70,7 @@ 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 diff --git a/src/Paramore.Brighter.Outbox.MySql/MySqlOutboxBuilder.cs b/src/Paramore.Brighter.Outbox.MySql/MySqlOutboxBuilder.cs index c39512f50e..bad152c290 100644 --- a/src/Paramore.Brighter.Outbox.MySql/MySqlOutboxBuilder.cs +++ b/src/Paramore.Brighter.Outbox.MySql/MySqlOutboxBuilder.cs @@ -69,7 +69,7 @@ PRIMARY KEY (`MessageId`) PRIMARY KEY (`MessageId`) ) ENGINE = InnoDB;"; - const string outboxExistsQuery = @"SHOW TABLES LIKE '{0}'; "; + 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 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/PostgreSqlOutboxBulder.cs b/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutboxBulder.cs index d910953074..4dc047485f 100644 --- a/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutboxBulder.cs +++ b/src/Paramore.Brighter.Outbox.PostgreSql/PostgreSqlOutboxBulder.cs @@ -65,7 +65,7 @@ Body bytea NULL ); "; - private const string OutboxExistsSQL = @"SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = N'{0}'"; + 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 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; - From 72598d656b3f9a0113e4b685f020d833bb18176f Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Tue, 27 Jun 2023 13:22:43 +0100 Subject: [PATCH 78/89] Improved outbox creation handling --- .../.idea/httpRequests/http-requests-log.http | 244 +++++++++--------- .../GreetingsWeb/Database/SchemaCreation.cs | 11 +- .../Database/SchemaCreation.cs | 34 ++- .../Handlers/GreetingMadeHandler.cs | 3 +- 4 files changed, 153 insertions(+), 139 deletions(-) diff --git a/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http b/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http index 382e3a71f4..31959997fb 100644 --- a/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http +++ b/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http @@ -9,7 +9,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-24T121016.200.json +<> 2023-06-27T091728.200.json ### @@ -24,7 +24,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-24T121015.200.json +<> 2023-06-27T091727.200.json ### @@ -39,7 +39,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-24T121014-1.200.json +<> 2023-06-27T091726-1.200.json ### @@ -54,7 +54,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-24T121014.200.json +<> 2023-06-27T091726.200.json ### @@ -69,22 +69,25 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-24T121013.200.json +<> 2023-06-27T091721.200.json ### -POST http://localhost:5000/Greetings/Tyrion/new -Content-Type: application/json -Content-Length: 47 +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-06-27T091718.200.json -<> 2023-06-24T121012.200.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 + +<> 2023-06-27T090113.200.json ### @@ -99,7 +102,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-24T121011.200.json +<> 2023-06-27T090110.200.json ### @@ -114,7 +117,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-24T121010-1.200.json +<> 2023-06-27T090108.200.json ### @@ -129,7 +132,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-24T121010.200.json +<> 2023-06-27T090104.200.json ### @@ -144,7 +147,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-24T121008.200.json +<> 2023-06-27T090100.200.json ### @@ -153,31 +156,31 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-06-24T121005.200.json +<> 2023-06-27T090051.200.json ### -GET http://localhost:5000/People/Tyrion +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 -<> 2023-06-24T121002.200.json +{ + "Name" : "Tyrion" +} + +<> 2023-06-27T090042.200.json ### -POST http://localhost:5000/Greetings/Tyrion/new -Content-Type: application/json -Content-Length: 47 +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-06-24T093617.200.json +<> 2023-06-27T090039.404.json ### @@ -192,7 +195,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-24T093616.200.json +<> 2023-06-27T085001.200.json ### @@ -207,7 +210,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-24T093614.200.json +<> 2023-06-27T085000.200.json ### @@ -222,7 +225,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-24T093611.500.json +<> 2023-06-27T084959.200.json ### @@ -237,7 +240,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-24T093606.200.json +<> 2023-06-27T084958.200.json ### @@ -252,7 +255,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-24T093605.200.json +<> 2023-06-27T084956.200.json ### @@ -261,92 +264,55 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-06-24T093600.200.json +<> 2023-06-27T084954.200.json ### -POST http://localhost:5000/Greetings/Tyrion/new +POST http://localhost:5000/People/new Content-Type: application/json -Content-Length: 47 +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" } -<> 2023-06-24T091635-1.200.json +<> 2023-06-27T084950.200.json ### -POST http://localhost:5000/Greetings/Tyrion/new -Content-Type: application/json -Content-Length: 47 +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-06-24T091635.200.json +<> 2023-06-27T084946.404.json ### -POST http://localhost:5000/Greetings/Tyrion/new -Content-Type: application/json -Content-Length: 47 +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-06-24T091634.200.json +<> 2023-06-27T084900.200.json ### -POST http://localhost:5000/Greetings/Tyrion/new -Content-Type: application/json -Content-Length: 47 +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-06-24T091632.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-06-24T091631.200.json - -### - -GET http://localhost:5000/Greetings/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 -<> 2023-06-24T091625.200.json - ### GET http://localhost:5000/People/Tyrion @@ -354,8 +320,6 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-06-24T091620.200.json - ### POST http://localhost:5000/Greetings/Tyrion/new @@ -369,7 +333,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-24T091307.500.json +<> 2023-06-24T121016.200.json ### @@ -384,7 +348,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-24T091303.200.json +<> 2023-06-24T121015.200.json ### @@ -399,7 +363,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-24T091302.200.json +<> 2023-06-24T121014-1.200.json ### @@ -414,7 +378,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-24T091301.200.json +<> 2023-06-24T121014.200.json ### @@ -429,16 +393,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-24T091259.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-06-24T091255.200.json +<> 2023-06-24T121013.200.json ### @@ -453,7 +408,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-19T203414.200.json +<> 2023-06-24T121012.200.json ### @@ -468,7 +423,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-19T203413.200.json +<> 2023-06-24T121011.200.json ### @@ -483,7 +438,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-19T203412.200.json +<> 2023-06-24T121010-1.200.json ### @@ -498,7 +453,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-19T203410.200.json +<> 2023-06-24T121010.200.json ### @@ -513,7 +468,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-19T203409.200.json +<> 2023-06-24T121008.200.json ### @@ -522,7 +477,7 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-06-19T203406.200.json +<> 2023-06-24T121005.200.json ### @@ -531,7 +486,7 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-06-19T203359.200.json +<> 2023-06-24T121002.200.json ### @@ -546,7 +501,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-19T191748.200.json +<> 2023-06-24T093617.200.json ### @@ -561,16 +516,22 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-19T191743.200.json +<> 2023-06-24T093616.200.json ### -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 -<> 2023-06-19T191738.200.json +{ + "Greeting" : "I drink, and I know things" +} + +<> 2023-06-24T093614.200.json ### @@ -585,31 +546,37 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-17T210322.200.json +<> 2023-06-24T093611.500.json ### -GET http://localhost:5000/Greetings/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 -<> 2023-06-17T210320.200.json +{ + "Greeting" : "I drink, and I know things" +} + +<> 2023-06-24T093606.200.json ### -POST http://localhost:5000/People/new +POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json -Content-Length: 23 +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" } -<> 2023-06-17T210317.200.json +<> 2023-06-24T093605.200.json ### @@ -618,23 +585,52 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-06-17T210315.404.json +<> 2023-06-24T093600.200.json ### -DELETE 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 +{ + "Greeting" : "I drink, and I know things" +} + +<> 2023-06-24T091635-1.200.json + ### -GET http://localhost:5000/Greetings/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 + +{ + "Greeting" : "I drink, and I know things" +} + +<> 2023-06-24T091635.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 -<> 2023-06-17T210308.200.json +{ + "Greeting" : "I drink, and I know things" +} + +<> 2023-06-24T091634.200.json ### @@ -649,16 +645,22 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-17T210303.200.json +<> 2023-06-24T091632.200.json ### -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 -<> 2023-06-17T210258.200.json +{ + "Greeting" : "I drink, and I know things" +} + +<> 2023-06-24T091631.200.json ### diff --git a/samples/WebAPI_Dapper/GreetingsWeb/Database/SchemaCreation.cs b/samples/WebAPI_Dapper/GreetingsWeb/Database/SchemaCreation.cs index 5560757ef5..42474fdf21 100644 --- a/samples/WebAPI_Dapper/GreetingsWeb/Database/SchemaCreation.cs +++ b/samples/WebAPI_Dapper/GreetingsWeb/Database/SchemaCreation.cs @@ -171,7 +171,8 @@ private static void CreateOutboxMsSql(string connectionString) using var existsQuery = sqlConnection.CreateCommand(); existsQuery.CommandText = SqlOutboxBuilder.GetExistsQuery(OUTBOX_TABLE_NAME); - bool exists = existsQuery.ExecuteScalar() != null; + var findOutbox = existsQuery.ExecuteScalar(); + bool exists = findOutbox is long and > 0; if (exists) return; @@ -188,7 +189,8 @@ private static void CreateOutboxMySql(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; @@ -204,8 +206,9 @@ private static void CreateOutboxPostgres(string connectionString) using var existsQuery = sqlConnection.CreateCommand(); existsQuery.CommandText = PostgreSqlOutboxBulder.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(); diff --git a/samples/WebAPI_Dapper/SalutationAnalytics/Database/SchemaCreation.cs b/samples/WebAPI_Dapper/SalutationAnalytics/Database/SchemaCreation.cs index 3d3d3f68e0..fc7b8992c4 100644 --- a/samples/WebAPI_Dapper/SalutationAnalytics/Database/SchemaCreation.cs +++ b/samples/WebAPI_Dapper/SalutationAnalytics/Database/SchemaCreation.cs @@ -196,7 +196,8 @@ private static void CreateInboxMySql(string connectionString) using var existsQuery = sqlConnection.CreateCommand(); existsQuery.CommandText = MySqlInboxBuilder.GetExistsQuery(INBOX_TABLE_NAME); - bool exists = existsQuery.ExecuteScalar() != null; + var findOutbox = existsQuery.ExecuteScalar(); + bool exists = findOutbox is long and > 0; if (exists) return; @@ -210,11 +211,12 @@ private static void CreateInboxMsSql(string connectionString) using var sqlConnection = new SqlConnection(connectionString); sqlConnection.Open(); - using var exists = sqlConnection.CreateCommand(); - exists.CommandText = SqlInboxBuilder.GetExistsQuery(INBOX_TABLE_NAME); - using var reader = exists.ExecuteReader(CommandBehavior.SingleRow); + using var existsQuery = sqlConnection.CreateCommand(); + existsQuery.CommandText = SqlInboxBuilder.GetExistsQuery(INBOX_TABLE_NAME); + var findOutbox = existsQuery.ExecuteScalar(); + bool exists = findOutbox is long and > 0; - if (reader.HasRows) return; + if (exists) return; using var command = sqlConnection.CreateCommand(); command.CommandText = SqlInboxBuilder.GetDDL(INBOX_TABLE_NAME); @@ -226,11 +228,14 @@ private static void CreateInboxPostgres(string connectionString) using var sqlConnection = new NpgsqlConnection(connectionString); sqlConnection.Open(); - using var exists = sqlConnection.CreateCommand(); - exists.CommandText = PostgreSqlInboxBuilder.GetExistsQuery(INBOX_TABLE_NAME); - using var reader = exists.ExecuteReader(CommandBehavior.SingleRow); + using var existsQuery = sqlConnection.CreateCommand(); + existsQuery.CommandText = PostgreSqlInboxBuilder.GetExistsQuery(INBOX_TABLE_NAME); + + var findOutbox = existsQuery.ExecuteScalar(); + bool exists = findOutbox is long and > 0; + + if (exists) return; - if (reader.HasRows) return; using var command = sqlConnection.CreateCommand(); command.CommandText = PostgreSqlInboxBuilder.GetDDL(INBOX_TABLE_NAME); @@ -298,7 +303,8 @@ private static void CreateOutboxMsSql(string connectionString) using var existsQuery = sqlConnection.CreateCommand(); existsQuery.CommandText = SqlOutboxBuilder.GetExistsQuery(OUTBOX_TABLE_NAME); - bool exists = existsQuery.ExecuteScalar() != null; + var findOutbox = existsQuery.ExecuteScalar(); + bool exists = findOutbox is long and > 0; if (exists) return; @@ -315,7 +321,8 @@ private static void CreateOutboxMySql(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; @@ -331,8 +338,9 @@ private static void CreateOutboxPostgres(string connectionString) using var existsQuery = sqlConnection.CreateCommand(); existsQuery.CommandText = PostgreSqlOutboxBulder.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(); diff --git a/samples/WebAPI_Dapper/SalutationPorts/Handlers/GreetingMadeHandler.cs b/samples/WebAPI_Dapper/SalutationPorts/Handlers/GreetingMadeHandler.cs index fdf6434dc5..0fca6ec34d 100644 --- a/samples/WebAPI_Dapper/SalutationPorts/Handlers/GreetingMadeHandler.cs +++ b/samples/WebAPI_Dapper/SalutationPorts/Handlers/GreetingMadeHandler.cs @@ -4,6 +4,7 @@ using Dapper; using Microsoft.Extensions.Logging; using Paramore.Brighter; +using Paramore.Brighter.Inbox.Attributes; using Paramore.Brighter.Logging.Attributes; using Paramore.Brighter.Policies.Attributes; using SalutationEntities; @@ -24,7 +25,7 @@ public GreetingMadeHandler(IAmABoxTransactionProvider transaction _logger = logger; } - //[UseInboxAsync(step:0, contextKey: typeof(GreetingMadeHandlerAsync), onceOnly: true )] -- we are using a global inbox, so need to be explicit!! + [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) From aa696534bc1562a89332773f63f880855005a772 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Wed, 28 Jun 2023 08:41:53 +0100 Subject: [PATCH 79/89] Fix PostgreSQL Inbox issues --- .../GreetingsWeb/Database/SchemaCreation.cs | 2 +- .../Database/SchemaCreation.cs | 18 +++++++-------- .../SalutationAnalytics/Program.cs | 22 ++++++++++++++++--- ...s => 202205161812_SqlInitialMigrations.cs} | 0 .../RelationalDatabaseConfiguration.cs | 6 +++-- src/Paramore.Brighter/inboxConfiguration.cs | 2 +- 6 files changed, 34 insertions(+), 16 deletions(-) rename samples/WebAPI_Dapper/Salutations_Migrations/Migrations/{202205161812_SqliteMigrations.cs => 202205161812_SqlInitialMigrations.cs} (100%) diff --git a/samples/WebAPI_Dapper/GreetingsWeb/Database/SchemaCreation.cs b/samples/WebAPI_Dapper/GreetingsWeb/Database/SchemaCreation.cs index 42474fdf21..da566aebe6 100644 --- a/samples/WebAPI_Dapper/GreetingsWeb/Database/SchemaCreation.cs +++ b/samples/WebAPI_Dapper/GreetingsWeb/Database/SchemaCreation.cs @@ -172,7 +172,7 @@ private static void CreateOutboxMsSql(string connectionString) using var existsQuery = sqlConnection.CreateCommand(); existsQuery.CommandText = SqlOutboxBuilder.GetExistsQuery(OUTBOX_TABLE_NAME); var findOutbox = existsQuery.ExecuteScalar(); - bool exists = findOutbox is long and > 0; + bool exists = findOutbox is > 0; if (exists) return; diff --git a/samples/WebAPI_Dapper/SalutationAnalytics/Database/SchemaCreation.cs b/samples/WebAPI_Dapper/SalutationAnalytics/Database/SchemaCreation.cs index fc7b8992c4..40c1382f1c 100644 --- a/samples/WebAPI_Dapper/SalutationAnalytics/Database/SchemaCreation.cs +++ b/samples/WebAPI_Dapper/SalutationAnalytics/Database/SchemaCreation.cs @@ -196,8 +196,8 @@ private static void CreateInboxMySql(string connectionString) using var existsQuery = sqlConnection.CreateCommand(); existsQuery.CommandText = MySqlInboxBuilder.GetExistsQuery(INBOX_TABLE_NAME); - var findOutbox = existsQuery.ExecuteScalar(); - bool exists = findOutbox is long and > 0; + var findInbox = existsQuery.ExecuteScalar(); + bool exists = findInbox is long and > 0; if (exists) return; @@ -213,8 +213,8 @@ private static void CreateInboxMsSql(string connectionString) using var existsQuery = sqlConnection.CreateCommand(); existsQuery.CommandText = SqlInboxBuilder.GetExistsQuery(INBOX_TABLE_NAME); - var findOutbox = existsQuery.ExecuteScalar(); - bool exists = findOutbox is long and > 0; + var findInbox = existsQuery.ExecuteScalar(); + bool exists = findInbox is long and > 0; if (exists) return; @@ -229,10 +229,10 @@ private static void CreateInboxPostgres(string connectionString) sqlConnection.Open(); using var existsQuery = sqlConnection.CreateCommand(); - existsQuery.CommandText = PostgreSqlInboxBuilder.GetExistsQuery(INBOX_TABLE_NAME); + existsQuery.CommandText = PostgreSqlInboxBuilder.GetExistsQuery(INBOX_TABLE_NAME.ToLower()); - var findOutbox = existsQuery.ExecuteScalar(); - bool exists = findOutbox is long and > 0; + var findInbox = existsQuery.ExecuteScalar(); + bool exists = findInbox is true; if (exists) return; @@ -337,9 +337,9 @@ private static void CreateOutboxPostgres(string connectionString) sqlConnection.Open(); using var existsQuery = sqlConnection.CreateCommand(); - existsQuery.CommandText = PostgreSqlOutboxBulder.GetExistsQuery(OUTBOX_TABLE_NAME); + existsQuery.CommandText = PostgreSqlOutboxBulder.GetExistsQuery(OUTBOX_TABLE_NAME.ToLower()); var findOutbox = existsQuery.ExecuteScalar(); - bool exists = findOutbox is long and > 0; + bool exists = findOutbox is true; if (exists) return; diff --git a/samples/WebAPI_Dapper/SalutationAnalytics/Program.cs b/samples/WebAPI_Dapper/SalutationAnalytics/Program.cs index 1bb0f9cd6e..2f90bbfeeb 100644 --- a/samples/WebAPI_Dapper/SalutationAnalytics/Program.cs +++ b/samples/WebAPI_Dapper/SalutationAnalytics/Program.cs @@ -9,7 +9,9 @@ using Paramore.Brighter; 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.RMQ; using Paramore.Brighter.MsSql; @@ -27,6 +29,7 @@ namespace SalutationAnalytics class Program { private const string OUTBOX_TABLE_NAME = "Outbox"; + private const string INBOX_TABLE_NAME = "Inbox"; public static async Task Main(string[] args) { @@ -104,7 +107,8 @@ private static void ConfigureBrighter(HostBuilderContext hostContext, IServiceCo var outboxConfiguration = new RelationalDatabaseConfiguration( DbConnectionString(hostContext), - outBoxTableName:OUTBOX_TABLE_NAME + outBoxTableName:OUTBOX_TABLE_NAME, + inboxTableName: INBOX_TABLE_NAME ); services.AddSingleton(outboxConfiguration); @@ -203,6 +207,7 @@ private static void ConfigurePostgreSql(HostBuilderContext hostBuilderContext, I 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() ); @@ -277,7 +282,19 @@ private static IAmAnInbox CreateInbox(HostBuilderContext hostContext, IAmARelati return new SqliteInbox(configuration); } - return new MySqlInbox(configuration); + return CreateProductionInbox(GetDatabaseType(hostContext), configuration); + } + + 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) @@ -291,7 +308,6 @@ private static string DbConnectionString(HostBuilderContext hostContext) private static DatabaseType GetDatabaseType(HostBuilderContext hostContext) { return hostContext.Configuration[DatabaseGlobals.DATABASE_TYPE_ENV] switch - { DatabaseGlobals.MYSQL => DatabaseType.MySql, DatabaseGlobals.MSSQL => DatabaseType.MsSql, diff --git a/samples/WebAPI_Dapper/Salutations_Migrations/Migrations/202205161812_SqliteMigrations.cs b/samples/WebAPI_Dapper/Salutations_Migrations/Migrations/202205161812_SqlInitialMigrations.cs similarity index 100% rename from samples/WebAPI_Dapper/Salutations_Migrations/Migrations/202205161812_SqliteMigrations.cs rename to samples/WebAPI_Dapper/Salutations_Migrations/Migrations/202205161812_SqlInitialMigrations.cs diff --git a/src/Paramore.Brighter/RelationalDatabaseConfiguration.cs b/src/Paramore.Brighter/RelationalDatabaseConfiguration.cs index 36145ef053..392dc9347c 100644 --- a/src/Paramore.Brighter/RelationalDatabaseConfiguration.cs +++ b/src/Paramore.Brighter/RelationalDatabaseConfiguration.cs @@ -2,6 +2,8 @@ { 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. @@ -19,8 +21,8 @@ public RelationalDatabaseConfiguration( bool binaryMessagePayload = false ) { - OutBoxTableName = outBoxTableName; - InBoxTableName = inboxTableName; + OutBoxTableName = outBoxTableName ?? OUTBOX_TABLE_NAME; + InBoxTableName = inboxTableName ?? INBOX_TABLE_NAME; ConnectionString = connectionString; QueueStoreTable = queueStoreTable; BinaryMessagePayload = binaryMessagePayload; diff --git a/src/Paramore.Brighter/inboxConfiguration.cs b/src/Paramore.Brighter/inboxConfiguration.cs index ae5b7f8f2e..137a163d1c 100644 --- a/src/Paramore.Brighter/inboxConfiguration.cs +++ b/src/Paramore.Brighter/inboxConfiguration.cs @@ -51,7 +51,7 @@ public InboxConfiguration( OnceOnlyAction actionOnExists = OnceOnlyAction.Throw, Func context = null) { - if (inbox == null) Inbox = new InMemoryInbox(); + Inbox = inbox ?? new InMemoryInbox(); Scope = scope; Context = context; From 48b77a3241f6ae3458d8c0d931f16bebcf58b00b Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Wed, 28 Jun 2023 09:59:24 +0100 Subject: [PATCH 80/89] Fix inbox and outbox creation --- .../SalutationAnalytics/Database/SchemaCreation.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/WebAPI_Dapper/SalutationAnalytics/Database/SchemaCreation.cs b/samples/WebAPI_Dapper/SalutationAnalytics/Database/SchemaCreation.cs index 40c1382f1c..c2eaeb7a87 100644 --- a/samples/WebAPI_Dapper/SalutationAnalytics/Database/SchemaCreation.cs +++ b/samples/WebAPI_Dapper/SalutationAnalytics/Database/SchemaCreation.cs @@ -214,7 +214,7 @@ private static void CreateInboxMsSql(string connectionString) using var existsQuery = sqlConnection.CreateCommand(); existsQuery.CommandText = SqlInboxBuilder.GetExistsQuery(INBOX_TABLE_NAME); var findInbox = existsQuery.ExecuteScalar(); - bool exists = findInbox is long and > 0; + bool exists = findInbox is > 0; if (exists) return; @@ -304,7 +304,7 @@ private static void CreateOutboxMsSql(string connectionString) using var existsQuery = sqlConnection.CreateCommand(); existsQuery.CommandText = SqlOutboxBuilder.GetExistsQuery(OUTBOX_TABLE_NAME); var findOutbox = existsQuery.ExecuteScalar(); - bool exists = findOutbox is long and > 0; + bool exists = findOutbox is > 0; if (exists) return; From 13f30736e557f1596ca9681b6c1c7064602718b5 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Fri, 30 Jun 2023 09:58:17 +0100 Subject: [PATCH 81/89] Allow different transports --- .../GreetingsWeb/GreetingsWeb.csproj | 1 + .../Messaging/MessagingTransport.cs | 21 ++ .../Properties/launchSettings.json | 12 +- samples/WebAPI_Dapper/GreetingsWeb/Startup.cs | 111 +++++++-- .../Messaging/MessagingTransport.cs | 21 ++ .../SalutationAnalytics/Program.cs | 217 ++++++++++++++---- .../Properties/launchSettings.json | 12 +- .../SalutationAnalytics.csproj | 1 + 8 files changed, 318 insertions(+), 78 deletions(-) create mode 100644 samples/WebAPI_Dapper/GreetingsWeb/Messaging/MessagingTransport.cs create mode 100644 samples/WebAPI_Dapper/SalutationAnalytics/Messaging/MessagingTransport.cs diff --git a/samples/WebAPI_Dapper/GreetingsWeb/GreetingsWeb.csproj b/samples/WebAPI_Dapper/GreetingsWeb/GreetingsWeb.csproj index 9f3771e6dc..74cb5cb80a 100644 --- a/samples/WebAPI_Dapper/GreetingsWeb/GreetingsWeb.csproj +++ b/samples/WebAPI_Dapper/GreetingsWeb/GreetingsWeb.csproj @@ -20,6 +20,7 @@ + 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/Properties/launchSettings.json b/samples/WebAPI_Dapper/GreetingsWeb/Properties/launchSettings.json index 6ddca8d915..b8d93cfd6d 100644 --- a/samples/WebAPI_Dapper/GreetingsWeb/Properties/launchSettings.json +++ b/samples/WebAPI_Dapper/GreetingsWeb/Properties/launchSettings.json @@ -17,7 +17,8 @@ "applicationUrl": "https://localhost:5001;http://localhost:5000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", - "BRIGHTER_GREETINGS_DATABASE": "Sqlite" + "BRIGHTER_GREETINGS_DATABASE": "Sqlite", + "BRIGHTER_TRANSPORT": "RabbitMQ" } }, "ProductionMySql": { @@ -28,7 +29,8 @@ "applicationUrl": "https://localhost:5001;http://localhost:5000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Production", - "BRIGHTER_GREETINGS_DATABASE": "MySQL" + "BRIGHTER_GREETINGS_DATABASE": "MySQL", + "BRIGHTER_TRANSPORT": "RabbitMQ" } }, "ProductionPostgres": { @@ -39,7 +41,8 @@ "applicationUrl": "https://localhost:5001;http://localhost:5000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Production", - "BRIGHTER_GREETINGS_DATABASE": "PostgresSQL" + "BRIGHTER_GREETINGS_DATABASE": "PostgresSQL", + "BRIGHTER_TRANSPORT": "RabbitMQ" } }, "ProductionMsSql": { @@ -50,7 +53,8 @@ "applicationUrl": "https://localhost:5001;http://localhost:5000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Production", - "BRIGHTER_GREETINGS_DATABASE": "MsSQL" + "BRIGHTER_GREETINGS_DATABASE": "MsSQL", + "BRIGHTER_TRANSPORT": "RabbitMQ" } } } diff --git a/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs b/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs index 6c40210d81..d65d33e989 100644 --- a/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs +++ b/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs @@ -1,9 +1,11 @@ using System; +using Confluent.SchemaRegistry; using FluentMigrator.Runner; using Greetings_MySqlMigrations.Migrations; using GreetingsPorts.Handlers; using GreetingsPorts.Policies; using GreetingsWeb.Database; +using GreetingsWeb.Messaging; using Hellang.Middleware.ProblemDetails; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; @@ -16,6 +18,7 @@ using Paramore.Brighter; 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; @@ -25,7 +28,6 @@ namespace GreetingsWeb { public class Startup { - private const string OUTBOX_TABLE_NAME = "Outbox"; private readonly IConfiguration _configuration; private readonly IWebHostEnvironment _env; @@ -36,6 +38,15 @@ public Startup(IConfiguration configuration, IWebHostEnvironment env) _env = env; } + 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) { @@ -159,31 +170,16 @@ private void ConfigureSqlite(IServiceCollection services) private void ConfigureBrighter(IServiceCollection services) { + var messagingTransport = GetTransportType(); + + AddSchemaRegistryMaybe(services, messagingTransport); + var outboxConfiguration = new RelationalDatabaseConfiguration( DbConnectionString(), - outBoxTableName:OUTBOX_TABLE_NAME + binaryMessagePayload: messagingTransport == MessagingTransport.Kafka ); services.AddSingleton(outboxConfiguration); - 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(); - (IAmAnOutbox outbox, Type connectionProvider, Type transactionProvider) makeOutbox = OutboxExtensions.MakeOutbox(_env, GetDatabaseType(), outboxConfiguration, services); @@ -197,7 +193,7 @@ private void ConfigureBrighter(IServiceCollection services) }) .UseExternalBus((configure) => { - configure.ProducerRegistry = producerRegistry; + configure.ProducerRegistry = ConfigureProducerRegistry(messagingTransport); configure.Outbox = makeOutbox.outbox; configure.TransactionProvider = makeOutbox.transactionProvider; configure.ConnectionProvider = makeOutbox.connectionProvider; @@ -221,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 @@ -235,7 +241,7 @@ private DatabaseType GetDatabaseType() 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") }; } @@ -252,8 +258,65 @@ private string GetConnectionString(DatabaseType databaseType) DatabaseType.MsSql => _configuration.GetConnectionString("GreetingsMsSql"), DatabaseType.Postgres => _configuration.GetConnectionString("GreetingsPostgreSql"), DatabaseType.Sqlite => GetDevDbConnectionString(), - _ => throw new InvalidOperationException("Could not determine the database type") + _ => 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("greeting.event"), + 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/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 2f90bbfeeb..2335fbcbc4 100644 --- a/samples/WebAPI_Dapper/SalutationAnalytics/Program.cs +++ b/samples/WebAPI_Dapper/SalutationAnalytics/Program.cs @@ -1,6 +1,8 @@ using System; using System.IO; using System.Threading.Tasks; +using Confluent.Kafka; +using Confluent.SchemaRegistry; using FluentMigrator.Runner; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -13,6 +15,7 @@ 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; @@ -21,16 +24,15 @@ using Paramore.Brighter.ServiceActivator.Extensions.Hosting; using Paramore.Brighter.Sqlite; using SalutationAnalytics.Database; +using SalutationAnalytics.Messaging; using SalutationPorts.Policies; using SalutationPorts.Requests; +using ChannelFactory = Paramore.Brighter.MessagingGateway.RMQ.ChannelFactory; namespace SalutationAnalytics { class Program { - private const string OUTBOX_TABLE_NAME = "Outbox"; - private const string INBOX_TABLE_NAME = "Inbox"; - public static async Task Main(string[] args) { var host = CreateHostBuilder(args).Build(); @@ -40,6 +42,15 @@ public static async Task Main(string[] args) host.CreateOutbox(); 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) @@ -67,48 +78,18 @@ 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: false, - timeoutInMilliseconds: 200, - isDurable: true, - makeChannels: OnMissingChannel.Create), //change to OnMissingChannel.Validate if you have infrastructure declared elsewhere - }; - - var relationalDatabaseConfiguration = - new RelationalDatabaseConfiguration(DbConnectionString(hostContext), SchemaCreation.INBOX_TABLE_NAME); - services.AddSingleton(relationalDatabaseConfiguration); + var messagingTransport = GetTransportType(hostContext); - var rmqConnection = new RmqMessagingGatewayConnection - { - AmpqUri = new AmqpUriSpecification(new Uri($"amqp://guest:guest@localhost:5672")), Exchange = new Exchange("paramore.brighter.exchange") - }; - - var rmqMessageConsumerFactory = new RmqMessageConsumerFactory(rmqConnection); + AddSchemaRegistryMaybe(services, messagingTransport); + + Subscription[] subscriptions = GetSubscriptions(messagingTransport); - var producerRegistry = new RmqProducerRegistryFactory( - rmqConnection, - new RmqPublication[] - { - new RmqPublication - { - Topic = new RoutingKey("SalutationReceived"), - MaxOutStandingMessages = 5, - MaxOutStandingCheckIntervalMilliSeconds = 500, - WaitForConfirmsTimeOutInMilliseconds = 1000, - MakeChannels = OnMissingChannel.Create - } - } - ).Create(); + var relationalDatabaseConfiguration = new RelationalDatabaseConfiguration(DbConnectionString(hostContext)); + services.AddSingleton(relationalDatabaseConfiguration); var outboxConfiguration = new RelationalDatabaseConfiguration( DbConnectionString(hostContext), - outBoxTableName:OUTBOX_TABLE_NAME, - inboxTableName: INBOX_TABLE_NAME + binaryMessagePayload: messagingTransport == MessagingTransport.Kafka ); services.AddSingleton(outboxConfiguration); @@ -118,7 +99,7 @@ private static void ConfigureBrighter(HostBuilderContext hostContext, IServiceCo 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; @@ -139,7 +120,7 @@ private static void ConfigureBrighter(HostBuilderContext hostContext, IServiceCo }) .UseExternalBus((config) => { - config.ProducerRegistry = producerRegistry; + config.ProducerRegistry = ConfigureProducerRegistry(messagingTransport); config.Outbox = makeOutbox.outbox; config.ConnectionProvider = makeOutbox.connectionProvider; config.TransactionProvider = makeOutbox.transactionProvider; @@ -148,7 +129,7 @@ private static void ConfigureBrighter(HostBuilderContext hostContext, IServiceCo services.AddHostedService(); } - + private static void ConfigureMigration(HostBuilderContext hostBuilderContext, IServiceCollection services) { //dev is always Sqlite @@ -284,6 +265,16 @@ private static IAmAnInbox CreateInbox(HostBuilderContext hostContext, IAmARelati 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) { @@ -316,7 +307,17 @@ 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 @@ -335,9 +336,133 @@ private static string GetConnectionString(HostBuilderContext hostContext, Databa }; } private static string GetDevDbConnectionString() + { + return "Filename=Salutations.db;Cache=Shared"; + } + + private static IAmAChannelFactory GetKafkaChannelFactory() { - return "Filename=Salutations.db;Cache=Shared"; + 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("greeting.event"), + 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("greeting.event"), + groupId: "kafka-GreetingsReceiverConsole-Sample", + timeoutInMilliseconds: 100, + offsetDefault: AutoOffsetReset.Earliest, + commitBatchSize: 5, + sweepUncommittedOffsetsIntervalMs: 10000, + makeChannels: OnMissingChannel.Create) + }; + return subscriptions; + } + + private static MessagingTransport GetTransportType(HostBuilderContext hostContext) + { + return hostContext.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/SalutationAnalytics/Properties/launchSettings.json b/samples/WebAPI_Dapper/SalutationAnalytics/Properties/launchSettings.json index 7e2b9ed678..639defffb1 100644 --- a/samples/WebAPI_Dapper/SalutationAnalytics/Properties/launchSettings.json +++ b/samples/WebAPI_Dapper/SalutationAnalytics/Properties/launchSettings.json @@ -5,7 +5,8 @@ "commandName": "Project", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", - "BRIGHTER_GREETINGS_DATABASE": "Sqlite" + "BRIGHTER_GREETINGS_DATABASE": "Sqlite", + "BRIGHTER_TRANSPORT": "RabbitMQ" } }, "ProductionMySql": { @@ -16,7 +17,8 @@ "applicationUrl": "https://localhost:5001;http://localhost:5000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Production", - "BRIGHTER_GREETINGS_DATABASE": "MySQL" + "BRIGHTER_GREETINGS_DATABASE": "MySQL", + "BRIGHTER_TRANSPORT": "RabbitMQ" } }, "ProductionPostgres": { @@ -27,7 +29,8 @@ "applicationUrl": "https://localhost:5001;http://localhost:5000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Production", - "BRIGHTER_GREETINGS_DATABASE": "PostgresSQL" + "BRIGHTER_GREETINGS_DATABASE": "PostgresSQL", + "BRIGHTER_TRANSPORT": "RabbitMQ" } }, "ProductionMsSql": { @@ -38,7 +41,8 @@ "applicationUrl": "https://localhost:5001;http://localhost:5000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Production", - "BRIGHTER_GREETINGS_DATABASE": "MsSQL" + "BRIGHTER_GREETINGS_DATABASE": "MsSQL", + "BRIGHTER_TRANSPORT": "RabbitMQ" } } } diff --git a/samples/WebAPI_Dapper/SalutationAnalytics/SalutationAnalytics.csproj b/samples/WebAPI_Dapper/SalutationAnalytics/SalutationAnalytics.csproj index e62aefe9db..6ce669fe5b 100644 --- a/samples/WebAPI_Dapper/SalutationAnalytics/SalutationAnalytics.csproj +++ b/samples/WebAPI_Dapper/SalutationAnalytics/SalutationAnalytics.csproj @@ -10,6 +10,7 @@ + From 7c86c5f82046e0521067b5706c36227d460cac2d Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Fri, 30 Jun 2023 21:30:58 +0100 Subject: [PATCH 82/89] Ensure Kafka works on sample --- .../GreetingsWeb/Database/SchemaCreation.cs | 77 ++++++++++++------- samples/WebAPI_Dapper/GreetingsWeb/Program.cs | 2 +- samples/WebAPI_Dapper/GreetingsWeb/Startup.cs | 2 +- .../Database/SchemaCreation.cs | 34 ++++---- .../SalutationAnalytics/Program.cs | 19 +++-- 5 files changed, 79 insertions(+), 55 deletions(-) diff --git a/samples/WebAPI_Dapper/GreetingsWeb/Database/SchemaCreation.cs b/samples/WebAPI_Dapper/GreetingsWeb/Database/SchemaCreation.cs index da566aebe6..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; @@ -40,6 +41,23 @@ public static IHost CheckDbIsUp(this IHost webHost) 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) { @@ -64,17 +82,6 @@ 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(DatabaseType databaseType, DbConnection conn) { @@ -110,16 +117,16 @@ private static void CreateDatabaseIfNotExists(DatabaseType databaseType, DbConne } } - 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) { @@ -138,33 +145,33 @@ private static void CreateOutbox(IConfiguration config, IWebHostEnvironment env) } } - private static void CreateOutboxDevelopment(string connectionString) + private static void CreateOutboxDevelopment(string connectionString, bool hasBinaryPayload) { - CreateOutboxSqlite(connectionString); + 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); + CreateOutboxMsSql(connectionString, hasBinaryPayload); break; case DatabaseType.Postgres: - CreateOutboxPostgres(connectionString); + CreateOutboxPostgres(connectionString, hasBinaryPayload); break; case DatabaseType.Sqlite: - CreateOutboxSqlite(connectionString); + CreateOutboxSqlite(connectionString, hasBinaryPayload); break; default: throw new InvalidOperationException("Could not create instance of Outbox for unknown Db type"); } } - private static void CreateOutboxMsSql(string connectionString) + private static void CreateOutboxMsSql(string connectionString, bool hasBinaryPayload) { using var sqlConnection = new SqlConnection(connectionString); sqlConnection.Open(); @@ -177,12 +184,12 @@ private static void CreateOutboxMsSql(string connectionString) if (exists) return; using var command = sqlConnection.CreateCommand(); - command.CommandText = SqlOutboxBuilder.GetDDL(OUTBOX_TABLE_NAME); + command.CommandText = SqlOutboxBuilder.GetDDL(OUTBOX_TABLE_NAME, hasBinaryPayload); command.ExecuteScalar(); } - private static void CreateOutboxMySql(string connectionString) + private static void CreateOutboxMySql(string connectionString, bool hasBinaryPayload) { using var sqlConnection = new MySqlConnection(connectionString); sqlConnection.Open(); @@ -195,11 +202,11 @@ private static void CreateOutboxMySql(string connectionString) 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) + private static void CreateOutboxPostgres(string connectionString, bool hasBinaryPayload) { using var sqlConnection = new NpgsqlConnection(connectionString); sqlConnection.Open(); @@ -212,11 +219,11 @@ private static void CreateOutboxPostgres(string connectionString) if (exists) return; using var command = sqlConnection.CreateCommand(); - command.CommandText = PostgreSqlOutboxBulder.GetDDL(OUTBOX_TABLE_NAME); + command.CommandText = PostgreSqlOutboxBulder.GetDDL(OUTBOX_TABLE_NAME, hasBinaryPayload); command.ExecuteScalar(); } - private static void CreateOutboxSqlite(string connectionString) + private static void CreateOutboxSqlite(string connectionString, bool hasBinaryPayload) { using var sqlConnection = new SqliteConnection(connectionString); sqlConnection.Open(); @@ -228,7 +235,7 @@ private static void CreateOutboxSqlite(string connectionString) if (reader.HasRows) return; using var command = sqlConnection.CreateCommand(); - command.CommandText = SqliteOutboxBuilder.GetDDL(OUTBOX_TABLE_NAME); + command.CommandText = SqliteOutboxBuilder.GetDDL(OUTBOX_TABLE_NAME, hasBinaryPayload); command.ExecuteScalar(); } @@ -325,5 +332,17 @@ private static DbConnection GetConnection(DatabaseType databaseType, string conn _ => 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/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/Startup.cs b/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs index d65d33e989..b6193994fe 100644 --- a/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs +++ b/samples/WebAPI_Dapper/GreetingsWeb/Startup.cs @@ -273,7 +273,7 @@ private static IAmAProducerRegistry GetKafkaProducerRegistry() { new KafkaPublication { - Topic = new RoutingKey("greeting.event"), + Topic = new RoutingKey("GreetingMade"), MessageSendMaxRetries = 3, MessageTimeoutMs = 1000, MaxInFlightRequestsPerConnection = 1, diff --git a/samples/WebAPI_Dapper/SalutationAnalytics/Database/SchemaCreation.cs b/samples/WebAPI_Dapper/SalutationAnalytics/Database/SchemaCreation.cs index c2eaeb7a87..9ddeeaf24e 100644 --- a/samples/WebAPI_Dapper/SalutationAnalytics/Database/SchemaCreation.cs +++ b/samples/WebAPI_Dapper/SalutationAnalytics/Database/SchemaCreation.cs @@ -59,7 +59,7 @@ public static IHost CreateInbox(this IHost host) return host; } - public static IHost CreateOutbox(this IHost webHost) + public static IHost CreateOutbox(this IHost webHost, bool hasBinaryMessagePayload) { using (var scope = webHost.Services.CreateScope()) { @@ -67,7 +67,7 @@ public static IHost CreateOutbox(this IHost webHost) var env = services.GetService(); var config = services.GetService(); - CreateOutbox(config, env); + CreateOutbox(config, env, hasBinaryMessagePayload); } return webHost; @@ -242,16 +242,16 @@ private static void CreateInboxPostgres(string connectionString) 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(GetDatabaseType(config), connectionString); + CreateOutboxProduction(GetDatabaseType(config), connectionString, hasBinaryMessagePayload); } catch (NpgsqlException pe) { @@ -270,33 +270,33 @@ private static void CreateOutbox(IConfiguration config, IHostEnvironment env) } } - private static void CreateOutboxDevelopment(string connectionString) + private static void CreateOutboxDevelopment(string connectionString, bool hasBinaryMessagePayload) { - CreateOutboxSqlite(connectionString); + CreateOutboxSqlite(connectionString, hasBinaryMessagePayload); } - private static void CreateOutboxProduction(DatabaseType databaseType, string connectionString) + private static void CreateOutboxProduction(DatabaseType databaseType, string connectionString, bool hasBinaryMessagePayload) { switch (databaseType) { case DatabaseType.MySql: - CreateOutboxMySql(connectionString); + CreateOutboxMySql(connectionString, hasBinaryMessagePayload); break; case DatabaseType.MsSql: - CreateOutboxMsSql(connectionString); + CreateOutboxMsSql(connectionString, hasBinaryMessagePayload); break; case DatabaseType.Postgres: - CreateOutboxPostgres(connectionString); + CreateOutboxPostgres(connectionString, hasBinaryMessagePayload); break; case DatabaseType.Sqlite: - CreateOutboxSqlite(connectionString); + CreateOutboxSqlite(connectionString, hasBinaryMessagePayload); break; default: throw new InvalidOperationException("Could not create instance of Outbox for unknown Db type"); } } - private static void CreateOutboxMsSql(string connectionString) + private static void CreateOutboxMsSql(string connectionString, bool hasBinaryMessagePayload) { using var sqlConnection = new SqlConnection(connectionString); sqlConnection.Open(); @@ -314,7 +314,7 @@ private static void CreateOutboxMsSql(string connectionString) } - private static void CreateOutboxMySql(string connectionString) + private static void CreateOutboxMySql(string connectionString, bool hasBinaryMessagePayload) { using var sqlConnection = new MySqlConnection(connectionString); sqlConnection.Open(); @@ -331,7 +331,7 @@ private static void CreateOutboxMySql(string connectionString) command.ExecuteScalar(); } - private static void CreateOutboxPostgres(string connectionString) + private static void CreateOutboxPostgres(string connectionString, bool hasBinaryMessagePayload) { using var sqlConnection = new NpgsqlConnection(connectionString); sqlConnection.Open(); @@ -348,7 +348,7 @@ private static void CreateOutboxPostgres(string connectionString) command.ExecuteScalar(); } - private static void CreateOutboxSqlite(string connectionString) + private static void CreateOutboxSqlite(string connectionString, bool hasBinaryMessagePayload) { using var sqlConnection = new SqliteConnection(connectionString); sqlConnection.Open(); @@ -360,7 +360,7 @@ private static void CreateOutboxSqlite(string connectionString) if (reader.HasRows) return; using var command = sqlConnection.CreateCommand(); - command.CommandText = SqliteOutboxBuilder.GetDDL(OUTBOX_TABLE_NAME); + command.CommandText = SqliteOutboxBuilder.GetDDL(OUTBOX_TABLE_NAME, hasBinaryMessagePayload); command.ExecuteScalar(); } diff --git a/samples/WebAPI_Dapper/SalutationAnalytics/Program.cs b/samples/WebAPI_Dapper/SalutationAnalytics/Program.cs index 2335fbcbc4..26d7119a90 100644 --- a/samples/WebAPI_Dapper/SalutationAnalytics/Program.cs +++ b/samples/WebAPI_Dapper/SalutationAnalytics/Program.cs @@ -39,10 +39,10 @@ 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; @@ -78,7 +78,7 @@ private static IHostBuilder CreateHostBuilder(string[] args) => private static void ConfigureBrighter(HostBuilderContext hostContext, IServiceCollection services) { - var messagingTransport = GetTransportType(hostContext); + var messagingTransport = GetTransportType(hostContext.Configuration[MessagingGlobals.BRIGHTER_TRANSPORT]); AddSchemaRegistryMaybe(services, messagingTransport); @@ -364,7 +364,7 @@ private static IAmAProducerRegistry GetKafkaProducerRegistry() { new KafkaPublication { - Topic = new RoutingKey("greeting.event"), + Topic = new RoutingKey("SalutationReceived"), MessageSendMaxRetries = 3, MessageTimeoutMs = 1000, MaxInFlightRequestsPerConnection = 1, @@ -443,7 +443,7 @@ private static Subscription[] GetKafkaSubscriptions() new KafkaSubscription( new SubscriptionName("paramore.sample.salutationanalytics"), channelName: new ChannelName("SalutationAnalytics"), - routingKey: new RoutingKey("greeting.event"), + routingKey: new RoutingKey("GreetingMade"), groupId: "kafka-GreetingsReceiverConsole-Sample", timeoutInMilliseconds: 100, offsetDefault: AutoOffsetReset.Earliest, @@ -454,9 +454,9 @@ private static Subscription[] GetKafkaSubscriptions() return subscriptions; } - private static MessagingTransport GetTransportType(HostBuilderContext hostContext) + private static MessagingTransport GetTransportType(string brighterTransport) { - return hostContext.Configuration[MessagingGlobals.BRIGHTER_TRANSPORT] switch + return brighterTransport switch { MessagingGlobals.RMQ => MessagingTransport.Rmq, MessagingGlobals.KAFKA => MessagingTransport.Kafka, @@ -464,5 +464,10 @@ private static MessagingTransport GetTransportType(HostBuilderContext hostContex "Messaging transport is not supported") }; } + + private static bool HasBinaryMessagePayload() + { + return GetTransportType(Environment.GetEnvironmentVariable("BRIGHTER_TRANSPORT")) == MessagingTransport.Kafka; + } } } From b33f40c043f62f5c16e76eafc4e1b6db55cee1b8 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Fri, 30 Jun 2023 21:32:23 +0100 Subject: [PATCH 83/89] Merged Kafka project, no longer needed --- .../.idea/httpRequests/http-requests-log.http | 262 +++++++------- Brighter.sln | 183 ---------- .../GreetingsEntities/Greeting.cs | 38 -- .../GreetingsEntities.csproj | 7 - .../GreetingsEntities/Person.cs | 26 -- .../GreetingsPorts/GreetingsPorts.csproj | 20 -- .../Handlers/AddGreetingHandlerAsync.cs | 89 ----- .../Handlers/AddPersonHandlerAsync.cs | 29 -- .../Handlers/DeletePersonHandlerAsync.cs | 72 ---- .../FIndGreetingsForPersonHandlerAsync.cs | 65 ---- .../Handlers/FindPersonByNameHandlerAsync.cs | 36 -- .../Policies/GreetingsPolicy.cs | 20 -- .../GreetingsPorts/Policies/Retry.cs | 44 --- .../GreetingsPorts/Requests/AddGreeting.cs | 18 - .../GreetingsPorts/Requests/AddPerson.cs | 16 - .../GreetingsPorts/Requests/DeletePerson.cs | 16 - .../Requests/FindGreetingsForPerson.cs | 15 - .../Requests/FindPersonByName.cs | 15 - .../GreetingsPorts/Requests/GreetingMade.cs | 15 - .../Responses/FindPersonResult.cs | 14 - .../Responses/FindPersonsGreetings.cs | 22 -- .../Controllers/GreetingsController.cs | 48 --- .../Controllers/PeopleController.cs | 60 ---- .../GreetingsWeb/Database/DatabaseType.cs | 20 -- .../GreetingsWeb/Database/OutboxExtensions.cs | 78 ----- .../GreetingsWeb/Database/SchemaCreation.cs | 330 ------------------ .../GreetingsWeb/Dockerfile | 10 - .../GreetingsWeb/GreetingsWeb.csproj | 66 ---- .../Mappers/GreetingMadeMessageMapper.cs | 46 --- .../GreetingsWeb/Models/NewGreeting.cs | 7 - .../GreetingsWeb/Models/NewPerson.cs | 7 - .../GreetingsWeb/Program.cs | 56 --- .../Properties/launchSettings.json | 57 --- .../GreetingsWeb/Startup.cs | 267 -------------- .../GreetingsWeb/appsettings.Development.json | 9 - .../GreetingsWeb/appsettings.Production.json | 17 - .../GreetingsWeb/appsettings.json | 10 - .../Greetings_MsSqlMigrations.csproj | 13 - .../Migrations/202306041332_InitialCreate.cs | 30 -- .../Greetings_MySqlMigrations.csproj | 14 - .../Migrations/20220527_InitialCreate.cs | 30 -- .../Greetings_PostgreSqlMigrations.csproj | 13 - .../Migrations/202306031734_InitialCreate.cs | 30 -- .../Greetings_SqliteMigrations.csproj | 13 - .../Migrations/202204221833_InitialCreate.cs | 30 -- .../Database/DatabaseType.cs | 20 -- .../Database/SchemaCreation.cs | 229 ------------ .../SalutationAnalytics/Dockerfile | 7 - .../Mappers/GreetingMadeMessageMapper.cs | 34 -- .../SalutationReceivedMessageMapper.cs | 22 -- .../SalutationAnalytics/Program.cs | 242 ------------- .../Properties/launchSettings.json | 19 - .../SalutationAnalytics.csproj | 42 --- .../appsettings.Development.json | 9 - .../appsettings.Production.json | 13 - .../SalutationAnalytics/appsettings.json | 9 - .../SalutationEntities/Salutation.cs | 16 - .../SalutationEntities.csproj | 7 - .../Handlers/GreetingMadeHandler.cs | 64 ---- .../SalutationPorts/Policies/Retry.cs | 27 -- .../Policies/SalutationPolicy.cs | 18 - .../SalutationPorts/Requests/GreetingMade.cs | 15 - .../Requests/SalutationReceived.cs | 15 - .../SalutationPorts/SalutationPorts.csproj | 17 - .../202205161812_SqliteMigrations.cs | 20 -- .../Salutations_SqliteMigrations.csproj | 13 - .../Migrations/20220527_MySqlMigrations.cs | 20 -- .../Salutations_mySqlMigrations.csproj | 13 - 68 files changed, 121 insertions(+), 3053 deletions(-) delete mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsEntities/Greeting.cs delete mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsEntities/GreetingsEntities.csproj delete mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsEntities/Person.cs delete mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsPorts/GreetingsPorts.csproj delete mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs delete mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/AddPersonHandlerAsync.cs delete mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/DeletePersonHandlerAsync.cs delete mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/FIndGreetingsForPersonHandlerAsync.cs delete mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/FindPersonByNameHandlerAsync.cs delete mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsPorts/Policies/GreetingsPolicy.cs delete mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsPorts/Policies/Retry.cs delete mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsPorts/Requests/AddGreeting.cs delete mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsPorts/Requests/AddPerson.cs delete mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsPorts/Requests/DeletePerson.cs delete mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsPorts/Requests/FindGreetingsForPerson.cs delete mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsPorts/Requests/FindPersonByName.cs delete mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsPorts/Requests/GreetingMade.cs delete mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsPorts/Responses/FindPersonResult.cs delete mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsPorts/Responses/FindPersonsGreetings.cs delete mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsWeb/Controllers/GreetingsController.cs delete mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsWeb/Controllers/PeopleController.cs delete mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/DatabaseType.cs delete mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/OutboxExtensions.cs delete mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/SchemaCreation.cs delete mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsWeb/Dockerfile delete mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsWeb/GreetingsWeb.csproj delete mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsWeb/Mappers/GreetingMadeMessageMapper.cs delete mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsWeb/Models/NewGreeting.cs delete mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsWeb/Models/NewPerson.cs delete mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsWeb/Program.cs delete mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsWeb/Properties/launchSettings.json delete mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs delete mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsWeb/appsettings.Development.json delete mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsWeb/appsettings.Production.json delete mode 100644 samples/WebAPI_Dapper_Kafka/GreetingsWeb/appsettings.json delete mode 100644 samples/WebAPI_Dapper_Kafka/Greetings_MsSqlMigrations/Greetings_MsSqlMigrations.csproj delete mode 100644 samples/WebAPI_Dapper_Kafka/Greetings_MsSqlMigrations/Migrations/202306041332_InitialCreate.cs delete mode 100644 samples/WebAPI_Dapper_Kafka/Greetings_MySqlMigrations/Greetings_MySqlMigrations.csproj delete mode 100644 samples/WebAPI_Dapper_Kafka/Greetings_MySqlMigrations/Migrations/20220527_InitialCreate.cs delete mode 100644 samples/WebAPI_Dapper_Kafka/Greetings_PostgreSqlMigrations/Greetings_PostgreSqlMigrations.csproj delete mode 100644 samples/WebAPI_Dapper_Kafka/Greetings_PostgreSqlMigrations/Migrations/202306031734_InitialCreate.cs delete mode 100644 samples/WebAPI_Dapper_Kafka/Greetings_SqliteMigrations/Greetings_SqliteMigrations.csproj delete mode 100644 samples/WebAPI_Dapper_Kafka/Greetings_SqliteMigrations/Migrations/202204221833_InitialCreate.cs delete mode 100644 samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Database/DatabaseType.cs delete mode 100644 samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Database/SchemaCreation.cs delete mode 100644 samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Dockerfile delete mode 100644 samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Mappers/GreetingMadeMessageMapper.cs delete mode 100644 samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Mappers/SalutationReceivedMessageMapper.cs delete mode 100644 samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Program.cs delete mode 100644 samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Properties/launchSettings.json delete mode 100644 samples/WebAPI_Dapper_Kafka/SalutationAnalytics/SalutationAnalytics.csproj delete mode 100644 samples/WebAPI_Dapper_Kafka/SalutationAnalytics/appsettings.Development.json delete mode 100644 samples/WebAPI_Dapper_Kafka/SalutationAnalytics/appsettings.Production.json delete mode 100644 samples/WebAPI_Dapper_Kafka/SalutationAnalytics/appsettings.json delete mode 100644 samples/WebAPI_Dapper_Kafka/SalutationEntities/Salutation.cs delete mode 100644 samples/WebAPI_Dapper_Kafka/SalutationEntities/SalutationEntities.csproj delete mode 100644 samples/WebAPI_Dapper_Kafka/SalutationPorts/Handlers/GreetingMadeHandler.cs delete mode 100644 samples/WebAPI_Dapper_Kafka/SalutationPorts/Policies/Retry.cs delete mode 100644 samples/WebAPI_Dapper_Kafka/SalutationPorts/Policies/SalutationPolicy.cs delete mode 100644 samples/WebAPI_Dapper_Kafka/SalutationPorts/Requests/GreetingMade.cs delete mode 100644 samples/WebAPI_Dapper_Kafka/SalutationPorts/Requests/SalutationReceived.cs delete mode 100644 samples/WebAPI_Dapper_Kafka/SalutationPorts/SalutationPorts.csproj delete mode 100644 samples/WebAPI_Dapper_Kafka/Salutations_SqliteMigrations/Migrations/202205161812_SqliteMigrations.cs delete mode 100644 samples/WebAPI_Dapper_Kafka/Salutations_SqliteMigrations/Salutations_SqliteMigrations.csproj delete mode 100644 samples/WebAPI_Dapper_Kafka/Salutations_mySqlMigrations/Migrations/20220527_MySqlMigrations.cs delete mode 100644 samples/WebAPI_Dapper_Kafka/Salutations_mySqlMigrations/Salutations_mySqlMigrations.csproj diff --git a/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http b/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http index 31959997fb..4301fc9a67 100644 --- a/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http +++ b/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http @@ -1,15 +1,9 @@ -POST http://localhost:5000/Greetings/Tyrion/new -Content-Type: application/json -Content-Length: 47 +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-06-27T091728.200.json +<> 2023-06-30T204010.200.json ### @@ -24,22 +18,16 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-27T091727.200.json +<> 2023-06-30T204007.200.json ### -POST http://localhost:5000/Greetings/Tyrion/new -Content-Type: application/json -Content-Length: 47 +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-06-27T091726-1.200.json +<> 2023-06-30T203955.200.json ### @@ -54,22 +42,31 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-27T091726.200.json +<> 2023-06-30T203951.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-06-30T203938.200.json + +### + +POST http://localhost:5000/People/new Content-Type: application/json -Content-Length: 47 +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" } -<> 2023-06-27T091721.200.json +<> 2023-06-30T203936.200.json ### @@ -78,16 +75,22 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-06-27T091718.200.json +<> 2023-06-30T203932.404.json ### -GET http://localhost:5000/Greetings/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 -<> 2023-06-27T090113.200.json +{ + "Greeting" : "I drink, and I know things" +} + +<> 2023-06-28T192431.200.json ### @@ -102,7 +105,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-27T090110.200.json +<> 2023-06-28T192225.200.json ### @@ -117,7 +120,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-27T090108.200.json +<> 2023-06-28T190130.200.json ### @@ -132,8 +135,6 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-27T090104.200.json - ### POST http://localhost:5000/Greetings/Tyrion/new @@ -147,31 +148,42 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-27T090100.200.json - ### -GET http://localhost:5000/Greetings/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 -<> 2023-06-27T090051.200.json +{ + "Greeting" : "I drink, and I know things" +} ### -POST http://localhost:5000/People/new +POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json -Content-Length: 23 +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" } -<> 2023-06-27T090042.200.json +<> 2023-06-28T182132.200.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 + +<> 2023-06-28T182115.200.json ### @@ -180,7 +192,7 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-06-27T090039.404.json +<> 2023-06-28T182112.200.json ### @@ -195,7 +207,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-27T085001.200.json +<> 2023-06-28T181931.200.json ### @@ -210,7 +222,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-27T085000.200.json +<> 2023-06-28T181824.200.json ### @@ -225,22 +237,16 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-27T084959.200.json +<> 2023-06-28T180308.200.json ### -POST http://localhost:5000/Greetings/Tyrion/new -Content-Type: application/json -Content-Length: 47 +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-06-27T084958.200.json +<> 2023-06-28T180149.200.json ### @@ -255,7 +261,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-27T084956.200.json +<> 2023-06-28T180148.200.json ### @@ -264,54 +270,52 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-06-27T084954.200.json +<> 2023-06-28T180142.200.json ### -POST http://localhost:5000/People/new +POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json -Content-Length: 23 +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" } -<> 2023-06-27T084950.200.json +<> 2023-06-28T094747.200.json ### -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 -<> 2023-06-27T084946.404.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-06-27T084900.200.json +<> 2023-06-28T094744.200.json ### -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 -### +{ + "Greeting" : "I drink, and I know things" +} -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-06-28T094741.200.json ### @@ -320,6 +324,8 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip +<> 2023-06-28T094734.200.json + ### POST http://localhost:5000/Greetings/Tyrion/new @@ -333,7 +339,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-24T121016.200.json +<> 2023-06-28T083529.200.json ### @@ -348,7 +354,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-24T121015.200.json +<> 2023-06-28T083525.200.json ### @@ -363,22 +369,16 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-24T121014-1.200.json +<> 2023-06-28T083523.200.json ### -POST http://localhost:5000/Greetings/Tyrion/new -Content-Type: application/json -Content-Length: 47 +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-06-24T121014.200.json +<> 2023-06-28T083517.200.json ### @@ -393,7 +393,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-24T121013.200.json +<> 2023-06-28T080820.200.json ### @@ -408,7 +408,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-24T121012.200.json +<> 2023-06-28T080816.200.json ### @@ -423,7 +423,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-24T121011.200.json +<> 2023-06-28T080812.500.json ### @@ -438,22 +438,16 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-24T121010-1.200.json +<> 2023-06-28T080809.200.json ### -POST http://localhost:5000/Greetings/Tyrion/new -Content-Type: application/json -Content-Length: 47 +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-06-24T121010.200.json +<> 2023-06-28T080804.200.json ### @@ -468,8 +462,6 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-24T121008.200.json - ### GET http://localhost:5000/Greetings/Tyrion @@ -477,16 +469,7 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-06-24T121005.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-06-24T121002.200.json +<> 2023-06-27T152459.200.json ### @@ -501,7 +484,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-24T093617.200.json +<> 2023-06-27T152457.200.json ### @@ -516,7 +499,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-24T093616.200.json +<> 2023-06-27T152456.200.json ### @@ -531,37 +514,40 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-24T093614.200.json +<> 2023-06-27T152453.200.json ### -POST http://localhost:5000/Greetings/Tyrion/new -Content-Type: application/json -Content-Length: 47 +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-06-27T152451.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-06-24T093611.500.json +<> 2023-06-27T152446.200.json ### -POST http://localhost:5000/Greetings/Tyrion/new +POST http://localhost:5000/People/new Content-Type: application/json -Content-Length: 47 +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" } -<> 2023-06-24T093606.200.json +<> 2023-06-27T152442.200.json ### @@ -576,16 +562,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-24T093605.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-06-24T093600.200.json +<> 2023-06-27T091728.200.json ### @@ -600,7 +577,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-24T091635-1.200.json +<> 2023-06-27T091727.200.json ### @@ -615,7 +592,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-24T091635.200.json +<> 2023-06-27T091726-1.200.json ### @@ -630,7 +607,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-24T091634.200.json +<> 2023-06-27T091726.200.json ### @@ -645,22 +622,25 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-24T091632.200.json +<> 2023-06-27T091721.200.json ### -POST http://localhost:5000/Greetings/Tyrion/new -Content-Type: application/json -Content-Length: 47 +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-06-27T091718.200.json -<> 2023-06-24T091631.200.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 + +<> 2023-06-27T090113.200.json ### diff --git a/Brighter.sln b/Brighter.sln index d27ef3aa57..85583cf9c8 100644 --- a/Brighter.sln +++ b/Brighter.sln @@ -330,32 +330,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Paramore.Brighter.Azure.Tes EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Paramore.Brighter.Archive.Azure", "src\Paramore.Brighter.Archive.Azure\Paramore.Brighter.Archive.Azure.csproj", "{F329B6C6-40C2-45BA-A2A8-276ACAFA1867}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "WebAPI_Dapper_Kafka", "WebAPI_Dapper_Kafka", "{36612AF5-23DB-46C0-9EB5-0F21911741F3}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GreetingsEntities", "samples\WebAPI_Dapper_Kafka\GreetingsEntities\GreetingsEntities.csproj", "{86AF8EFE-E3E0-46BF-88EB-F9096DAB2DC3}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GreetingsPorts", "samples\WebAPI_Dapper_Kafka\GreetingsPorts\GreetingsPorts.csproj", "{83F3CA28-B7DD-4BF0-94B3-F0D35DF55B7A}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GreetingsWeb", "samples\WebAPI_Dapper_Kafka\GreetingsWeb\GreetingsWeb.csproj", "{9E311E12-A0CE-4994-92B2-80F3A4BAEB24}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Greetings_SqliteMigrations", "samples\WebAPI_Dapper_Kafka\Greetings_SqliteMigrations\Greetings_SqliteMigrations.csproj", "{80375113-CC4F-4F04-AEC0-97FD8F5887CE}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SalutationEntities", "samples\WebAPI_Dapper_Kafka\SalutationEntities\SalutationEntities.csproj", "{B1672D09-15A1-4722-8E63-529E201DC6F6}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SalutationPorts", "samples\WebAPI_Dapper_Kafka\SalutationPorts\SalutationPorts.csproj", "{2DBA8A1A-76AC-4F6F-9533-2977AAD9DB15}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SalutationAnalytics", "samples\WebAPI_Dapper_Kafka\SalutationAnalytics\SalutationAnalytics.csproj", "{559212C0-0912-4586-AD6B-462E7A0E738E}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Salutations_SqliteMigrations", "samples\WebAPI_Dapper_Kafka\Salutations_SqliteMigrations\Salutations_SqliteMigrations.csproj", "{B50A44CE-5F54-4559-90E0-1AF81655E944}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Salutations_mySqlMigrations", "samples\WebAPI_Dapper_Kafka\Salutations_mySqlMigrations\Salutations_mySqlMigrations.csproj", "{E66CA4D7-8339-4674-B102-BC7CA890B7C5}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Greetings_MySqlMigrations", "samples\WebAPI_Dapper_Kafka\Greetings_MySqlMigrations\Greetings_MySqlMigrations.csproj", "{2FADCED2-8563-4261-9AA8-514E5888075F}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Greetings_PostgreSqlMigrations", "samples\WebAPI_Dapper_Kafka\Greetings_PostgreSqlMigrations\Greetings_PostgreSqlMigrations.csproj", "{B64979D7-5FAD-4C5E-BCFC-B7FFA9C962C4}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Greetings_MsSqlMigrations", "samples\WebAPI_Dapper_Kafka\Greetings_MsSqlMigrations\Greetings_MsSqlMigrations.csproj", "{2B11781D-13B0-4602-A6B4-96881F670CC0}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1914,150 +1888,6 @@ Global {EC046F36-F93F-447A-86EA-F60585232867}.Release|Mixed Platforms.Build.0 = Release|Any CPU {EC046F36-F93F-447A-86EA-F60585232867}.Release|x86.ActiveCfg = Release|Any CPU {EC046F36-F93F-447A-86EA-F60585232867}.Release|x86.Build.0 = Release|Any CPU - {86AF8EFE-E3E0-46BF-88EB-F9096DAB2DC3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {86AF8EFE-E3E0-46BF-88EB-F9096DAB2DC3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {86AF8EFE-E3E0-46BF-88EB-F9096DAB2DC3}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {86AF8EFE-E3E0-46BF-88EB-F9096DAB2DC3}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {86AF8EFE-E3E0-46BF-88EB-F9096DAB2DC3}.Debug|x86.ActiveCfg = Debug|Any CPU - {86AF8EFE-E3E0-46BF-88EB-F9096DAB2DC3}.Debug|x86.Build.0 = Debug|Any CPU - {86AF8EFE-E3E0-46BF-88EB-F9096DAB2DC3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {86AF8EFE-E3E0-46BF-88EB-F9096DAB2DC3}.Release|Any CPU.Build.0 = Release|Any CPU - {86AF8EFE-E3E0-46BF-88EB-F9096DAB2DC3}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {86AF8EFE-E3E0-46BF-88EB-F9096DAB2DC3}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {86AF8EFE-E3E0-46BF-88EB-F9096DAB2DC3}.Release|x86.ActiveCfg = Release|Any CPU - {86AF8EFE-E3E0-46BF-88EB-F9096DAB2DC3}.Release|x86.Build.0 = Release|Any CPU - {83F3CA28-B7DD-4BF0-94B3-F0D35DF55B7A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {83F3CA28-B7DD-4BF0-94B3-F0D35DF55B7A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {83F3CA28-B7DD-4BF0-94B3-F0D35DF55B7A}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {83F3CA28-B7DD-4BF0-94B3-F0D35DF55B7A}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {83F3CA28-B7DD-4BF0-94B3-F0D35DF55B7A}.Debug|x86.ActiveCfg = Debug|Any CPU - {83F3CA28-B7DD-4BF0-94B3-F0D35DF55B7A}.Debug|x86.Build.0 = Debug|Any CPU - {83F3CA28-B7DD-4BF0-94B3-F0D35DF55B7A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {83F3CA28-B7DD-4BF0-94B3-F0D35DF55B7A}.Release|Any CPU.Build.0 = Release|Any CPU - {83F3CA28-B7DD-4BF0-94B3-F0D35DF55B7A}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {83F3CA28-B7DD-4BF0-94B3-F0D35DF55B7A}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {83F3CA28-B7DD-4BF0-94B3-F0D35DF55B7A}.Release|x86.ActiveCfg = Release|Any CPU - {83F3CA28-B7DD-4BF0-94B3-F0D35DF55B7A}.Release|x86.Build.0 = Release|Any CPU - {9E311E12-A0CE-4994-92B2-80F3A4BAEB24}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9E311E12-A0CE-4994-92B2-80F3A4BAEB24}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9E311E12-A0CE-4994-92B2-80F3A4BAEB24}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {9E311E12-A0CE-4994-92B2-80F3A4BAEB24}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {9E311E12-A0CE-4994-92B2-80F3A4BAEB24}.Debug|x86.ActiveCfg = Debug|Any CPU - {9E311E12-A0CE-4994-92B2-80F3A4BAEB24}.Debug|x86.Build.0 = Debug|Any CPU - {9E311E12-A0CE-4994-92B2-80F3A4BAEB24}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9E311E12-A0CE-4994-92B2-80F3A4BAEB24}.Release|Any CPU.Build.0 = Release|Any CPU - {9E311E12-A0CE-4994-92B2-80F3A4BAEB24}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {9E311E12-A0CE-4994-92B2-80F3A4BAEB24}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {9E311E12-A0CE-4994-92B2-80F3A4BAEB24}.Release|x86.ActiveCfg = Release|Any CPU - {9E311E12-A0CE-4994-92B2-80F3A4BAEB24}.Release|x86.Build.0 = Release|Any CPU - {80375113-CC4F-4F04-AEC0-97FD8F5887CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {80375113-CC4F-4F04-AEC0-97FD8F5887CE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {80375113-CC4F-4F04-AEC0-97FD8F5887CE}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {80375113-CC4F-4F04-AEC0-97FD8F5887CE}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {80375113-CC4F-4F04-AEC0-97FD8F5887CE}.Debug|x86.ActiveCfg = Debug|Any CPU - {80375113-CC4F-4F04-AEC0-97FD8F5887CE}.Debug|x86.Build.0 = Debug|Any CPU - {80375113-CC4F-4F04-AEC0-97FD8F5887CE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {80375113-CC4F-4F04-AEC0-97FD8F5887CE}.Release|Any CPU.Build.0 = Release|Any CPU - {80375113-CC4F-4F04-AEC0-97FD8F5887CE}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {80375113-CC4F-4F04-AEC0-97FD8F5887CE}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {80375113-CC4F-4F04-AEC0-97FD8F5887CE}.Release|x86.ActiveCfg = Release|Any CPU - {80375113-CC4F-4F04-AEC0-97FD8F5887CE}.Release|x86.Build.0 = Release|Any CPU - {B1672D09-15A1-4722-8E63-529E201DC6F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B1672D09-15A1-4722-8E63-529E201DC6F6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B1672D09-15A1-4722-8E63-529E201DC6F6}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {B1672D09-15A1-4722-8E63-529E201DC6F6}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {B1672D09-15A1-4722-8E63-529E201DC6F6}.Debug|x86.ActiveCfg = Debug|Any CPU - {B1672D09-15A1-4722-8E63-529E201DC6F6}.Debug|x86.Build.0 = Debug|Any CPU - {B1672D09-15A1-4722-8E63-529E201DC6F6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B1672D09-15A1-4722-8E63-529E201DC6F6}.Release|Any CPU.Build.0 = Release|Any CPU - {B1672D09-15A1-4722-8E63-529E201DC6F6}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {B1672D09-15A1-4722-8E63-529E201DC6F6}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {B1672D09-15A1-4722-8E63-529E201DC6F6}.Release|x86.ActiveCfg = Release|Any CPU - {B1672D09-15A1-4722-8E63-529E201DC6F6}.Release|x86.Build.0 = Release|Any CPU - {2DBA8A1A-76AC-4F6F-9533-2977AAD9DB15}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2DBA8A1A-76AC-4F6F-9533-2977AAD9DB15}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2DBA8A1A-76AC-4F6F-9533-2977AAD9DB15}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {2DBA8A1A-76AC-4F6F-9533-2977AAD9DB15}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {2DBA8A1A-76AC-4F6F-9533-2977AAD9DB15}.Debug|x86.ActiveCfg = Debug|Any CPU - {2DBA8A1A-76AC-4F6F-9533-2977AAD9DB15}.Debug|x86.Build.0 = Debug|Any CPU - {2DBA8A1A-76AC-4F6F-9533-2977AAD9DB15}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2DBA8A1A-76AC-4F6F-9533-2977AAD9DB15}.Release|Any CPU.Build.0 = Release|Any CPU - {2DBA8A1A-76AC-4F6F-9533-2977AAD9DB15}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {2DBA8A1A-76AC-4F6F-9533-2977AAD9DB15}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {2DBA8A1A-76AC-4F6F-9533-2977AAD9DB15}.Release|x86.ActiveCfg = Release|Any CPU - {2DBA8A1A-76AC-4F6F-9533-2977AAD9DB15}.Release|x86.Build.0 = Release|Any CPU - {559212C0-0912-4586-AD6B-462E7A0E738E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {559212C0-0912-4586-AD6B-462E7A0E738E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {559212C0-0912-4586-AD6B-462E7A0E738E}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {559212C0-0912-4586-AD6B-462E7A0E738E}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {559212C0-0912-4586-AD6B-462E7A0E738E}.Debug|x86.ActiveCfg = Debug|Any CPU - {559212C0-0912-4586-AD6B-462E7A0E738E}.Debug|x86.Build.0 = Debug|Any CPU - {559212C0-0912-4586-AD6B-462E7A0E738E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {559212C0-0912-4586-AD6B-462E7A0E738E}.Release|Any CPU.Build.0 = Release|Any CPU - {559212C0-0912-4586-AD6B-462E7A0E738E}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {559212C0-0912-4586-AD6B-462E7A0E738E}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {559212C0-0912-4586-AD6B-462E7A0E738E}.Release|x86.ActiveCfg = Release|Any CPU - {559212C0-0912-4586-AD6B-462E7A0E738E}.Release|x86.Build.0 = Release|Any CPU - {B50A44CE-5F54-4559-90E0-1AF81655E944}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B50A44CE-5F54-4559-90E0-1AF81655E944}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B50A44CE-5F54-4559-90E0-1AF81655E944}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {B50A44CE-5F54-4559-90E0-1AF81655E944}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {B50A44CE-5F54-4559-90E0-1AF81655E944}.Debug|x86.ActiveCfg = Debug|Any CPU - {B50A44CE-5F54-4559-90E0-1AF81655E944}.Debug|x86.Build.0 = Debug|Any CPU - {B50A44CE-5F54-4559-90E0-1AF81655E944}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B50A44CE-5F54-4559-90E0-1AF81655E944}.Release|Any CPU.Build.0 = Release|Any CPU - {B50A44CE-5F54-4559-90E0-1AF81655E944}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {B50A44CE-5F54-4559-90E0-1AF81655E944}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {B50A44CE-5F54-4559-90E0-1AF81655E944}.Release|x86.ActiveCfg = Release|Any CPU - {B50A44CE-5F54-4559-90E0-1AF81655E944}.Release|x86.Build.0 = Release|Any CPU - {E66CA4D7-8339-4674-B102-BC7CA890B7C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E66CA4D7-8339-4674-B102-BC7CA890B7C5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E66CA4D7-8339-4674-B102-BC7CA890B7C5}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {E66CA4D7-8339-4674-B102-BC7CA890B7C5}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {E66CA4D7-8339-4674-B102-BC7CA890B7C5}.Debug|x86.ActiveCfg = Debug|Any CPU - {E66CA4D7-8339-4674-B102-BC7CA890B7C5}.Debug|x86.Build.0 = Debug|Any CPU - {E66CA4D7-8339-4674-B102-BC7CA890B7C5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E66CA4D7-8339-4674-B102-BC7CA890B7C5}.Release|Any CPU.Build.0 = Release|Any CPU - {E66CA4D7-8339-4674-B102-BC7CA890B7C5}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {E66CA4D7-8339-4674-B102-BC7CA890B7C5}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {E66CA4D7-8339-4674-B102-BC7CA890B7C5}.Release|x86.ActiveCfg = Release|Any CPU - {E66CA4D7-8339-4674-B102-BC7CA890B7C5}.Release|x86.Build.0 = Release|Any CPU - {2FADCED2-8563-4261-9AA8-514E5888075F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2FADCED2-8563-4261-9AA8-514E5888075F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2FADCED2-8563-4261-9AA8-514E5888075F}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {2FADCED2-8563-4261-9AA8-514E5888075F}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {2FADCED2-8563-4261-9AA8-514E5888075F}.Debug|x86.ActiveCfg = Debug|Any CPU - {2FADCED2-8563-4261-9AA8-514E5888075F}.Debug|x86.Build.0 = Debug|Any CPU - {2FADCED2-8563-4261-9AA8-514E5888075F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2FADCED2-8563-4261-9AA8-514E5888075F}.Release|Any CPU.Build.0 = Release|Any CPU - {2FADCED2-8563-4261-9AA8-514E5888075F}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {2FADCED2-8563-4261-9AA8-514E5888075F}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {2FADCED2-8563-4261-9AA8-514E5888075F}.Release|x86.ActiveCfg = Release|Any CPU - {2FADCED2-8563-4261-9AA8-514E5888075F}.Release|x86.Build.0 = Release|Any CPU - {B64979D7-5FAD-4C5E-BCFC-B7FFA9C962C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B64979D7-5FAD-4C5E-BCFC-B7FFA9C962C4}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B64979D7-5FAD-4C5E-BCFC-B7FFA9C962C4}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {B64979D7-5FAD-4C5E-BCFC-B7FFA9C962C4}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {B64979D7-5FAD-4C5E-BCFC-B7FFA9C962C4}.Debug|x86.ActiveCfg = Debug|Any CPU - {B64979D7-5FAD-4C5E-BCFC-B7FFA9C962C4}.Debug|x86.Build.0 = Debug|Any CPU - {B64979D7-5FAD-4C5E-BCFC-B7FFA9C962C4}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B64979D7-5FAD-4C5E-BCFC-B7FFA9C962C4}.Release|Any CPU.Build.0 = Release|Any CPU - {B64979D7-5FAD-4C5E-BCFC-B7FFA9C962C4}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {B64979D7-5FAD-4C5E-BCFC-B7FFA9C962C4}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {B64979D7-5FAD-4C5E-BCFC-B7FFA9C962C4}.Release|x86.ActiveCfg = Release|Any CPU - {B64979D7-5FAD-4C5E-BCFC-B7FFA9C962C4}.Release|x86.Build.0 = Release|Any CPU - {2B11781D-13B0-4602-A6B4-96881F670CC0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2B11781D-13B0-4602-A6B4-96881F670CC0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2B11781D-13B0-4602-A6B4-96881F670CC0}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {2B11781D-13B0-4602-A6B4-96881F670CC0}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {2B11781D-13B0-4602-A6B4-96881F670CC0}.Debug|x86.ActiveCfg = Debug|Any CPU - {2B11781D-13B0-4602-A6B4-96881F670CC0}.Debug|x86.Build.0 = Debug|Any CPU - {2B11781D-13B0-4602-A6B4-96881F670CC0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2B11781D-13B0-4602-A6B4-96881F670CC0}.Release|Any CPU.Build.0 = Release|Any CPU - {2B11781D-13B0-4602-A6B4-96881F670CC0}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {2B11781D-13B0-4602-A6B4-96881F670CC0}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {2B11781D-13B0-4602-A6B4-96881F670CC0}.Release|x86.ActiveCfg = Release|Any CPU - {2B11781D-13B0-4602-A6B4-96881F670CC0}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -2171,19 +2001,6 @@ Global {D7361C21-AB4D-4E82-8094-FB86C2ED1800} = {65F8C2DE-0CB6-4102-8187-A247F1D5D3D7} {18742337-075A-40D6-B67F-91F5894A50C3} = {65F8C2DE-0CB6-4102-8187-A247F1D5D3D7} {AA2AA086-9B8A-4910-A793-E92B1E352351} = {329736D2-BF92-4D06-A7BF-19F4B6B64EDD} - {36612AF5-23DB-46C0-9EB5-0F21911741F3} = {235DE1F1-E71B-4817-8E27-3B34FF006E4C} - {86AF8EFE-E3E0-46BF-88EB-F9096DAB2DC3} = {36612AF5-23DB-46C0-9EB5-0F21911741F3} - {83F3CA28-B7DD-4BF0-94B3-F0D35DF55B7A} = {36612AF5-23DB-46C0-9EB5-0F21911741F3} - {9E311E12-A0CE-4994-92B2-80F3A4BAEB24} = {36612AF5-23DB-46C0-9EB5-0F21911741F3} - {80375113-CC4F-4F04-AEC0-97FD8F5887CE} = {36612AF5-23DB-46C0-9EB5-0F21911741F3} - {B1672D09-15A1-4722-8E63-529E201DC6F6} = {36612AF5-23DB-46C0-9EB5-0F21911741F3} - {2DBA8A1A-76AC-4F6F-9533-2977AAD9DB15} = {36612AF5-23DB-46C0-9EB5-0F21911741F3} - {559212C0-0912-4586-AD6B-462E7A0E738E} = {36612AF5-23DB-46C0-9EB5-0F21911741F3} - {B50A44CE-5F54-4559-90E0-1AF81655E944} = {36612AF5-23DB-46C0-9EB5-0F21911741F3} - {E66CA4D7-8339-4674-B102-BC7CA890B7C5} = {36612AF5-23DB-46C0-9EB5-0F21911741F3} - {2FADCED2-8563-4261-9AA8-514E5888075F} = {36612AF5-23DB-46C0-9EB5-0F21911741F3} - {B64979D7-5FAD-4C5E-BCFC-B7FFA9C962C4} = {36612AF5-23DB-46C0-9EB5-0F21911741F3} - {2B11781D-13B0-4602-A6B4-96881F670CC0} = {36612AF5-23DB-46C0-9EB5-0F21911741F3} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {8B7C7E31-2E32-4E0D-9426-BC9AF22E9F4C} diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsEntities/Greeting.cs b/samples/WebAPI_Dapper_Kafka/GreetingsEntities/Greeting.cs deleted file mode 100644 index 0b73f5bdfd..0000000000 --- a/samples/WebAPI_Dapper_Kafka/GreetingsEntities/Greeting.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Runtime.CompilerServices; - -namespace GreetingsEntities -{ - public class Greeting - { - public long Id { get; set; } - public string Message { get; set; } - //public Person Recipient { get; set; } - public long RecipientId { get; set; } - - public Greeting() { /*Required by Dapperextensions*/} - - public Greeting(string message) - { - Message = message; - } - - public Greeting(string message, Person recipient) - { - Message = message; - RecipientId = recipient.Id; - } - - public Greeting(int id, string message, Person recipient) - { - Id = id; - Message = message; - RecipientId = recipient.Id; - } - - public string Greet() - { - return $"{Message}!"; - } - } -} diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsEntities/GreetingsEntities.csproj b/samples/WebAPI_Dapper_Kafka/GreetingsEntities/GreetingsEntities.csproj deleted file mode 100644 index 4f444d8c8b..0000000000 --- a/samples/WebAPI_Dapper_Kafka/GreetingsEntities/GreetingsEntities.csproj +++ /dev/null @@ -1,7 +0,0 @@ - - - - net6.0 - - - diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsEntities/Person.cs b/samples/WebAPI_Dapper_Kafka/GreetingsEntities/Person.cs deleted file mode 100644 index 10dce975d4..0000000000 --- a/samples/WebAPI_Dapper_Kafka/GreetingsEntities/Person.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace GreetingsEntities -{ - public class Person - { - public byte[] TimeStamp { get; set; } - public long Id { get; set; } - public string Name { get; set; } - public IList Greetings { get; set; } = new List(); - - public Person(){ /*Required for DapperExtensions*/} - - public Person(string name) - { - Name = name; - } - - public Person(int id, string name) - { - Id = id; - Name = name; - } - } -} diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/GreetingsPorts.csproj b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/GreetingsPorts.csproj deleted file mode 100644 index 864e81571b..0000000000 --- a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/GreetingsPorts.csproj +++ /dev/null @@ -1,20 +0,0 @@ - - - - net6.0 - - - - - - - - - - - - - - - - diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs deleted file mode 100644 index 88e87c9c07..0000000000 --- a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs +++ /dev/null @@ -1,89 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Dapper; -using Paramore.Brighter; -using GreetingsEntities; -using GreetingsPorts.Requests; -using Microsoft.Extensions.Logging; -using Paramore.Brighter.Logging.Attributes; -using Paramore.Brighter.Policies.Attributes; - -namespace GreetingsPorts.Handlers -{ - public class AddGreetingHandlerAsync: RequestHandlerAsync - { - private readonly IAmACommandProcessor _postBox; - private readonly ILogger _logger; - private readonly IAmATransactionConnectionProvider _transactionProvider; - - - public AddGreetingHandlerAsync(IAmATransactionConnectionProvider transactionProvider, IAmACommandProcessor postBox, ILogger logger) - { - _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; - } - - [RequestLoggingAsync(0, HandlerTiming.Before)] - [UsePolicyAsync(step:1, policy: Policies.Retry.EXPONENTIAL_RETRYPOLICYASYNC)] - public override async Task HandleAsync(AddGreeting addGreeting, CancellationToken cancellationToken = default) - { - var posts = new List(); - - //We use the unit of work to grab connection and transaction, because Outbox needs - //to share them 'behind the scenes' - - var conn = await _transactionProvider.GetConnectionAsync(cancellationToken); - var tx = await _transactionProvider.GetTransactionAsync(cancellationToken); - try - { - var people = await conn.QueryAsync( - "select * from Person where name = @name", - new {name = addGreeting.Name}, - tx - ); - var person = people.SingleOrDefault(); - - 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 _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 - await _postBox.ClearOutboxAsync(posts, cancellationToken:cancellationToken); - - return await base.HandleAsync(addGreeting, cancellationToken); - } - } -} diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/AddPersonHandlerAsync.cs b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/AddPersonHandlerAsync.cs deleted file mode 100644 index f5e0d276d9..0000000000 --- a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/AddPersonHandlerAsync.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; -using Dapper; -using GreetingsPorts.Requests; -using Paramore.Brighter; -using Paramore.Brighter.Logging.Attributes; -using Paramore.Brighter.Policies.Attributes; - -namespace GreetingsPorts.Handlers -{ - public class AddPersonHandlerAsync : RequestHandlerAsync - { - private readonly IAmARelationalDbConnectionProvider _relationalDbConnectionProvider; - - public AddPersonHandlerAsync(IAmARelationalDbConnectionProvider relationalDbConnectionProvider) - { - _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 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_Kafka/GreetingsPorts/Handlers/DeletePersonHandlerAsync.cs b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/DeletePersonHandlerAsync.cs deleted file mode 100644 index fe2731a534..0000000000 --- a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/DeletePersonHandlerAsync.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Dapper; -using GreetingsEntities; -using GreetingsPorts.Requests; -using Microsoft.Extensions.Logging; -using Paramore.Brighter; -using Paramore.Brighter.Logging.Attributes; -using Paramore.Brighter.Policies.Attributes; - -namespace GreetingsPorts.Handlers -{ - public class DeletePersonHandlerAsync : RequestHandlerAsync - { - private readonly IAmARelationalDbConnectionProvider _relationalDbConnectionProvider; - private readonly ILogger _logger; - - public DeletePersonHandlerAsync(IAmARelationalDbConnectionProvider relationalDbConnectionProvider, ILogger logger) - { - _relationalDbConnectionProvider = relationalDbConnectionProvider; - _logger = logger; - } - - [RequestLoggingAsync(0, HandlerTiming.Before)] - [UsePolicyAsync(step:1, policy: Policies.Retry.EXPONENTIAL_RETRYPOLICYASYNC)] - public override async Task HandleAsync(DeletePerson deletePerson, CancellationToken cancellationToken = default) - { - 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(); - - 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); - - await tx.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); - 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_Kafka/GreetingsPorts/Handlers/FIndGreetingsForPersonHandlerAsync.cs b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/FIndGreetingsForPersonHandlerAsync.cs deleted file mode 100644 index 45752cff49..0000000000 --- a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/FIndGreetingsForPersonHandlerAsync.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Dapper; -using GreetingsEntities; -using GreetingsPorts.Policies; -using GreetingsPorts.Requests; -using GreetingsPorts.Responses; -using Paramore.Brighter; -using Paramore.Darker; -using Paramore.Darker.Policies; -using Paramore.Darker.QueryLogging; - -namespace GreetingsPorts.Handlers -{ - public class FIndGreetingsForPersonHandlerAsync : QueryHandlerAsync - { - private readonly IAmARelationalDbConnectionProvider _relationalDbConnectionProvider; - - public FIndGreetingsForPersonHandlerAsync(IAmARelationalDbConnectionProvider relationalDbConnectionProvider) - { - _relationalDbConnectionProvider = relationalDbConnectionProvider; - } - - [QueryLogging(0)] - [RetryableQuery(1, Retry.EXPONENTIAL_RETRYPOLICYASYNC)] - public override async Task ExecuteAsync(FindGreetingsForPerson query, CancellationToken cancellationToken = new CancellationToken()) - { - //Retrieving parent and child is a bit tricky with Dapper. From raw SQL We wget back a set that has a row-per-child. We need to turn that - //into one entity per parent, with a collection of children. To do that we bring everything back into memory, group by parent id and collate all - //the children for that group. - - var sql = @"select p.Id, p.Name, g.Id, g.Message - from Person p - inner join Greeting g on g.Recipient_Id = p.Id"; - await using var connection = await _relationalDbConnectionProvider.GetConnectionAsync(cancellationToken); - var people = await connection.QueryAsync(sql, (person, greeting) => - { - 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 => - { - var groupedPerson = grp.First(); - groupedPerson.Greetings = grp.Select(p => p.Greetings.Single()).ToList(); - return groupedPerson; - }); - - var person = peopleGreetings.Single(); - - return new FindPersonsGreetings - { - Name = person.Name, Greetings = person.Greetings.Select(g => new Salutation(g.Greet())) - }; - } - - } -} diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/FindPersonByNameHandlerAsync.cs b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/FindPersonByNameHandlerAsync.cs deleted file mode 100644 index 7ada5290dc..0000000000 --- a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Handlers/FindPersonByNameHandlerAsync.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Dapper; -using GreetingsEntities; -using GreetingsPorts.Policies; -using GreetingsPorts.Requests; -using GreetingsPorts.Responses; -using Paramore.Brighter; -using Paramore.Darker; -using Paramore.Darker.Policies; -using Paramore.Darker.QueryLogging; - -namespace GreetingsPorts.Handlers -{ - public class FindPersonByNameHandlerAsync : QueryHandlerAsync - { - private readonly IAmARelationalDbConnectionProvider _relationalDbConnectionProvider; - - public FindPersonByNameHandlerAsync(IAmARelationalDbConnectionProvider relationalDbConnectionProvider) - { - _relationalDbConnectionProvider = relationalDbConnectionProvider; - } - - [QueryLogging(0)] - [RetryableQuery(1, Retry.EXPONENTIAL_RETRYPOLICYASYNC)] - public override async Task ExecuteAsync(FindPersonByName query, CancellationToken cancellationToken = new CancellationToken()) - { - 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_Kafka/GreetingsPorts/Policies/GreetingsPolicy.cs b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Policies/GreetingsPolicy.cs deleted file mode 100644 index ed2afaec91..0000000000 --- a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Policies/GreetingsPolicy.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Paramore.Brighter; - -namespace GreetingsPorts.Policies -{ - public class GreetingsPolicy : DefaultPolicy - { - public GreetingsPolicy() - { - AddGreetingsPolicies(); - } - - private void AddGreetingsPolicies() - { - Add(Retry.RETRYPOLICYASYNC, Retry.GetSimpleHandlerRetryPolicy()); - Add(Retry.EXPONENTIAL_RETRYPOLICYASYNC, Retry.GetExponentialHandlerRetryPolicy()); - Add(Paramore.Darker.Policies.Constants.RetryPolicyName, Retry.GetDefaultRetryPolicy()); - Add(Paramore.Darker.Policies.Constants.CircuitBreakerPolicyName, Retry.GetDefaultCircuitBreakerPolicy()); - } - } -} diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Policies/Retry.cs b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Policies/Retry.cs deleted file mode 100644 index df39319263..0000000000 --- a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Policies/Retry.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using Polly; -using Polly.CircuitBreaker; -using Polly.Contrib.WaitAndRetry; -using Polly.Retry; - -namespace GreetingsPorts.Policies -{ - public static class Retry - { - public const string RETRYPOLICYASYNC = "GreetingsPorts.Policies.RetryPolicyAsync"; - public const string EXPONENTIAL_RETRYPOLICYASYNC = "GreetingsPorts.Policies.ExponenttialRetryPolicyAsync"; - - public static AsyncRetryPolicy GetSimpleHandlerRetryPolicy() - { - return Policy.Handle().WaitAndRetryAsync(new[] - { - TimeSpan.FromMilliseconds(50), TimeSpan.FromMilliseconds(100), TimeSpan.FromMilliseconds(150) - }); - } - - public static AsyncRetryPolicy GetExponentialHandlerRetryPolicy() - { - var delay = Backoff.ExponentialBackoff(TimeSpan.FromMilliseconds(100), retryCount: 5, fastFirst: true); - return Policy.Handle().WaitAndRetryAsync(delay); - } - - public static AsyncRetryPolicy GetDefaultRetryPolicy() - { - return Policy.Handle() - .WaitAndRetryAsync(new[] - { - TimeSpan.FromMilliseconds(50), TimeSpan.FromMilliseconds(100), TimeSpan.FromMilliseconds(150) - }); - } - - public static AsyncCircuitBreakerPolicy GetDefaultCircuitBreakerPolicy() - { - return Policy.Handle().CircuitBreakerAsync( - 1, TimeSpan.FromMilliseconds(500) - ); - } - } -} diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Requests/AddGreeting.cs b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Requests/AddGreeting.cs deleted file mode 100644 index 0e10dc0697..0000000000 --- a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Requests/AddGreeting.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using Paramore.Brighter; - -namespace GreetingsPorts.Requests -{ - public class AddGreeting : Command - { - public string Name { get; } - public string Greeting { get; } - - public AddGreeting(string name, string greeting) - : base(Guid.NewGuid()) - { - Name = name; - Greeting = greeting; - } - } -} diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Requests/AddPerson.cs b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Requests/AddPerson.cs deleted file mode 100644 index 2e86c6d7f3..0000000000 --- a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Requests/AddPerson.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using Paramore.Brighter; - -namespace GreetingsPorts.Requests -{ - public class AddPerson : Command - { - public string Name { get; set; } - - public AddPerson(string name) - : base(Guid.NewGuid()) - { - Name = name; - } - } -} diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Requests/DeletePerson.cs b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Requests/DeletePerson.cs deleted file mode 100644 index d09f213ba9..0000000000 --- a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Requests/DeletePerson.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using Paramore.Brighter; - -namespace GreetingsPorts.Requests -{ - public class DeletePerson : Command - { - public string Name { get; } - - public DeletePerson(string name) - : base(Guid.NewGuid()) - { - Name = name; - } - } -} diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Requests/FindGreetingsForPerson.cs b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Requests/FindGreetingsForPerson.cs deleted file mode 100644 index b7ef165f22..0000000000 --- a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Requests/FindGreetingsForPerson.cs +++ /dev/null @@ -1,15 +0,0 @@ -using GreetingsPorts.Responses; -using Paramore.Darker; - -namespace GreetingsPorts.Requests -{ - public class FindGreetingsForPerson : IQuery - { - public string Name { get; } - - public FindGreetingsForPerson(string name) - { - Name = name; - } - } -} diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Requests/FindPersonByName.cs b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Requests/FindPersonByName.cs deleted file mode 100644 index bc571978c0..0000000000 --- a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Requests/FindPersonByName.cs +++ /dev/null @@ -1,15 +0,0 @@ -using GreetingsPorts.Responses; -using Paramore.Darker; - -namespace GreetingsPorts.Requests -{ - public class FindPersonByName : IQuery - { - public string Name { get; } - - public FindPersonByName(string name) - { - Name = name; - } - } -} diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Requests/GreetingMade.cs b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Requests/GreetingMade.cs deleted file mode 100644 index 4f8ea156a2..0000000000 --- a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Requests/GreetingMade.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using Paramore.Brighter; - -namespace GreetingsPorts.Requests -{ - public class GreetingMade : Event - { - public string Greeting { get; set; } - - public GreetingMade(string greeting) : base(Guid.NewGuid()) - { - Greeting = greeting; - } - } -} diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Responses/FindPersonResult.cs b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Responses/FindPersonResult.cs deleted file mode 100644 index ea50242c8c..0000000000 --- a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Responses/FindPersonResult.cs +++ /dev/null @@ -1,14 +0,0 @@ -using GreetingsEntities; - -namespace GreetingsPorts.Responses -{ - public class FindPersonResult - { - public string Name { get; private set; } - public FindPersonResult(Person person) - { - Name = person.Name; - } - - } -} diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Responses/FindPersonsGreetings.cs b/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Responses/FindPersonsGreetings.cs deleted file mode 100644 index 6ab530b4ba..0000000000 --- a/samples/WebAPI_Dapper_Kafka/GreetingsPorts/Responses/FindPersonsGreetings.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Collections.Generic; - -namespace GreetingsPorts.Responses -{ - public class FindPersonsGreetings - { - public string Name { get; set; } - public IEnumerable Greetings { get;set; } - - } - - public class Salutation - { - public string Words { get; set; } - - public Salutation(string words) - { - Words = words; - } - - } -} diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Controllers/GreetingsController.cs b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Controllers/GreetingsController.cs deleted file mode 100644 index 10bbb3ad1e..0000000000 --- a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Controllers/GreetingsController.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.Threading.Tasks; -using GreetingsPorts.Requests; -using GreetingsPorts.Responses; -using GreetingsWeb.Models; -using Microsoft.AspNetCore.Mvc; -using Paramore.Brighter; -using Paramore.Darker; - -namespace GreetingsWeb.Controllers -{ - [ApiController] - [Route("[controller]")] - public class GreetingsController : Controller - { - private readonly IAmACommandProcessor _commandProcessor; - private readonly IQueryProcessor _queryProcessor; - - public GreetingsController(IAmACommandProcessor commandProcessor, IQueryProcessor queryProcessor) - { - _commandProcessor = commandProcessor; - _queryProcessor = queryProcessor; - } - - [Route("{name}")] - [HttpGet] - public async Task Get(string name) - { - var personsGreetings = await _queryProcessor.ExecuteAsync(new FindGreetingsForPerson(name)); - - if (personsGreetings == null) return new NotFoundResult(); - - return Ok(personsGreetings); - } - - [Route("{name}/new")] - [HttpPost] - public async Task> Post(string name, NewGreeting newGreeting) - { - await _commandProcessor.SendAsync(new AddGreeting(name, newGreeting.Greeting)); - - var personsGreetings = await _queryProcessor.ExecuteAsync(new FindGreetingsForPerson(name)); - - if (personsGreetings == null) return new NotFoundResult(); - - return Ok(personsGreetings); - } - } -} diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Controllers/PeopleController.cs b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Controllers/PeopleController.cs deleted file mode 100644 index 0ce06da254..0000000000 --- a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Controllers/PeopleController.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; -using System.Threading.Tasks; -using GreetingsPorts.Requests; -using GreetingsPorts.Responses; -using GreetingsWeb.Models; -using Microsoft.AspNetCore.Mvc; -using Paramore.Brighter; -using Paramore.Darker; - -namespace GreetingsWeb.Controllers -{ - [ApiController] - [Route("[controller]")] - public class PeopleController : Controller - { - private readonly IAmACommandProcessor _commandProcessor; - private readonly IQueryProcessor _queryProcessor; - - public PeopleController(IAmACommandProcessor commandProcessor, IQueryProcessor queryProcessor) - { - _commandProcessor = commandProcessor; - _queryProcessor = queryProcessor; - } - - - [Route("{name}")] - [HttpGet] - public async Task> Get(string name) - { - var foundPerson = await _queryProcessor.ExecuteAsync(new FindPersonByName(name)); - - if (foundPerson == null) return new NotFoundResult(); - - return Ok(foundPerson); - } - - [Route("{name}")] - [HttpDelete] - public async Task Delete(string name) - { - await _commandProcessor.SendAsync(new DeletePerson(name)); - - return Ok(); - } - - [Route("new")] - [HttpPost] - public async Task> Post(NewPerson newPerson) - { - await _commandProcessor.SendAsync(new AddPerson(newPerson.Name)); - - var addedPerson = await _queryProcessor.ExecuteAsync(new FindPersonByName(newPerson.Name)); - - if (addedPerson == null) return new NotFoundResult(); - - return Ok(addedPerson); - } - - } -} diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/DatabaseType.cs b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/DatabaseType.cs deleted file mode 100644 index 29118a556c..0000000000 --- a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/DatabaseType.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace GreetingsWeb.Database; - -public static class DatabaseGlobals -{ - //environment string key - public const string DATABASE_TYPE_ENV = "BRIGHTER_GREETINGS_DATABASE"; - - public const string MYSQL = "MySQL"; - public const string MSSQL = "MsSQL"; - public const string POSTGRESSQL = "PostgresSQL"; - public const string SQLITE = "Sqlite"; -} - -public enum DatabaseType -{ - MySql, - MsSql, - Postgres, - Sqlite -} diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/OutboxExtensions.cs b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/OutboxExtensions.cs deleted file mode 100644 index 6c80d26793..0000000000 --- a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/OutboxExtensions.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System; -using GreetingsEntities; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Npgsql; -using Paramore.Brighter; -using Paramore.Brighter.Extensions.DependencyInjection; -using Paramore.Brighter.Extensions.Hosting; -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 GreetingsWeb.Database -{ - - public class OutboxExtensions - { - public static (IAmAnOutbox, Type, Type) MakeOutbox( - IWebHostEnvironment env, - DatabaseType databaseType, - RelationalDatabaseConfiguration configuration) - { - (IAmAnOutbox, Type, Type) outbox; - if (env.IsDevelopment()) - { - outbox = MakeSqliteOutBox(configuration); - } - else - { - outbox = databaseType switch - { - DatabaseType.MySql => MakeMySqlOutbox(configuration), - DatabaseType.MsSql => MakeMsSqlOutbox(configuration), - DatabaseType.Postgres => MakePostgresSqlOutbox(configuration), - DatabaseType.Sqlite => MakeSqliteOutBox(configuration), - _ => throw new InvalidOperationException("Unknown Db type for Outbox configuration") - }; - } - - return outbox; - } - - private static (IAmAnOutbox, Type, Type) MakePostgresSqlOutbox(RelationalDatabaseConfiguration configuration) - { - //if we want to use our IAmARelationalDatabaseConnectionProvider or IAmAABoxTransactionProvider - //from the Outbox in our handlers, then we need to construct an NpgsqlDataSource and register the composite types - //then pass that to the Outbox constructor so that connections created by the Outbox will be aware of - //those composite types - var dataSourceBuilder = new NpgsqlDataSourceBuilder(configuration.ConnectionString); - dataSourceBuilder.MapComposite(); - dataSourceBuilder.MapComposite(); - var dataSource = dataSourceBuilder.Build(); - - return (new PostgreSqlOutbox(configuration, dataSource), 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_Kafka/GreetingsWeb/Database/SchemaCreation.cs b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/SchemaCreation.cs deleted file mode 100644 index 2d74feb6e3..0000000000 --- a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Database/SchemaCreation.cs +++ /dev/null @@ -1,330 +0,0 @@ -using System; -using System.Data; -using System.Data.Common; -using FluentMigrator.Runner; -using Microsoft.AspNetCore.Hosting; -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.Outbox.MsSql; -using Paramore.Brighter.Outbox.MySql; -using Paramore.Brighter.Outbox.PostgreSql; -using Paramore.Brighter.Outbox.Sqlite; -using Polly; - -namespace GreetingsWeb.Database -{ - public static class SchemaCreation - { - private const string OUTBOX_TABLE_NAME = "Outbox"; - - public static IHost CheckDbIsUp(this IHost webHost) - { - using var scope = webHost.Services.CreateScope(); - - var services = scope.ServiceProvider; - var env = services.GetService(); - var config = services.GetService(); - var (dbType, connectionString) = DbServerConnectionString(config, env); - - //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(dbType, connectionString); - CreateDatabaseIfNotExists(dbType, GetDbConnection(dbType, connectionString)); - - return webHost; - } - - public static IHost MigrateDatabase(this IHost webHost) - { - using (var scope = webHost.Services.CreateScope()) - { - var services = scope.ServiceProvider; - - try - { - var runner = services.GetRequiredService(); - runner.ListMigrations(); - runner.MigrateUp(); - } - catch (Exception ex) - { - var logger = services.GetRequiredService>(); - logger.LogError(ex, "An error occurred while migrating the database."); - throw; - } - } - - return webHost; - } - - public static IHost CreateOutbox(this IHost webHost, bool hasBinaryPayload = false) - { - using var scope = webHost.Services.CreateScope(); - var services = scope.ServiceProvider; - var env = services.GetService(); - var config = services.GetService(); - - CreateOutbox(config, env, hasBinaryPayload); - - return webHost; - } - - 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 = 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 Outbox 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, bool hasBinaryPayload) - { - try - { - var connectionString = DbConnectionString(config, env); - - if (env.IsDevelopment()) - CreateOutboxDevelopment(connectionString, hasBinaryPayload); - else - 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) - { - 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, bool hasBinaryPayload) - { - CreateOutboxSqlite(connectionString, hasBinaryPayload); - } - - private static void CreateOutboxProduction( - DatabaseType databaseType, - string connectionString, - bool hasBinaryPayload) - { - switch (databaseType) - { - case DatabaseType.MySql: - 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 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); - bool exists = existsQuery.ExecuteScalar() != null; - - 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; - - if (exists) return; - - using var command = sqlConnection.CreateCommand(); - 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); - bool exists = existsQuery.ExecuteScalar() != null; - - if (exists) return; - - using var command = sqlConnection.CreateCommand(); - command.CommandText = PostgreSqlOutboxBulder.GetDDL(OUTBOX_TABLE_NAME, hasBinaryPayload); - command.ExecuteScalar(); - } - - private static void CreateOutboxSqlite(string connectionString, bool hasBinaryPayload) - { - 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 DbConnectionString(IConfiguration config, IWebHostEnvironment env) - { - //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 (DatabaseType, string) DbServerConnectionString(IConfiguration config, IWebHostEnvironment env) - { - var databaseType = GetDatabaseType(config); - var connectionString = env.IsDevelopment() ? GetDevConnectionString() : GetProductionConnectionString(config, databaseType); - return (databaseType, connectionString); - } - - private static string GetDevConnectionString() - { - return "Filename=Greetings.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("GreetingsMySql"), - DatabaseType.MsSql => config.GetConnectionString("GreetingsMsSql"), - DatabaseType.Postgres => config.GetConnectionString("GreetingsPostgreSql"), - 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) - }; - } - } -} diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Dockerfile b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Dockerfile deleted file mode 100644 index b6633e7655..0000000000 --- a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Dockerfile +++ /dev/null @@ -1,10 +0,0 @@ -FROM mcr.microsoft.com/dotnet/aspnet:5.0-buster-slim - -WORKDIR /app -COPY out/ . - -# Expose the port -EXPOSE 5000 -ENV ASPNETCORE_URLS=http://+:5000 -#run the site -ENTRYPOINT ["dotnet", "GreetingsWeb.dll"] diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/GreetingsWeb.csproj b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/GreetingsWeb.csproj deleted file mode 100644 index f60f5e7db4..0000000000 --- a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/GreetingsWeb.csproj +++ /dev/null @@ -1,66 +0,0 @@ - - - - net6.0 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - <_ContentIncludedByDefault Remove="out\web.config" /> - <_ContentIncludedByDefault Remove="out\appsettings.Development.json" /> - <_ContentIncludedByDefault Remove="out\appsettings.json" /> - <_ContentIncludedByDefault Remove="out\appsettings.Production.json" /> - <_ContentIncludedByDefault Remove="out\GreetingsAdapters.deps.json" /> - <_ContentIncludedByDefault Remove="out\GreetingsAdapters.runtimeconfig.json" /> - - - - - ..\..\..\libs\Npgsql\net6.0\Npgsql.dll - - - - diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Mappers/GreetingMadeMessageMapper.cs b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Mappers/GreetingMadeMessageMapper.cs deleted file mode 100644 index 5d626e058b..0000000000 --- a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Mappers/GreetingMadeMessageMapper.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System.Net.Mime; -using System.Text.Json; -using Confluent.Kafka; -using Confluent.Kafka.SyncOverAsync; -using Confluent.SchemaRegistry; -using Confluent.SchemaRegistry.Serdes; -using GreetingsPorts.Requests; -using Paramore.Brighter; -using Paramore.Brighter.MessagingGateway.Kafka; - - -namespace GreetingsWeb.Mappers -{ - public class GreetingMadeMessageMapper : IAmAMessageMapper - { - private readonly ISchemaRegistryClient _schemaRegistryClient; - private readonly string _partitionKey = "KafkaTestQueueExample_Partition_One"; - private SerializationContext _serializationContext; - private const string Topic = "greeting.event"; - public GreetingMadeMessageMapper(ISchemaRegistryClient schemaRegistryClient) - { - _schemaRegistryClient = schemaRegistryClient; - //We care about ensuring that we serialize the body using the Confluent tooling, as it registers and validates schema - _serializationContext = new SerializationContext(MessageComponentType.Value, Topic); - } - - public Message MapToMessage(GreetingMade request) - { - var header = new MessageHeader(messageId: request.Id, topic: Topic, messageType: MessageType.MT_EVENT); - //This uses the Confluent JSON serializer, which wraps Newtonsoft but also performs schema registration and validation - var serializer = new JsonSerializer(_schemaRegistryClient, ConfluentJsonSerializationConfig.SerdesJsonSerializerConfig(), ConfluentJsonSerializationConfig.NJsonSchemaGeneratorSettings()).AsSyncOverAsync(); - var s = serializer.Serialize(request, _serializationContext); - var body = new MessageBody(s, MediaTypeNames.Application.Octet, CharacterEncoding.Raw); - header.PartitionKey = _partitionKey; - - var message = new Message(header, body); - return message; - - } - - public GreetingMade MapToRequest(Message message) - { - throw new System.NotImplementedException(); - } - } -} diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Models/NewGreeting.cs b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Models/NewGreeting.cs deleted file mode 100644 index 3870feb5fd..0000000000 --- a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Models/NewGreeting.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace GreetingsWeb.Models -{ - public class NewGreeting - { - public string Greeting { get; set; } - } -} diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Models/NewPerson.cs b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Models/NewPerson.cs deleted file mode 100644 index 19dc16dfb8..0000000000 --- a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Models/NewPerson.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace GreetingsWeb.Models -{ - public class NewPerson - { - public string Name { get; set; } - } -} diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Program.cs b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Program.cs deleted file mode 100644 index 13e578f898..0000000000 --- a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Program.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System.IO; -using FluentMigrator.Runner; -using GreetingsWeb.Database; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; - -namespace GreetingsWeb -{ - public class Program - { - public static void Main(string[] args) - { - var host = CreateHostBuilder(args).Build(); - - host.CheckDbIsUp(); - host.MigrateDatabase(); - //NOTE: Because we use the Serdes serializer with Kafka, that adds scheme registry info to the message payload - //we use a binary payload outbox to avoid corruption. - host.CreateOutbox(hasBinaryPayload: true); - - host.Run(); - } - - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureAppConfiguration((context, configBuilder) => - { - var env = context.HostingEnvironment; - configBuilder.AddJsonFile("appsettings.json", optional: false); - configBuilder.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true); - configBuilder.AddEnvironmentVariables(prefix:"BRIGHTER_"); - }) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseKestrel(); - webBuilder.UseContentRoot(Directory.GetCurrentDirectory()); - webBuilder.CaptureStartupErrors(true); - webBuilder.UseSetting("detailedErrors", "true"); - webBuilder.ConfigureLogging((hostingContext, logging) => - { - logging.AddConsole(); - logging.AddDebug(); - logging.AddFluentMigratorConsole(); - }); - webBuilder.UseDefaultServiceProvider((context, options) => - { - var isDevelopment = context.HostingEnvironment.IsDevelopment(); - options.ValidateScopes = isDevelopment; - options.ValidateOnBuild = isDevelopment; - }); - webBuilder.UseStartup(); - }); - } -} diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Properties/launchSettings.json b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Properties/launchSettings.json deleted file mode 100644 index 6ddca8d915..0000000000 --- a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Properties/launchSettings.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "$schema": "http://json.schemastore.org/launchsettings.json", - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:8854", - "sslPort": 44355 - } - }, - "profiles": { - "Development": { - "commandName": "Project", - "dotnetRunMessages": "true", - "launchBrowser": true, - "launchUrl": "swagger", - "applicationUrl": "https://localhost:5001;http://localhost:5000", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development", - "BRIGHTER_GREETINGS_DATABASE": "Sqlite" - } - }, - "ProductionMySql": { - "commandName": "Project", - "dotnetRunMessages": "true", - "launchBrowser": true, - "launchUrl": "swagger", - "applicationUrl": "https://localhost:5001;http://localhost:5000", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Production", - "BRIGHTER_GREETINGS_DATABASE": "MySQL" - } - }, - "ProductionPostgres": { - "commandName": "Project", - "dotnetRunMessages": "true", - "launchBrowser": true, - "launchUrl": "swagger", - "applicationUrl": "https://localhost:5001;http://localhost:5000", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Production", - "BRIGHTER_GREETINGS_DATABASE": "PostgresSQL" - } - }, - "ProductionMsSql": { - "commandName": "Project", - "dotnetRunMessages": "true", - "launchBrowser": true, - "launchUrl": "swagger", - "applicationUrl": "https://localhost:5001;http://localhost:5000", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Production", - "BRIGHTER_GREETINGS_DATABASE": "MsSQL" - } - } - } -} diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs deleted file mode 100644 index 5431109a20..0000000000 --- a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/Startup.cs +++ /dev/null @@ -1,267 +0,0 @@ -using System; -using Confluent.SchemaRegistry; -using FluentMigrator.Runner; -using Greetings_MySqlMigrations.Migrations; -using Greetings_PostgreSqlMigrations.Migrations; -using Greetings_SqliteMigrations.Migrations; -using GreetingsPorts.Handlers; -using GreetingsPorts.Policies; -using GreetingsWeb.Database; -using Hellang.Middleware.ProblemDetails; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.OpenApi.Models; -using OpenTelemetry.Metrics; -using OpenTelemetry.Trace; -using Paramore.Brighter; -using Paramore.Brighter.Extensions.DependencyInjection; -using Paramore.Brighter.Extensions.Hosting; -using Paramore.Brighter.MessagingGateway.Kafka; -using Paramore.Darker.AspNetCore; -using Paramore.Darker.Policies; -using Paramore.Darker.QueryLogging; - -namespace GreetingsWeb -{ - public class Startup - { - private const string OUTBOX_TABLE_NAME = "Outbox"; - - private readonly IConfiguration _configuration; - private readonly IWebHostEnvironment _env; - - public Startup(IConfiguration configuration, IWebHostEnvironment env) - { - _configuration = configuration; - _env = 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(); - app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "GreetingsAPI v1")); - } - - app.UseHttpsRedirection(); - app.UseRouting(); - - app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); - } - - - // This method gets called by the runtime. Use this method to add services to the container. - public void ConfigureServices(IServiceCollection services) - { - services.AddMvcCore().AddApiExplorer(); - services.AddControllers(options => - { - options.RespectBrowserAcceptHeader = true; - }) - .AddXmlSerializerFormatters(); - services.AddProblemDetails(); - services.AddSwaggerGen(c => - { - c.SwaggerDoc("v1", new OpenApiInfo { Title = "GreetingsAPI", Version = "v1" }); - }); - - services.AddOpenTelemetry() - .WithTracing(builder => builder - .AddAspNetCoreInstrumentation() - .AddConsoleExporter()) - .WithMetrics(builder => builder - .AddAspNetCoreInstrumentation() - .AddConsoleExporter()); - - ConfigureMigration(services); - ConfigureBrighter(services); - ConfigureDarker(services); - } - - private void ConfigureMigration(IServiceCollection services) - { - //dev is always Sqlite - if (_env.IsDevelopment()) - ConfigureSqlite(services); - else - ConfigureProductionDatabase(GetDatabaseType(), services); - } - - private void ConfigureProductionDatabase(DatabaseType databaseType, IServiceCollection services) - { - switch (databaseType) - { - 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 ConfigureMsSql(IServiceCollection services) - { - services - .AddFluentMigratorCore() - .ConfigureRunner(c => c.AddSqlServer() - .WithGlobalConnectionString(DbConnectionString()) - .ScanIn(typeof(MsSqlInitialCreate).Assembly).For.Migrations() - ); - } - - private void ConfigureMySql(IServiceCollection services) - { - services - .AddFluentMigratorCore() - .ConfigureRunner(c => c.AddMySql5() - .WithGlobalConnectionString(DbConnectionString()) - .ScanIn(typeof(MySqlInitialCreate).Assembly).For.Migrations() - ); - } - - private void ConfigurePostgreSql(IServiceCollection services) - { - //TODO: add Postgres migrations - services - .AddFluentMigratorCore() - .ConfigureRunner(c => c.AddMySql5() - .WithGlobalConnectionString(DbConnectionString()) - .ScanIn(typeof(PostgreSqlInitialCreate).Assembly).For.Migrations() - ); - } - - private void ConfigureSqlite(IServiceCollection services) - { - services - .AddFluentMigratorCore() - .ConfigureRunner(c => - { - c.AddSQLite() - .WithGlobalConnectionString(DbConnectionString()) - .ScanIn(typeof(SqlliteInitialCreate).Assembly).For.Migrations(); - }); - } - - private void ConfigureBrighter(IServiceCollection services) - { - var outboxConfiguration = new RelationalDatabaseConfiguration( - DbConnectionString(), - outBoxTableName: OUTBOX_TABLE_NAME, - //NOTE: With the Serdes serializer, if we don't use a binary payload, the payload will be corrupted - binaryMessagePayload: true - ); - services.AddSingleton(outboxConfiguration); - - var schemaRegistryConfig = new SchemaRegistryConfig { Url = "http://localhost:8081" }; - var cachedSchemaRegistryClient = new CachedSchemaRegistryClient(schemaRegistryConfig); - services.AddSingleton(cachedSchemaRegistryClient); - - var kafkaConfiguration = new KafkaMessagingGatewayConfiguration - { - Name = "paramore.brighter.greetingsender", BootStrapServers = new[] { "localhost:9092" } - }; - var producerRegistry = new KafkaProducerRegistryFactory( - kafkaConfiguration, - new KafkaPublication[] - { - new KafkaPublication - { - Topic = new RoutingKey("greeting.event"), - MessageSendMaxRetries = 3, - MessageTimeoutMs = 1000, - MaxInFlightRequestsPerConnection = 1, - MakeChannels = OnMissingChannel.Create - } - }) - .Create(); - - (IAmAnOutbox outbox, Type connectionProvider, Type transactionProvider) makeOutbox = - OutboxExtensions.MakeOutbox(_env, GetDatabaseType(), outboxConfiguration); - - 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 = makeOutbox.outbox; - configure.ConnectionProvider = makeOutbox.connectionProvider; - configure.TransactionProvider = makeOutbox.transactionProvider; - }) - .UseOutboxSweeper(options => - { - options.TimerInterval = 5; - options.MinimumMessageAge = 5000; - }) - .AutoFromAssemblies(typeof(AddPersonHandlerAsync).Assembly); - } - - private void ConfigureDarker(IServiceCollection services) - { - services.AddDarker(options => - { - options.HandlerLifetime = ServiceLifetime.Scoped; - options.QueryProcessorLifetime = ServiceLifetime.Scoped; - }) - .AddHandlersFromAssemblies(typeof(FindPersonByNameHandlerAsync).Assembly) - .AddJsonQueryLogging() - .AddPolicies(new GreetingsPolicy()); - } - - 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() ? GetDevDbConnectionString() : GetConnectionString(GetDatabaseType()); - } - - private DatabaseType GetDatabaseType() - { - 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") - }; - } - - private static string GetDevDbConnectionString() - { - return "Filename=Greetings.db;Cache=Shared"; - } - - private string GetConnectionString(DatabaseType databaseType) - { - return databaseType switch - { - DatabaseType.MySql => _configuration.GetConnectionString("GreetingsMySql"), - DatabaseType.MsSql => _configuration.GetConnectionString("GreetingsMsSql"), - DatabaseType.Postgres => _configuration.GetConnectionString("GreetingsPostgreSql"), - DatabaseType.Sqlite => GetDevDbConnectionString(), - _ => throw new InvalidOperationException("Could not determine the database type") - }; - } - } -} diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/appsettings.Development.json b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/appsettings.Development.json deleted file mode 100644 index 1d7a4e1ad3..0000000000 --- a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/appsettings.Development.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Debug", - "Microsoft": "Information", - "Microsoft.Hosting.Lifetime": "Information" - } - } -} diff --git a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/appsettings.Production.json b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/appsettings.Production.json deleted file mode 100644 index 5ad8dd3201..0000000000 --- a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/appsettings.Production.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Warning", - "System": "Warning", - "Microsoft": "Warning" - } - }, - "ConnectionStrings": { - "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_Kafka/GreetingsWeb/appsettings.json b/samples/WebAPI_Dapper_Kafka/GreetingsWeb/appsettings.json deleted file mode 100644 index 6b9d1b5c56..0000000000 --- a/samples/WebAPI_Dapper_Kafka/GreetingsWeb/appsettings.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft": "Information", - "Microsoft.Hosting.Lifetime": "Information" - } - }, - "AllowedHosts": "*", -} diff --git a/samples/WebAPI_Dapper_Kafka/Greetings_MsSqlMigrations/Greetings_MsSqlMigrations.csproj b/samples/WebAPI_Dapper_Kafka/Greetings_MsSqlMigrations/Greetings_MsSqlMigrations.csproj deleted file mode 100644 index 9107f11c61..0000000000 --- a/samples/WebAPI_Dapper_Kafka/Greetings_MsSqlMigrations/Greetings_MsSqlMigrations.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - - net6.0 - enable - enable - - - - - - - diff --git a/samples/WebAPI_Dapper_Kafka/Greetings_MsSqlMigrations/Migrations/202306041332_InitialCreate.cs b/samples/WebAPI_Dapper_Kafka/Greetings_MsSqlMigrations/Migrations/202306041332_InitialCreate.cs deleted file mode 100644 index e6db18eccc..0000000000 --- a/samples/WebAPI_Dapper_Kafka/Greetings_MsSqlMigrations/Migrations/202306041332_InitialCreate.cs +++ /dev/null @@ -1,30 +0,0 @@ -using FluentMigrator; - -namespace Greetings_PostgreSqlMigrations.Migrations; - -[Migration(1)] -public class MsSqlInitialCreate : 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_Kafka/Greetings_MySqlMigrations/Greetings_MySqlMigrations.csproj b/samples/WebAPI_Dapper_Kafka/Greetings_MySqlMigrations/Greetings_MySqlMigrations.csproj deleted file mode 100644 index be526d4af0..0000000000 --- a/samples/WebAPI_Dapper_Kafka/Greetings_MySqlMigrations/Greetings_MySqlMigrations.csproj +++ /dev/null @@ -1,14 +0,0 @@ - - - - net6.0 - enable - disable - - - - - - - - diff --git a/samples/WebAPI_Dapper_Kafka/Greetings_MySqlMigrations/Migrations/20220527_InitialCreate.cs b/samples/WebAPI_Dapper_Kafka/Greetings_MySqlMigrations/Migrations/20220527_InitialCreate.cs deleted file mode 100644 index 9fa274705a..0000000000 --- a/samples/WebAPI_Dapper_Kafka/Greetings_MySqlMigrations/Migrations/20220527_InitialCreate.cs +++ /dev/null @@ -1,30 +0,0 @@ -using FluentMigrator; - -namespace Greetings_MySqlMigrations.Migrations; - -[Migration(1)] -public class MySqlInitialCreate : Migration -{ - public override void Up() - { - Create.Table("Person") - .WithColumn("Id").AsInt32().NotNullable().PrimaryKey().Identity() - .WithColumn("Name").AsString().Unique() - .WithColumn("TimeStamp").AsDateTime().Nullable().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_Kafka/Greetings_PostgreSqlMigrations/Greetings_PostgreSqlMigrations.csproj b/samples/WebAPI_Dapper_Kafka/Greetings_PostgreSqlMigrations/Greetings_PostgreSqlMigrations.csproj deleted file mode 100644 index 78d464b3d5..0000000000 --- a/samples/WebAPI_Dapper_Kafka/Greetings_PostgreSqlMigrations/Greetings_PostgreSqlMigrations.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - - net6.0 - enable - enable - - - - - - - diff --git a/samples/WebAPI_Dapper_Kafka/Greetings_PostgreSqlMigrations/Migrations/202306031734_InitialCreate.cs b/samples/WebAPI_Dapper_Kafka/Greetings_PostgreSqlMigrations/Migrations/202306031734_InitialCreate.cs deleted file mode 100644 index 7ada805d2b..0000000000 --- a/samples/WebAPI_Dapper_Kafka/Greetings_PostgreSqlMigrations/Migrations/202306031734_InitialCreate.cs +++ /dev/null @@ -1,30 +0,0 @@ -using FluentMigrator; - -namespace Greetings_PostgreSqlMigrations.Migrations; - -[Migration(1)] -public class PostgreSqlInitialCreate : 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_Kafka/Greetings_SqliteMigrations/Greetings_SqliteMigrations.csproj b/samples/WebAPI_Dapper_Kafka/Greetings_SqliteMigrations/Greetings_SqliteMigrations.csproj deleted file mode 100644 index eb42023016..0000000000 --- a/samples/WebAPI_Dapper_Kafka/Greetings_SqliteMigrations/Greetings_SqliteMigrations.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - - net6.0 - enable - disable - - - - - - - diff --git a/samples/WebAPI_Dapper_Kafka/Greetings_SqliteMigrations/Migrations/202204221833_InitialCreate.cs b/samples/WebAPI_Dapper_Kafka/Greetings_SqliteMigrations/Migrations/202204221833_InitialCreate.cs deleted file mode 100644 index 1b1edf2cbf..0000000000 --- a/samples/WebAPI_Dapper_Kafka/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_Kafka/SalutationAnalytics/Database/DatabaseType.cs b/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Database/DatabaseType.cs deleted file mode 100644 index 19e4d6162d..0000000000 --- a/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Database/DatabaseType.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace SalutationAnalytics.Database; - -public static class DatabaseGlobals -{ - //environment string key - public const string DATABASE_TYPE_ENV = "BRIGHTER_GREETINGS_DATABASE"; - - public const string MYSQL = "MySQL"; - public const string MSSQL = "MsSQL"; - public const string POSTGRESSQL = "PostgresSQL"; - public const string SQLITE = "Sqlite"; -} - -public enum DatabaseType -{ - MySql, - MsSql, - Postgres, - Sqlite -} diff --git a/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Database/SchemaCreation.cs b/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Database/SchemaCreation.cs deleted file mode 100644 index f727b948a3..0000000000 --- a/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Database/SchemaCreation.cs +++ /dev/null @@ -1,229 +0,0 @@ -using System; -using System.Data; -using FluentMigrator.Runner; -using Microsoft.Data.Sqlite; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using MySqlConnector; -using Paramore.Brighter.Inbox.MySql; -using Paramore.Brighter.Inbox.Sqlite; -using Paramore.Brighter.Outbox.MySql; -using Paramore.Brighter.Outbox.Sqlite; -using Polly; - -namespace SalutationAnalytics.Database -{ - public static class SchemaCreation - { - internal const string INBOX_TABLE_NAME = "Inbox"; - internal const string OUTBOX_TABLE_NAME = "Outbox"; - - public static IHost CheckDbIsUp(this IHost host) - { - using var scope = host.Services.CreateScope(); - - var services = scope.ServiceProvider; - var env = services.GetService(); - var config = services.GetService(); - string connectionString = DbServerConnectionString(config, env); - - //We don't check in development as using Sqlite - if (env.IsDevelopment()) return host; - - WaitToConnect(connectionString); - CreateDatabaseIfNotExists(connectionString); - - return host; - } - - public static IHost CreateInbox(this IHost host) - { - using (var scope = host.Services.CreateScope()) - { - var services = scope.ServiceProvider; - var env = services.GetService(); - var config = services.GetService(); - - CreateInbox(config, env); - } - - return host; - } - - public static IHost MigrateDatabase(this IHost host) - { - using (var scope = host.Services.CreateScope()) - { - var services = scope.ServiceProvider; - - try - { - var runner = services.GetRequiredService(); - runner.ListMigrations(); - runner.MigrateUp(); - } - catch (Exception ex) - { - var logger = services.GetRequiredService>(); - logger.LogError(ex, "An error occurred while migrating the database."); - throw; - } - } - - return host; - } - - private static void CreateDatabaseIfNotExists(string connectionString) - { - //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(); - } - - private static void CreateInbox(IConfiguration config, IHostEnvironment env) - { - try - { - var connectionString = DbConnectionString(config, env); - - if (env.IsDevelopment()) - CreateInboxDevelopment(connectionString); - else - CreateInboxProduction(connectionString); - } - catch (System.Exception e) - { - Console.WriteLine($"Issue with creating Inbox table, {e.Message}"); - throw; - } - } - - private static void CreateInboxDevelopment(string connectionString) - { - using var sqlConnection = new SqliteConnection(connectionString); - sqlConnection.Open(); - - using var exists = sqlConnection.CreateCommand(); - exists.CommandText = SqliteInboxBuilder.GetExistsQuery(INBOX_TABLE_NAME); - using var reader = exists.ExecuteReader(CommandBehavior.SingleRow); - - if (reader.HasRows) return; - - using var command = sqlConnection.CreateCommand(); - command.CommandText = SqliteInboxBuilder.GetDDL(INBOX_TABLE_NAME); - command.ExecuteScalar(); - } - - private static void CreateInboxProduction(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; - - if (exists) return; - - using var command = sqlConnection.CreateCommand(); - command.CommandText = MySqlInboxBuilder.GetDDL(INBOX_TABLE_NAME); - command.ExecuteScalar(); - } - - 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 CreateOutbox(IConfiguration config, IHostEnvironment env) - { - try - { - var connectionString = DbConnectionString(config, env); - - if (env.IsDevelopment()) - CreateOutboxDevelopment(connectionString); - else - CreateOutboxProduction(connectionString); - } - catch (System.Exception e) - { - Console.WriteLine($"Issue with creating Outbox table, {e.Message}"); - throw; - } - } - - private static void CreateOutboxDevelopment(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(); - } - - private static void CreateOutboxProduction(string connectionString) - { - 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; - - if (exists) return; - - using var command = sqlConnection.CreateCommand(); - command.CommandText = MySqlOutboxBuilder.GetDDL(OUTBOX_TABLE_NAME); - 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"); - } - - private static string DbServerConnectionString(IConfiguration config, IHostEnvironment env) - { - return env.IsDevelopment() ? "Filename=Salutations.db;Cache=Shared" : config.GetConnectionString("SalutationsDb"); - } - - private static void WaitToConnect(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 = new MySqlConnection(connectionString); - conn.Open(); - }); - } - } -} diff --git a/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Dockerfile b/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Dockerfile deleted file mode 100644 index dfaa24d0b8..0000000000 --- a/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Dockerfile +++ /dev/null @@ -1,7 +0,0 @@ -FROM mcr.microsoft.com/dotnet/aspnet:5.0-buster-slim - -WORKDIR /app -COPY out/ . - -#run the site -ENTRYPOINT ["dotnet", "GreetingsWatcher.dll"] diff --git a/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Mappers/GreetingMadeMessageMapper.cs b/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Mappers/GreetingMadeMessageMapper.cs deleted file mode 100644 index 3286d4be2f..0000000000 --- a/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Mappers/GreetingMadeMessageMapper.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System.Text.Json; -using Confluent.Kafka; -using Confluent.Kafka.SyncOverAsync; -using Confluent.SchemaRegistry.Serdes; -using Paramore.Brighter; -using SalutationPorts.Requests; - -namespace SalutationAnalytics.Mappers -{ - public class GreetingMadeMessageMapper : IAmAMessageMapper - { - private readonly SerializationContext _serializationContext; - private const string Topic = "greeting.event"; - - public GreetingMadeMessageMapper() - { - //We care about ensuring that we serialize the body using the Confluent tooling, as it registers and validates schema - _serializationContext = new SerializationContext(MessageComponentType.Value, Topic); - } - public Message MapToMessage(GreetingMade request) - { - throw new System.NotImplementedException(); - } - - public GreetingMade MapToRequest(Message message) - { - var deserializer = new JsonDeserializer().AsSyncOverAsync(); - //This uses the Confluent JSON serializer, which wraps Newtonsoft but also performs schema registration and validation - var greetingCommand = deserializer.Deserialize(message.Body.Bytes, message.Body.Bytes is null, _serializationContext); - - return greetingCommand; - } - } -} diff --git a/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Mappers/SalutationReceivedMessageMapper.cs b/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Mappers/SalutationReceivedMessageMapper.cs deleted file mode 100644 index d4e3fe6616..0000000000 --- a/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Mappers/SalutationReceivedMessageMapper.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Text.Json; -using Paramore.Brighter; -using SalutationPorts.Requests; - -namespace SalutationAnalytics.Mappers -{ - public class SalutationReceivedMessageMapper : IAmAMessageMapper - { - public Message MapToMessage(SalutationReceived request) - { - var header = new MessageHeader(messageId: request.Id, topic: "salutationreceived.event", messageType: MessageType.MT_EVENT); - var body = new MessageBody(System.Text.Json.JsonSerializer.Serialize(request, new JsonSerializerOptions(JsonSerializerDefaults.General))); - var message = new Message(header, body); - return message; - } - - public SalutationReceived MapToRequest(Message message) - { - throw new System.NotImplementedException(); - } - } -} diff --git a/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Program.cs b/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Program.cs deleted file mode 100644 index ccb2904992..0000000000 --- a/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Program.cs +++ /dev/null @@ -1,242 +0,0 @@ -using System; -using System.IO; -using System.Threading.Tasks; -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.Extensions.DependencyInjection; -using Paramore.Brighter.Inbox; -using Paramore.Brighter.Inbox.MySql; -using Paramore.Brighter.Inbox.Sqlite; -using Paramore.Brighter.MessagingGateway.Kafka; -using Paramore.Brighter.MySql; -using Paramore.Brighter.ServiceActivator.Extensions.DependencyInjection; -using Paramore.Brighter.ServiceActivator.Extensions.Hosting; -using Paramore.Brighter.Sqlite; -using SalutationAnalytics.Database; -using SalutationPorts.Policies; -using SalutationPorts.Requests; - -namespace SalutationAnalytics -{ - class Program - { - public static async Task Main(string[] args) - { - var host = CreateHostBuilder(args).Build(); - host.CheckDbIsUp(); - host.MigrateDatabase(); - host.CreateInbox(); - host.CreateOutbox(); - await host.RunAsync(); - } - - private static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureHostConfiguration(configurationBuilder => - { - 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: "BRIGHTER_"); - configurationBuilder.AddCommandLine(args); - }) - .ConfigureLogging((context, builder) => - { - builder.AddConsole(); - builder.AddDebug(); - }) - .ConfigureServices((hostContext, services) => - { - ConfigureMigration(hostContext, services); - ConfigureDapper(hostContext, services); - ConfigureBrighter(hostContext, services); - }) - .UseConsoleLifetime(); - - private static void ConfigureBrighter(HostBuilderContext hostContext, IServiceCollection services) - { - var subscriptions = new KafkaSubscription[] - { - new KafkaSubscription( - new SubscriptionName("paramore.sample.salutationanalytics"), - channelName: new ChannelName("SalutationAnalytics"), - routingKey: new RoutingKey("greeting.event"), - groupId: "kafka-GreetingsReceiverConsole-Sample", - timeoutInMilliseconds: 100, - offsetDefault: AutoOffsetReset.Earliest, - commitBatchSize: 5, - sweepUncommittedOffsetsIntervalMs: 10000, - makeChannels: OnMissingChannel.Create) - }; - - //We take a direct dependency on the schema registry in the message mapper - var schemaRegistryConfig = new SchemaRegistryConfig { Url = "http://localhost:8081"}; - var cachedSchemaRegistryClient = new CachedSchemaRegistryClient(schemaRegistryConfig); - services.AddSingleton(cachedSchemaRegistryClient); - - //create the gateway - var consumerFactory = new KafkaMessageConsumerFactory( - new KafkaMessagingGatewayConfiguration { Name = "paramore.brighter", BootStrapServers = new[] { "localhost:9092" } } - ); - - var relationalDatabaseConfiguration = - new RelationalDatabaseConfiguration( - DbConnectionString(hostContext), - SchemaCreation.INBOX_TABLE_NAME, - binaryMessagePayload:true - ); - - services.AddSingleton(relationalDatabaseConfiguration); - - var producerRegistry = new KafkaProducerRegistryFactory( - new KafkaMessagingGatewayConfiguration - { - Name = "paramore.brighter.greetingsender", - BootStrapServers = new[] { "localhost:9092" } - }, - new KafkaPublication[] - { - new KafkaPublication - { - Topic = new RoutingKey("salutationrecieved.event"), - MessageSendMaxRetries = 3, - MessageTimeoutMs = 1000, - MaxInFlightRequestsPerConnection = 1, - MakeChannels = OnMissingChannel.Create - } - } - ).Create(); - - services.AddServiceActivator(options => - { - options.Subscriptions = subscriptions; - options.UseScoped = true; - options.ChannelFactory = new ChannelFactory(consumerFactory); - options.HandlerLifetime = ServiceLifetime.Scoped; - options.MapperLifetime = ServiceLifetime.Singleton; - options.CommandProcessorLifetime = ServiceLifetime.Scoped; - options.PolicyRegistry = new SalutationPolicy(); - options.InboxConfiguration = new InboxConfiguration( - ConfigureInbox(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((configure) => - { - configure.ProducerRegistry = producerRegistry; - }) - .AutoFromAssemblies(); - - services.AddHostedService(); - } - - private static void ConfigureMigration(HostBuilderContext hostBuilderContext, IServiceCollection services) - { - if (hostBuilderContext.HostingEnvironment.IsDevelopment()) - { - services - .AddFluentMigratorCore() - .ConfigureRunner(c => - { - c.AddSQLite() - .WithGlobalConnectionString(DbConnectionString(hostBuilderContext)) - .ScanIn(typeof(Salutations_SqliteMigrations.Migrations.SqliteInitialCreate).Assembly).For.Migrations(); - }); - } - else - { - services - .AddFluentMigratorCore() - .ConfigureRunner(c => c.AddMySql5() - .WithGlobalConnectionString(DbConnectionString(hostBuilderContext)) - .ScanIn(typeof(Salutations_mySqlMigrations.Migrations.MySqlInitialCreate).Assembly).For.Migrations() - ); - } - } - - private static void ConfigureDapper(HostBuilderContext hostBuilderContext, IServiceCollection services) - { - ConfigureDapperByHost(GetDatabaseType(hostBuilderContext), services); - } - - private static void ConfigureDapperByHost(DatabaseType databaseType, 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"); - } - } - - private static void ConfigureDapperSqlite(IServiceCollection services) - { - services.AddScoped(); - services.AddScoped(); - } - - private static void ConfigureDapperMySql(IServiceCollection services) - { - services.AddScoped(); - services.AddScoped(); - } - - - private static IAmAnInbox ConfigureInbox(HostBuilderContext hostContext, IAmARelationalDatabaseConfiguration configuration) - { - if (hostContext.HostingEnvironment.IsDevelopment()) - { - return new SqliteInbox(new RelationalDatabaseConfiguration(DbConnectionString(hostContext), SchemaCreation.INBOX_TABLE_NAME)); - } - - return new MySqlInbox(new RelationalDatabaseConfiguration(DbConnectionString(hostContext), SchemaCreation.INBOX_TABLE_NAME)); - } - - 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"); - } - - private static DatabaseType GetDatabaseType(HostBuilderContext hostContext) - { - return hostContext.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") - }; - } - - - 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"); - } - } -} diff --git a/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Properties/launchSettings.json b/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Properties/launchSettings.json deleted file mode 100644 index 4154fbe3ff..0000000000 --- a/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/Properties/launchSettings.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "$schema": "http://json.schemastore.org/launchsettings.json", - "profiles": { - "Development": { - "commandName": "Project", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development", - "BRIGHTER_GREETINGS_DATABASE" : "Sqlite" - } - }, - "Production": { - "commandName": "Project", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Production", - "BRIGHTER_GREETINGS_DATABASE" : "MySQL" - } - } - } -} diff --git a/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/SalutationAnalytics.csproj b/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/SalutationAnalytics.csproj deleted file mode 100644 index 4e3d2ee4db..0000000000 --- a/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/SalutationAnalytics.csproj +++ /dev/null @@ -1,42 +0,0 @@ - - - - Exe - net6.0 - - - - - - - - - - - - - - - - - - - - - - - - - - true - PreserveNewest - PreserveNewest - - - true - PreserveNewest - PreserveNewest - - - - diff --git a/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/appsettings.Development.json b/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/appsettings.Development.json deleted file mode 100644 index 1d7a4e1ad3..0000000000 --- a/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/appsettings.Development.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Debug", - "Microsoft": "Information", - "Microsoft.Hosting.Lifetime": "Information" - } - } -} diff --git a/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/appsettings.Production.json b/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/appsettings.Production.json deleted file mode 100644 index dbbfec462a..0000000000 --- a/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/appsettings.Production.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Debug", - "System": "Warning", - "Microsoft": "Warning" - } - }, - "ConnectionStrings": { - "Salutations": "server=localhost; port=3306; uid=root; pwd=root; database=Salutations", - "SalutationsDb": "server=localhost; port=3306; uid=root; pwd=root" - } -} \ No newline at end of file diff --git a/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/appsettings.json b/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/appsettings.json deleted file mode 100644 index 27bbd50072..0000000000 --- a/samples/WebAPI_Dapper_Kafka/SalutationAnalytics/appsettings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Debug", - "System": "Information", - "Microsoft": "Information" - } - } -} \ No newline at end of file diff --git a/samples/WebAPI_Dapper_Kafka/SalutationEntities/Salutation.cs b/samples/WebAPI_Dapper_Kafka/SalutationEntities/Salutation.cs deleted file mode 100644 index b3c44a995b..0000000000 --- a/samples/WebAPI_Dapper_Kafka/SalutationEntities/Salutation.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace SalutationEntities -{ - public class Salutation - { - public long Id { get; set; } - public byte[] TimeStamp { get; set; } - public string Greeting { get; set; } - - public Salutation() { /* ORM needs to create */ } - - public Salutation(string greeting) - { - Greeting = greeting; - } - } -} diff --git a/samples/WebAPI_Dapper_Kafka/SalutationEntities/SalutationEntities.csproj b/samples/WebAPI_Dapper_Kafka/SalutationEntities/SalutationEntities.csproj deleted file mode 100644 index dbc151713b..0000000000 --- a/samples/WebAPI_Dapper_Kafka/SalutationEntities/SalutationEntities.csproj +++ /dev/null @@ -1,7 +0,0 @@ - - - - net6.0 - - - diff --git a/samples/WebAPI_Dapper_Kafka/SalutationPorts/Handlers/GreetingMadeHandler.cs b/samples/WebAPI_Dapper_Kafka/SalutationPorts/Handlers/GreetingMadeHandler.cs deleted file mode 100644 index eb4a0ff3b6..0000000000 --- a/samples/WebAPI_Dapper_Kafka/SalutationPorts/Handlers/GreetingMadeHandler.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System; -using System.Collections.Generic; -using Dapper; -using Microsoft.Extensions.Logging; -using Paramore.Brighter; -using Paramore.Brighter.Logging.Attributes; -using Paramore.Brighter.Policies.Attributes; -using SalutationEntities; -using SalutationPorts.Requests; - -namespace SalutationPorts.Handlers -{ - public class GreetingMadeHandler : RequestHandler - { - private readonly IAmATransactionConnectionProvider _transactionConnectionProvider; - private readonly IAmACommandProcessor _postBox; - private readonly ILogger _logger; - - public GreetingMadeHandler(IAmATransactionConnectionProvider transactionConnectionProvider, IAmACommandProcessor postBox, ILogger logger) - { - _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!! - [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 = _transactionConnectionProvider.GetTransaction(); - try - { - var salutation = new Salutation(@event.Greeting); - - _transactionConnectionProvider.GetConnection().Execute( - "insert into Salutation (greeting) values (@greeting)", - new {greeting = salutation.Greeting}, - tx); - - posts.Add(_postBox.DepositPost( - new SalutationReceived(DateTimeOffset.Now), - _transactionConnectionProvider)); - - tx.Commit(); - } - catch (Exception e) - { - _logger.LogError(e, "Could not save salutation"); - - //if it went wrong rollback entity write and Outbox write - tx.Rollback(); - - return base.Handle(@event); - } - - _postBox.ClearOutbox(posts.ToArray()); - - return base.Handle(@event); - } - } -} diff --git a/samples/WebAPI_Dapper_Kafka/SalutationPorts/Policies/Retry.cs b/samples/WebAPI_Dapper_Kafka/SalutationPorts/Policies/Retry.cs deleted file mode 100644 index 58013a5a3f..0000000000 --- a/samples/WebAPI_Dapper_Kafka/SalutationPorts/Policies/Retry.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using Polly; -using Polly.Contrib.WaitAndRetry; -using Polly.Retry; - -namespace SalutationPorts.Policies -{ - public static class Retry - { - public const string RETRYPOLICY = "SalutationPorts.Policies.RetryPolicy"; - public const string EXPONENTIAL_RETRYPOLICY = "SalutationPorts.Policies.ExponenttialRetryPolicy"; - - public static RetryPolicy GetSimpleHandlerRetryPolicy() - { - return Policy.Handle().WaitAndRetry(new[] - { - TimeSpan.FromMilliseconds(50), TimeSpan.FromMilliseconds(100), TimeSpan.FromMilliseconds(150) - }); - } - - public static RetryPolicy GetExponentialHandlerRetryPolicy() - { - var delay = Backoff.ExponentialBackoff(TimeSpan.FromMilliseconds(100), retryCount: 5, fastFirst: true); - return Policy.Handle().WaitAndRetry(delay); - } - } -} diff --git a/samples/WebAPI_Dapper_Kafka/SalutationPorts/Policies/SalutationPolicy.cs b/samples/WebAPI_Dapper_Kafka/SalutationPorts/Policies/SalutationPolicy.cs deleted file mode 100644 index 28024f22a7..0000000000 --- a/samples/WebAPI_Dapper_Kafka/SalutationPorts/Policies/SalutationPolicy.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Paramore.Brighter; - -namespace SalutationPorts.Policies -{ - public class SalutationPolicy : DefaultPolicy - { - public SalutationPolicy() - { - AddSalutationPolicies(); - } - - private void AddSalutationPolicies() - { - Add(Retry.RETRYPOLICY, Retry.GetSimpleHandlerRetryPolicy()); - Add(Retry.EXPONENTIAL_RETRYPOLICY, Retry.GetExponentialHandlerRetryPolicy()); - } - } -} diff --git a/samples/WebAPI_Dapper_Kafka/SalutationPorts/Requests/GreetingMade.cs b/samples/WebAPI_Dapper_Kafka/SalutationPorts/Requests/GreetingMade.cs deleted file mode 100644 index e22c75a351..0000000000 --- a/samples/WebAPI_Dapper_Kafka/SalutationPorts/Requests/GreetingMade.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using Paramore.Brighter; - -namespace SalutationPorts.Requests -{ - public class GreetingMade : Event - { - public string Greeting { get; set; } - - public GreetingMade(string greeting) : base(Guid.NewGuid()) - { - Greeting = greeting; - } - } -} diff --git a/samples/WebAPI_Dapper_Kafka/SalutationPorts/Requests/SalutationReceived.cs b/samples/WebAPI_Dapper_Kafka/SalutationPorts/Requests/SalutationReceived.cs deleted file mode 100644 index c2610ec790..0000000000 --- a/samples/WebAPI_Dapper_Kafka/SalutationPorts/Requests/SalutationReceived.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using Paramore.Brighter; - -namespace SalutationPorts.Requests -{ - public class SalutationReceived : Event - { - public DateTimeOffset ReceivedAt { get; } - - public SalutationReceived(DateTimeOffset receivedAt) : base(Guid.NewGuid()) - { - ReceivedAt = receivedAt; - } - } -} diff --git a/samples/WebAPI_Dapper_Kafka/SalutationPorts/SalutationPorts.csproj b/samples/WebAPI_Dapper_Kafka/SalutationPorts/SalutationPorts.csproj deleted file mode 100644 index 9dbe4c27ca..0000000000 --- a/samples/WebAPI_Dapper_Kafka/SalutationPorts/SalutationPorts.csproj +++ /dev/null @@ -1,17 +0,0 @@ - - - - net6.0 - - - - - - - - - - - - - diff --git a/samples/WebAPI_Dapper_Kafka/Salutations_SqliteMigrations/Migrations/202205161812_SqliteMigrations.cs b/samples/WebAPI_Dapper_Kafka/Salutations_SqliteMigrations/Migrations/202205161812_SqliteMigrations.cs deleted file mode 100644 index 0afb14f529..0000000000 --- a/samples/WebAPI_Dapper_Kafka/Salutations_SqliteMigrations/Migrations/202205161812_SqliteMigrations.cs +++ /dev/null @@ -1,20 +0,0 @@ -using FluentMigrator; - -namespace Salutations_SqliteMigrations.Migrations; - -[Migration(1)] -public class SqliteInitialCreate : 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_Kafka/Salutations_SqliteMigrations/Salutations_SqliteMigrations.csproj b/samples/WebAPI_Dapper_Kafka/Salutations_SqliteMigrations/Salutations_SqliteMigrations.csproj deleted file mode 100644 index e157295180..0000000000 --- a/samples/WebAPI_Dapper_Kafka/Salutations_SqliteMigrations/Salutations_SqliteMigrations.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - - net6.0 - enable - disable - - - - - - - \ No newline at end of file diff --git a/samples/WebAPI_Dapper_Kafka/Salutations_mySqlMigrations/Migrations/20220527_MySqlMigrations.cs b/samples/WebAPI_Dapper_Kafka/Salutations_mySqlMigrations/Migrations/20220527_MySqlMigrations.cs deleted file mode 100644 index 6b8a2c9ecc..0000000000 --- a/samples/WebAPI_Dapper_Kafka/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").AsDateTime().WithDefault(SystemMethods.CurrentDateTime); - } - - public override void Down() - { - Delete.Table("Salutation"); - } -} diff --git a/samples/WebAPI_Dapper_Kafka/Salutations_mySqlMigrations/Salutations_mySqlMigrations.csproj b/samples/WebAPI_Dapper_Kafka/Salutations_mySqlMigrations/Salutations_mySqlMigrations.csproj deleted file mode 100644 index 9107f11c61..0000000000 --- a/samples/WebAPI_Dapper_Kafka/Salutations_mySqlMigrations/Salutations_mySqlMigrations.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - - net6.0 - enable - enable - - - - - - - From 8d1b1798798bc3a3ba3b1d928b41df45c0546551 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Sat, 1 Jul 2023 19:33:13 +0100 Subject: [PATCH 84/89] Remove redundant example elements, and improve README.md --- .../.idea/httpRequests/http-requests-log.http | 194 ++++++++---------- .../Properties/launchSettings.json | 2 +- samples/WebAPI_Dapper/README.md | 109 ++++------ samples/WebAPI_Dapper/build.sh | 15 -- samples/WebAPI_Dapper_Kafka/README.md | 95 --------- samples/WebAPI_Dapper_Kafka/TODO.txt | 6 - samples/WebAPI_Dapper_Kafka/build.sh | 15 -- .../WebAPI_Dapper_Kafka/docker-compose.yml | 104 ---------- samples/WebAPI_Dapper_Kafka/tests.http | 31 --- 9 files changed, 131 insertions(+), 440 deletions(-) delete mode 100644 samples/WebAPI_Dapper/build.sh delete mode 100644 samples/WebAPI_Dapper_Kafka/README.md delete mode 100644 samples/WebAPI_Dapper_Kafka/TODO.txt delete mode 100644 samples/WebAPI_Dapper_Kafka/build.sh delete mode 100644 samples/WebAPI_Dapper_Kafka/docker-compose.yml delete mode 100644 samples/WebAPI_Dapper_Kafka/tests.http diff --git a/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http b/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http index 4301fc9a67..51d59ee800 100644 --- a/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http +++ b/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http @@ -3,6 +3,92 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip +<> 2023-07-01T185039.200.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 + +<> 2023-07-01T185036.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-01T185033.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-01T185032.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-01T185030.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-01T185021.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 + +### + +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-06-30T204010.200.json ### @@ -536,111 +622,3 @@ Accept-Encoding: br,deflate,gzip,x-gzip ### -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" -} - -<> 2023-06-27T152442.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-06-27T091728.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-06-27T091727.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-06-27T091726-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" -} - -<> 2023-06-27T091726.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-06-27T091721.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-06-27T091718.200.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 - -<> 2023-06-27T090113.200.json - -### - diff --git a/samples/WebAPI_Dapper/GreetingsWeb/Properties/launchSettings.json b/samples/WebAPI_Dapper/GreetingsWeb/Properties/launchSettings.json index b8d93cfd6d..14641d72fe 100644 --- a/samples/WebAPI_Dapper/GreetingsWeb/Properties/launchSettings.json +++ b/samples/WebAPI_Dapper/GreetingsWeb/Properties/launchSettings.json @@ -18,7 +18,7 @@ "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", "BRIGHTER_GREETINGS_DATABASE": "Sqlite", - "BRIGHTER_TRANSPORT": "RabbitMQ" + "BRIGHTER_TRANSPORT": "Kafka" } }, "ProductionMySql": { diff --git a/samples/WebAPI_Dapper/README.md b/samples/WebAPI_Dapper/README.md index b78be28dd2..317ed01f9f 100644 --- a/samples/WebAPI_Dapper/README.md +++ b/samples/WebAPI_Dapper/README.md @@ -1,30 +1,37 @@ -# 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. -*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 -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. +Configuration is via Environment variables. The following are supported: + +- 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: @@ -32,93 +39,65 @@ In case you are using Command Line Interface for running the project, consider a 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/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_Kafka/README.md b/samples/WebAPI_Dapper_Kafka/README.md deleted file mode 100644 index c095897e7e..0000000000 --- a/samples/WebAPI_Dapper_Kafka/README.md +++ /dev/null @@ -1,95 +0,0 @@ -# Table of content -- [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) -# 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. - -*Production* - runs in Docker;uses Kafka 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. - -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 Kafka 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. - -* **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. - - -## Build and Deploy - -### Building - -Use the build.sh file to: - -- 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. - -(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 common error is to change something, forget to run build.sh and use an old Docker image. - -### Deploy - -We provide a docker compose file to allow you to run a 'Production' environment or to startup Kafka and PostgreSQL for production: -```sh -docker compose up -d kafka # will just start kafka -``` - -```sh -docker compose up -d mysql # will just start mysql -``` - -and so on. - -### 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. - -## 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_Kafka/TODO.txt b/samples/WebAPI_Dapper_Kafka/TODO.txt deleted file mode 100644 index fde917f5a8..0000000000 --- a/samples/WebAPI_Dapper_Kafka/TODO.txt +++ /dev/null @@ -1,6 +0,0 @@ -# TO DO List - - - Implement for MySQL - - Consider if we need to implement migrations etc for MSSQL & Postgres? Would need a different flag from isdev to pick the db we wanted to use - - Anything else? - - Delete this file \ No newline at end of file diff --git a/samples/WebAPI_Dapper_Kafka/build.sh b/samples/WebAPI_Dapper_Kafka/build.sh deleted file mode 100644 index 052b5d5dc6..0000000000 --- a/samples/WebAPI_Dapper_Kafka/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_Kafka/docker-compose.yml b/samples/WebAPI_Dapper_Kafka/docker-compose.yml deleted file mode 100644 index fc66877111..0000000000 --- a/samples/WebAPI_Dapper_Kafka/docker-compose.yml +++ /dev/null @@ -1,104 +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$$') - zookeeper: - image: confluentinc/cp-zookeeper:latest - hostname: zookeeper - container_name: zookeeper - environment: - ZOOKEEPER_CLIENT_PORT: 2181 - ZOOKEEPER_TICK_TIME: 2000 - volumes: - - ./zoo/data:/var/lib/zookeeper/data - - ./zoo/log:/var/lib/zookeeper/log - kafka: - image: confluentinc/cp-enterprise-kafka:latest - hostname: kafka - container_name: kafka - depends_on: - - zookeeper - ports: - - "9092:9092" - - "9101:9101" - environment: - KAFKA_BROKER_ID: 1 - KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 - KAFKA_AUTO_CREATE_TOPICS_ENABLE: "false" - KAFKA_DELETE_TOPIC_ENABLE: "true" - KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT - KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092 - KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 - KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0 - KAFKA_CONFLUENT_SCHEMA_REGISTRY_URL: http://schema-registry:8081 - volumes: - - ./broker/data:/var/lib/kafka/data - - schema-registry: - image: confluentinc/cp-schema-registry:latest - hostname: schema-registry - container_name: schema-registry - depends_on: - - kafka - ports: - - "8081:8081" - environment: - SCHEMA_REGISTRY_HOST_NAME: schema-registry - SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS: 'kafka:29092' - SCHEMA_REGISTRY_LISTENERS: http://0.0.0.0:8081 - - control-center: - image: confluentinc/cp-enterprise-control-center:latest - hostname: control-center - container_name: control-center - depends_on: - - kafka - - schema-registry - ports: - - "9021:9021" - environment: - CONTROL_CENTER_BOOTSTRAP_SERVERS: 'kafka:29092' - CONTROL_CENTER_SCHEMA_REGISTRY_URL: "http://schema-registry:8081" - CONTROL_CENTER_REPLICATION_FACTOR: 1 - CONTROL_CENTER_INTERNAL_TOPICS_PARTITIONS: 1 - CONTROL_CENTER_MONITORING_INTERCEPTOR_TOPIC_PARTITIONS: 1 - CONFLUENT_METRICS_TOPIC_REPLICATION: 1 - PORT: 9021 - -volumes: - my-db: - driver: local - - diff --git a/samples/WebAPI_Dapper_Kafka/tests.http b/samples/WebAPI_Dapper_Kafka/tests.http deleted file mode 100644 index 56382375bb..0000000000 --- a/samples/WebAPI_Dapper_Kafka/tests.http +++ /dev/null @@ -1,31 +0,0 @@ -### Clean up between runs if needed - -DELETE http://localhost:5000/People/Tyrion HTTP/1.1 - -### Add a Person - -POST http://localhost:5000/People/new HTTP/1.1 -Content-Type: application/json - -{ - "Name" : "Tyrion" -} - -### Now see that person - -GET http://localhost:5000/People/Tyrion HTTP/1.1 - - -### Now add some more greetings - -POST http://localhost:5000/Greetings/Tyrion/new HTTP/1.1 -Content-Type: application/json - -{ - "Greeting" : "I drink, and I know things" -} - -### And now look up Tyrion's greetings - -GET http://localhost:5000/Greetings/Tyrion HTTP/1.1 - From 1b1a09957be5f0a99f2f54979dcd8f5244f56171 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Tue, 4 Jul 2023 20:24:21 +0100 Subject: [PATCH 85/89] Migrate ef core examples;fix issues with target framework type --- .../.idea/httpRequests/http-requests-log.http | 246 ++++++++---------- samples/WebAPI_Dapper/README.md | 1 + .../GreetingsEntities.csproj | 2 +- .../GreetingsPorts/GreetingsPorts.csproj | 2 +- .../GreetingsWeb/Database/SchemaCreation.cs | 3 +- .../GreetingsWeb/GreetingsWeb.csproj | 4 +- samples/WebAPI_EFCore/GreetingsWeb/Startup.cs | 200 +++++++------- .../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.csproj | 2 +- .../SalutationEntities.csproj | 2 +- .../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 ---- ....Brighter.MsSql.EntityFrameworkCore.csproj | 2 +- ....Brighter.MySql.EntityFrameworkCore.csproj | 2 +- ...hter.PostgreSql.EntityFrameworkCore.csproj | 2 +- .../Paramore.Brighter.PostgreSql.csproj | 1 + ...Brighter.Sqlite.EntityFrameworkCore.csproj | 2 +- 26 files changed, 276 insertions(+), 371 deletions(-) delete mode 100644 samples/WebAPI_EFCore/build.sh delete mode 100644 samples/WebAPI_EFCore/docker-compose.yml diff --git a/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http b/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http index 51d59ee800..c7a060a3c7 100644 --- a/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http +++ b/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http @@ -1,21 +1,3 @@ -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-01T185039.200.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 - -<> 2023-07-01T185036.200.json - -### - POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json Content-Length: 47 @@ -27,7 +9,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-07-01T185033.200.json +<> 2023-07-04T202333.200.json ### @@ -42,7 +24,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-07-01T185032.200.json +<> 2023-07-04T202332.200.json ### @@ -57,7 +39,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-07-01T185030.200.json +<> 2023-07-04T202330.200.json ### @@ -66,7 +48,7 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-07-01T185021.200.json +<> 2023-07-04T202327.200.json ### @@ -75,61 +57,51 @@ 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 + ### -GET http://localhost:5000/People/Tyrion +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" +} + +<> 2023-07-04T201859.200.json + ### -GET http://localhost:5000/Greetings/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 -<> 2023-06-30T204010.200.json - ### -POST http://localhost:5000/Greetings/Tyrion/new -Content-Type: application/json -Content-Length: 47 +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-06-30T204007.200.json - ### -GET http://localhost:5000/Greetings/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 -<> 2023-06-30T203955.200.json - ### -POST http://localhost:5000/Greetings/Tyrion/new -Content-Type: application/json -Content-Length: 47 +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-06-30T203951.200.json - ### GET http://localhost:5000/People/Tyrion @@ -137,7 +109,7 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-06-30T203938.200.json +<> 2023-07-03T093743.500.json ### @@ -152,7 +124,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Name" : "Tyrion" } -<> 2023-06-30T203936.200.json +<> 2023-07-02T190050.500.json ### @@ -161,37 +133,25 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-06-30T203932.404.json +<> 2023-07-02T190040.500.json ### -POST http://localhost:5000/Greetings/Tyrion/new -Content-Type: application/json -Content-Length: 47 +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-06-28T192431.200.json +<> 2023-07-01T185039.200.json ### -POST http://localhost:5000/Greetings/Tyrion/new -Content-Type: application/json -Content-Length: 47 +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-06-28T192225.200.json +<> 2023-07-01T185036.200.json ### @@ -206,7 +166,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-28T190130.200.json +<> 2023-07-01T185033.200.json ### @@ -221,6 +181,8 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } +<> 2023-07-01T185032.200.json + ### POST http://localhost:5000/Greetings/Tyrion/new @@ -234,51 +196,39 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } +<> 2023-07-01T185030.200.json + ### -POST http://localhost:5000/Greetings/Tyrion/new -Content-Type: application/json -Content-Length: 47 +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-01T185021.200.json ### -POST http://localhost:5000/Greetings/Tyrion/new -Content-Type: application/json -Content-Length: 47 +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-06-28T182132.200.json - ### -GET http://localhost:5000/Greetings/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 -<> 2023-06-28T182115.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 -<> 2023-06-28T182112.200.json +<> 2023-06-30T204010.200.json ### @@ -293,22 +243,16 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-28T181931.200.json +<> 2023-06-30T204007.200.json ### -POST http://localhost:5000/Greetings/Tyrion/new -Content-Type: application/json -Content-Length: 47 +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-06-28T181824.200.json +<> 2023-06-30T203955.200.json ### @@ -323,31 +267,31 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-28T180308.200.json +<> 2023-06-30T203951.200.json ### -GET http://localhost:5000/Greetings/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 -<> 2023-06-28T180149.200.json +<> 2023-06-30T203938.200.json ### -POST http://localhost:5000/Greetings/Tyrion/new +POST http://localhost:5000/People/new Content-Type: application/json -Content-Length: 47 +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" } -<> 2023-06-28T180148.200.json +<> 2023-06-30T203936.200.json ### @@ -356,7 +300,7 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-06-28T180142.200.json +<> 2023-06-30T203932.404.json ### @@ -371,7 +315,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-28T094747.200.json +<> 2023-06-28T192431.200.json ### @@ -386,7 +330,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-28T094744.200.json +<> 2023-06-28T192225.200.json ### @@ -401,16 +345,20 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-28T094741.200.json +<> 2023-06-28T190130.200.json ### -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 -<> 2023-06-28T094734.200.json +{ + "Greeting" : "I drink, and I know things" +} ### @@ -425,8 +373,6 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-28T083529.200.json - ### POST http://localhost:5000/Greetings/Tyrion/new @@ -440,8 +386,6 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-28T083525.200.json - ### POST http://localhost:5000/Greetings/Tyrion/new @@ -455,7 +399,16 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-28T083523.200.json +<> 2023-06-28T182132.200.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 + +<> 2023-06-28T182115.200.json ### @@ -464,7 +417,7 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-06-28T083517.200.json +<> 2023-06-28T182112.200.json ### @@ -479,7 +432,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-28T080820.200.json +<> 2023-06-28T181931.200.json ### @@ -494,7 +447,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-28T080816.200.json +<> 2023-06-28T181824.200.json ### @@ -509,7 +462,16 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-28T080812.500.json +<> 2023-06-28T180308.200.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 + +<> 2023-06-28T180149.200.json ### @@ -524,7 +486,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-28T080809.200.json +<> 2023-06-28T180148.200.json ### @@ -533,7 +495,7 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-06-28T080804.200.json +<> 2023-06-28T180142.200.json ### @@ -548,14 +510,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -### - -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-06-27T152459.200.json +<> 2023-06-28T094747.200.json ### @@ -570,7 +525,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-27T152457.200.json +<> 2023-06-28T094744.200.json ### @@ -585,7 +540,16 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-27T152456.200.json +<> 2023-06-28T094741.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-06-28T094734.200.json ### @@ -600,25 +564,37 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-27T152453.200.json +<> 2023-06-28T083529.200.json ### -GET http://localhost:5000/Greetings/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 -<> 2023-06-27T152451.200.json +{ + "Greeting" : "I drink, and I know things" +} + +<> 2023-06-28T083525.200.json ### -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 -<> 2023-06-27T152446.200.json +{ + "Greeting" : "I drink, and I know things" +} + +<> 2023-06-28T083523.200.json ### diff --git a/samples/WebAPI_Dapper/README.md b/samples/WebAPI_Dapper/README.md index 317ed01f9f..de34cf2c7d 100644 --- a/samples/WebAPI_Dapper/README.md +++ b/samples/WebAPI_Dapper/README.md @@ -38,6 +38,7 @@ In case you are using Command Line Interface for running the project, consider a ```sh dotnet run --launch-profile XXXXXX -d ``` + ## Architecture ### GreetingsAPI 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 1431eba4c3..581b9df3f9 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/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 33715dc036..149c2db21c 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 9e868ca01e..a242ad1d3a 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,87 +103,38 @@ private void CheckDbIsUp() private void ConfigureBrighter(IServiceCollection services) { - if (_env.IsDevelopment()) - { - 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(); - - 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 = - new SqliteOutbox( - new RelationalDatabaseConfiguration(DbConnectionString(), _outBoxTableName)); - configure.TransactionProvider = typeof(SqliteUnitOfWork); - } - ) - .UseOutboxSweeper(options => - { - options.TimerInterval = 5; - options.MinimumMessageAge = 5000; - }) - .AutoFromAssemblies(); - } - else - { - IAmAProducerRegistry producerRegistry = 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(); - - services.AddBrighter(options => - { - options.HandlerLifetime = ServiceLifetime.Scoped; - options.MapperLifetime = ServiceLifetime.Singleton; - options.PolicyRegistry = new GreetingsPolicy(); - }) - .UseExternalBus((config) => - { - config.ProducerRegistry = producerRegistry; - config.Outbox = - new MySqlOutbox( - new RelationalDatabaseConfiguration(DbConnectionString(), _outBoxTableName)); - config.TransactionProvider = typeof(MySqlUnitOfWork); - } - ) - .UseOutboxSweeper() - .AutoFromAssemblies(); - } + (IAmAnOutbox outbox, Type transactionProvider, Type connectionProvider) = MakeOutbox(); + + IAmAProducerRegistry producerRegistry = ConfigureProducerRegistry(); + + var outboxConfiguration = new RelationalDatabaseConfiguration( + DbConnectionString() + ); + services.AddSingleton(outboxConfiguration); + 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) @@ -202,7 +147,6 @@ private void ConfigureDarker(IServiceCollection services) .AddHandlersFromAssemblies(typeof(FindPersonByNameHandlerAsync).Assembly) .AddJsonQueryLogging() .AddPolicies(new GreetingsPolicy()); - } private void ConfigureEFCore(IServiceCollection services) @@ -214,33 +158,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 e12fb758ff..977d72402c 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/SalutationAnalytics.csproj b/samples/WebAPI_EFCore/SalutationAnalytics/SalutationAnalytics.csproj index 8de828d38c..bad76f092f 100644 --- a/samples/WebAPI_EFCore/SalutationAnalytics/SalutationAnalytics.csproj +++ b/samples/WebAPI_EFCore/SalutationAnalytics/SalutationAnalytics.csproj @@ -2,7 +2,7 @@ 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/SalutationPorts.csproj b/samples/WebAPI_EFCore/SalutationPorts/SalutationPorts.csproj index aeec1415a1..bd893744c5 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 e35e6bc62e..22f48f9b25 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/src/Paramore.Brighter.MsSql.EntityFrameworkCore/Paramore.Brighter.MsSql.EntityFrameworkCore.csproj b/src/Paramore.Brighter.MsSql.EntityFrameworkCore/Paramore.Brighter.MsSql.EntityFrameworkCore.csproj index 510d92c4bd..7f981bec38 100644 --- a/src/Paramore.Brighter.MsSql.EntityFrameworkCore/Paramore.Brighter.MsSql.EntityFrameworkCore.csproj +++ b/src/Paramore.Brighter.MsSql.EntityFrameworkCore/Paramore.Brighter.MsSql.EntityFrameworkCore.csproj @@ -3,7 +3,7 @@ This is the MsSql Connection provider to obtain the connection from Entity Framework Core. Paul Reardon - netstandard2.1;net6.0 + netstandard2.1;net6.0;net7.0 RabbitMQ;AMQP;Command;Event;Service Activator;Decoupled;Invocation;Messaging;Remote;Command Dispatcher;Command Processor;Request;Service;Task Queue;Work Queue;Retry;Circuit Breaker;Availability diff --git a/src/Paramore.Brighter.MySql.EntityFrameworkCore/Paramore.Brighter.MySql.EntityFrameworkCore.csproj b/src/Paramore.Brighter.MySql.EntityFrameworkCore/Paramore.Brighter.MySql.EntityFrameworkCore.csproj index a83c3caeb5..e3032ecafa 100644 --- a/src/Paramore.Brighter.MySql.EntityFrameworkCore/Paramore.Brighter.MySql.EntityFrameworkCore.csproj +++ b/src/Paramore.Brighter.MySql.EntityFrameworkCore/Paramore.Brighter.MySql.EntityFrameworkCore.csproj @@ -1,7 +1,7 @@  - netstandard2.1;net6.0 + netstandard2.1;net6.0;net7.0 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 7da55bf29d..643fb54172 100644 --- a/src/Paramore.Brighter.PostgreSql.EntityFrameworkCore/Paramore.Brighter.PostgreSql.EntityFrameworkCore.csproj +++ b/src/Paramore.Brighter.PostgreSql.EntityFrameworkCore/Paramore.Brighter.PostgreSql.EntityFrameworkCore.csproj @@ -3,7 +3,7 @@ Sam Rumley Common components required to get a PostgreSql connection from Entity Framework Core. - netstandard2.1;net6.0 + netstandard2.1;net6.0;net7.0 RabbitMQ;AMQP;Command;Event;Service Activator;Decoupled;Invocation;Messaging;Remote;Command Dispatcher;Command Processor;Request;Service;Task Queue;Work Queue;Retry;Circuit Breaker;Availability diff --git a/src/Paramore.Brighter.PostgreSql/Paramore.Brighter.PostgreSql.csproj b/src/Paramore.Brighter.PostgreSql/Paramore.Brighter.PostgreSql.csproj index 724fdbf0c0..dc94da54a3 100644 --- a/src/Paramore.Brighter.PostgreSql/Paramore.Brighter.PostgreSql.csproj +++ b/src/Paramore.Brighter.PostgreSql/Paramore.Brighter.PostgreSql.csproj @@ -12,6 +12,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/Paramore.Brighter.Sqlite.EntityFrameworkCore/Paramore.Brighter.Sqlite.EntityFrameworkCore.csproj b/src/Paramore.Brighter.Sqlite.EntityFrameworkCore/Paramore.Brighter.Sqlite.EntityFrameworkCore.csproj index b0cb76ddba..21e8e869ac 100644 --- a/src/Paramore.Brighter.Sqlite.EntityFrameworkCore/Paramore.Brighter.Sqlite.EntityFrameworkCore.csproj +++ b/src/Paramore.Brighter.Sqlite.EntityFrameworkCore/Paramore.Brighter.Sqlite.EntityFrameworkCore.csproj @@ -2,7 +2,7 @@ Ian Cooper - net6.0;netstandard2.1 + netstandard2.1;net6.0;net7.0 RabbitMQ;AMQP;Command;Event;Service Activator;Decoupled;Invocation;Messaging;Remote;Command Dispatcher;Command Processor;Request;Service;Task Queue;Work Queue;Retry;Circuit Breaker;Availability From 07041286968132494f49ca486721feb238428bd3 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Wed, 5 Jul 2023 08:42:25 +0100 Subject: [PATCH 86/89] Get SalutationAnalytics.csproj working with hostbuilder changes --- .../.idea/httpRequests/http-requests-log.http | 114 +++++++++--------- samples/WebAPI_EFCore/GreetingsWeb/Startup.cs | 7 +- .../SalutationAnalytics/Program.cs | 89 ++++++++++---- .../SalutationAnalytics.csproj | 2 + .../Handlers/GreetingMadeHandler.cs | 29 +++-- .../SalutationPorts/Policies/Retry.cs | 12 +- .../Policies/SalutationPolicy.cs | 4 +- 7 files changed, 150 insertions(+), 107 deletions(-) diff --git a/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http b/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http index c7a060a3c7..584b8249e0 100644 --- a/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http +++ b/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http @@ -9,6 +9,66 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } +<> 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" +} + +<> 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" +} + +<> 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" +} + +<> 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" +} + <> 2023-07-04T202333.200.json ### @@ -544,57 +604,3 @@ 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 - -<> 2023-06-28T094734.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-06-28T083529.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-06-28T083525.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-06-28T083523.200.json - -### - diff --git a/samples/WebAPI_EFCore/GreetingsWeb/Startup.cs b/samples/WebAPI_EFCore/GreetingsWeb/Startup.cs index a242ad1d3a..60376552e2 100644 --- a/samples/WebAPI_EFCore/GreetingsWeb/Startup.cs +++ b/samples/WebAPI_EFCore/GreetingsWeb/Startup.cs @@ -104,13 +104,10 @@ private void CheckDbIsUp() private void ConfigureBrighter(IServiceCollection services) { (IAmAnOutbox outbox, Type transactionProvider, Type connectionProvider) = MakeOutbox(); + var outboxConfiguration = new RelationalDatabaseConfiguration(DbConnectionString()); + services.AddSingleton(outboxConfiguration); IAmAProducerRegistry producerRegistry = ConfigureProducerRegistry(); - - var outboxConfiguration = new RelationalDatabaseConfiguration( - DbConnectionString() - ); - services.AddSingleton(outboxConfiguration); services.AddBrighter(options => { diff --git a/samples/WebAPI_EFCore/SalutationAnalytics/Program.cs b/samples/WebAPI_EFCore/SalutationAnalytics/Program.cs index 7e2bbd271d..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; @@ -61,43 +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 }; - 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); - - var producerRegistry = new RmqProducerRegistryFactory( - rmqConnection, - new RmqPublication[] - { - new RmqPublication - { - Topic = new RoutingKey("SalutationReceived"), - MaxOutStandingMessages = 5, - MaxOutStandingCheckIntervalMilliSeconds = 500, - WaitForConfirmsTimeOutInMilliseconds = 1000, - MakeChannels = OnMissingChannel.Create - } - } - ).Create(); + AmpqUri = new AmqpUriSpecification(new Uri("amqp://guest:guest@localhost:5672")), + Exchange = new Exchange("paramore.brighter.exchange"), + }); services.AddServiceActivator(options => { @@ -118,12 +110,16 @@ private static void ConfigureBrighter(HostBuilderContext hostContext, IServiceCo .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 @@ -172,6 +168,31 @@ private static IAmAnInbox ConfigureInbox(HostBuilderContext hostContext) 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) { @@ -180,5 +201,23 @@ private static string DbConnectionString(HostBuilderContext hostContext) ? "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 bad76f092f..75088f4585 100644 --- a/samples/WebAPI_EFCore/SalutationAnalytics/SalutationAnalytics.csproj +++ b/samples/WebAPI_EFCore/SalutationAnalytics/SalutationAnalytics.csproj @@ -9,11 +9,13 @@ + + diff --git a/samples/WebAPI_EFCore/SalutationPorts/Handlers/GreetingMadeHandler.cs b/samples/WebAPI_EFCore/SalutationPorts/Handlers/GreetingMadeHandler.cs index 109f5bf588..ecb171a58d 100644 --- a/samples/WebAPI_EFCore/SalutationPorts/Handlers/GreetingMadeHandler.cs +++ b/samples/WebAPI_EFCore/SalutationPorts/Handlers/GreetingMadeHandler.cs @@ -12,48 +12,47 @@ 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, IAmATransactionConnectionProvider provider, 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(); - await _transactionProvider.GetTransactionAsync(cancellationToken); + var tx =_transactionProvider.GetTransaction(); try { var salutation = new Salutation(@event.Greeting); _uow.Salutations.Add(salutation); - posts.Add(await _postBox.DepositPostAsync( + posts.Add(_postBox.DepositPost( new SalutationReceived(DateTimeOffset.Now), - _transactionProvider, - cancellationToken: cancellationToken) + _transactionProvider) ); - await _uow.SaveChangesAsync(cancellationToken); + _uow.SaveChanges(); - await _transactionProvider.CommitAsync(cancellationToken); + _transactionProvider.Commit(); } catch (Exception e) { Console.WriteLine(e); - await _transactionProvider.RollbackAsync(cancellationToken); + _transactionProvider.Rollback(); Console.WriteLine("Salutation analytical record not saved"); @@ -64,9 +63,9 @@ public override async Task HandleAsync(GreetingMade @event, Cancel _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()); } } } From 25a1d67d7e5ae564d27fab90d82bab92ce5c9f61 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Fri, 7 Jul 2023 20:01:48 +0100 Subject: [PATCH 87/89] Fix DynamoDb sample and tools --- .../.idea/httpRequests/http-requests-log.http | 280 +++++++++--------- Docker/dynamodb/shared-local-instance.db | Bin 552960 -> 552960 bytes .../Handlers/AddGreetingHandlerAsync.cs | 39 ++- .../Handlers/AddPersonHandlerAsync.cs | 8 +- .../Handlers/DeletePersonHandlerAsync.cs | 8 +- .../FIndGreetingsForPersonHandlerAsync.cs | 8 +- .../Handlers/FindPersonByNameHandlerAsync.cs | 8 +- samples/WebAPI_Dynamo/GreetingsWeb/Startup.cs | 1 + .../SalutationAnalytics/Program.cs | 3 +- .../Handlers/GreetingMadeHandler.cs | 58 ++-- .../SalutationPorts/Policies/Retry.cs | 12 +- .../Policies/SalutationPolicy.cs | 4 +- samples/WebAPI_Dynamo/tests.http | 2 +- .../DynamoDbTableBuilder.cs | 2 +- ...pDbTableQuery.cs => DynamoDbTableQuery.cs} | 2 +- .../DynamoDbUnitOfWork.cs | 21 +- .../IAmADynamoDbConnectionProvider.cs | 15 + .../IAmADynamoDbTransactionProvider.cs | 10 + .../IDynamoDbClientProvider.cs | 27 -- .../IDynamoDbTransactionProvider.cs | 9 - ...hter.Extensions.DependencyInjection.csproj | 1 + .../ServiceCollectionExtensions.cs | 29 +- .../CommittableTransactionProvider.cs | 5 + .../IAmABoxTransactionProvider.cs | 5 + .../IAmARelationalDbConnectionProvider.cs | 12 - ...a_transaction_between_outbox_and_entity.cs | 3 +- 26 files changed, 297 insertions(+), 275 deletions(-) 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 diff --git a/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http b/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http index 584b8249e0..3666dfe013 100644 --- a/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http +++ b/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http @@ -1,45 +1,43 @@ POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json -Content-Length: 47 +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" } -<> 2023-07-04T212401.200.json - ### POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json -Content-Length: 47 +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" } -<> 2023-07-04T212358.200.json +<> 2023-07-07T195459.200.json ### POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json -Content-Length: 47 +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" } -<> 2023-07-04T212356.200.json +<> 2023-07-07T192903.200.json ### @@ -54,22 +52,16 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-07-04T211455.200.json +<> 2023-07-07T190420.200.json ### -POST http://localhost:5000/Greetings/Tyrion/new -Content-Type: application/json -Content-Length: 47 +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-04T202333.200.json +<> 2023-07-07T190355.200.json ### @@ -84,77 +76,95 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-07-04T202332.200.json +<> 2023-07-07T190352.200.json ### -POST http://localhost:5000/Greetings/Tyrion/new -Content-Type: application/json -Content-Length: 47 +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-04T202330.200.json +<> 2023-07-07T190349.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 -<> 2023-07-04T202327.200.json +<> 2023-07-07T165603.200.json ### -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 -<> 2023-07-04T201903.200.json +{ + "Greeting" : "I drink, and I know things" +} + +<> 2023-07-07T165600.200.json ### -POST http://localhost:5000/People/new +POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json -Content-Length: 23 +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" } -<> 2023-07-04T201859.200.json +<> 2023-07-07T165559.200.json ### -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 +{ + "Greeting" : "I drink, and I know things" +} + +<> 2023-07-07T165557.200.json + ### -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 +{ + "Greeting" : "I drink, and I know things" +} + +<> 2023-07-07T165555.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 +<> 2023-07-07T165552.200.json + ### GET http://localhost:5000/People/Tyrion @@ -162,6 +172,8 @@ 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 + ### GET http://localhost:5000/People/Tyrion @@ -169,7 +181,7 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-07-03T093743.500.json +<> 2023-07-07T165406.200.json ### @@ -184,7 +196,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Name" : "Tyrion" } -<> 2023-07-02T190050.500.json +<> 2023-07-07T165401.200.json ### @@ -193,25 +205,37 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-07-02T190040.500.json +<> 2023-07-07T165354.500.json ### -GET http://localhost:5000/Greetings/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 -<> 2023-07-01T185039.200.json +{ + "Greeting" : "I drink, and I know things" +} + +<> 2023-07-04T212401.200.json ### -GET http://localhost:5000/Greetings/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 -<> 2023-07-01T185036.200.json +{ + "Greeting" : "I drink, and I know things" +} + +<> 2023-07-04T212358.200.json ### @@ -226,7 +250,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-07-01T185033.200.json +<> 2023-07-04T212356.200.json ### @@ -241,7 +265,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-07-01T185032.200.json +<> 2023-07-04T211455.200.json ### @@ -256,24 +280,38 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-07-01T185030.200.json +<> 2023-07-04T202333.200.json ### -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 -<> 2023-07-01T185021.200.json +{ + "Greeting" : "I drink, and I know things" +} + +<> 2023-07-04T202332.200.json ### -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 +{ + "Greeting" : "I drink, and I know things" +} + +<> 2023-07-04T202330.200.json + ### GET http://localhost:5000/People/Tyrion @@ -281,54 +319,46 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip +<> 2023-07-04T202327.200.json + ### -GET http://localhost:5000/Greetings/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 -<> 2023-06-30T204010.200.json +<> 2023-07-04T201903.200.json ### -POST http://localhost:5000/Greetings/Tyrion/new +POST http://localhost:5000/People/new Content-Type: application/json -Content-Length: 47 +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" } -<> 2023-06-30T204007.200.json +<> 2023-07-04T201859.200.json ### -GET http://localhost:5000/Greetings/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 -<> 2023-06-30T203955.200.json - ### -POST http://localhost:5000/Greetings/Tyrion/new -Content-Type: application/json -Content-Length: 47 +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-06-30T203951.200.json - ### GET http://localhost:5000/People/Tyrion @@ -336,23 +366,13 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-06-30T203938.200.json - ### -POST http://localhost:5000/People/new -Content-Type: application/json -Content-Length: 23 +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 -{ - "Name" : "Tyrion" -} - -<> 2023-06-30T203936.200.json - ### GET http://localhost:5000/People/Tyrion @@ -360,65 +380,49 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-06-30T203932.404.json +<> 2023-07-03T093743.500.json ### -POST http://localhost:5000/Greetings/Tyrion/new +POST http://localhost:5000/People/new Content-Type: application/json -Content-Length: 47 +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" } -<> 2023-06-28T192431.200.json +<> 2023-07-02T190050.500.json ### -POST http://localhost:5000/Greetings/Tyrion/new -Content-Type: application/json -Content-Length: 47 +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-06-28T192225.200.json +<> 2023-07-02T190040.500.json ### -POST http://localhost:5000/Greetings/Tyrion/new -Content-Type: application/json -Content-Length: 47 +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-06-28T190130.200.json +<> 2023-07-01T185039.200.json ### -POST http://localhost:5000/Greetings/Tyrion/new -Content-Type: application/json -Content-Length: 47 +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-01T185036.200.json ### @@ -433,6 +437,8 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } +<> 2023-07-01T185033.200.json + ### POST http://localhost:5000/Greetings/Tyrion/new @@ -446,6 +452,8 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } +<> 2023-07-01T185032.200.json + ### POST http://localhost:5000/Greetings/Tyrion/new @@ -459,16 +467,16 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-28T182132.200.json +<> 2023-07-01T185030.200.json ### -GET http://localhost:5000/Greetings/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 -<> 2023-06-28T182115.200.json +<> 2023-07-01T185021.200.json ### @@ -477,37 +485,21 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-06-28T182112.200.json - ### -POST http://localhost:5000/Greetings/Tyrion/new -Content-Type: application/json -Content-Length: 47 +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-06-28T181931.200.json - ### -POST http://localhost:5000/Greetings/Tyrion/new -Content-Type: application/json -Content-Length: 47 +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-06-28T181824.200.json +<> 2023-06-30T204010.200.json ### @@ -522,7 +514,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-28T180308.200.json +<> 2023-06-30T204007.200.json ### @@ -531,7 +523,7 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-06-28T180149.200.json +<> 2023-06-30T203955.200.json ### @@ -546,7 +538,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-28T180148.200.json +<> 2023-06-30T203951.200.json ### @@ -555,37 +547,31 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-06-28T180142.200.json +<> 2023-06-30T203938.200.json ### -POST http://localhost:5000/Greetings/Tyrion/new +POST http://localhost:5000/People/new Content-Type: application/json -Content-Length: 47 +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" } -<> 2023-06-28T094747.200.json +<> 2023-06-30T203936.200.json ### -POST http://localhost:5000/Greetings/Tyrion/new -Content-Type: application/json -Content-Length: 47 +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-06-28T094744.200.json +<> 2023-06-30T203932.404.json ### @@ -600,7 +586,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-06-28T094741.200.json +<> 2023-06-28T192431.200.json ### diff --git a/Docker/dynamodb/shared-local-instance.db b/Docker/dynamodb/shared-local-instance.db index f88a226447c36374f2462fbfe850838387e01705..b9617ae6bd9cdb74d7ca538aa41336f6f16e6937 100644 GIT binary patch delta 17499 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 delta 12696 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 diff --git a/samples/WebAPI_Dynamo/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs b/samples/WebAPI_Dynamo/GreetingsPorts/Handlers/AddGreetingHandlerAsync.cs index 2f3748ca2b..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(IAmABoxTransactionProvider uow, IAmACommandProcessor postBox, ILogger logger) + public AddGreetingHandlerAsync(IAmADynamoDbTransactionProvider transactionProvider, IAmACommandProcessor postBox, ILogger logger) { - _unitOfWork = (DynamoDbUnitOfWork)uow; + _transactionProvider = transactionProvider; _postBox = postBox; _logger = logger; } @@ -36,37 +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 = await _unitOfWork.GetTransactionAsync(cancellationToken); + 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), - _unitOfWork, + 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 - await _unitOfWork.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_Dynamo/GreetingsPorts/Handlers/AddPersonHandlerAsync.cs b/samples/WebAPI_Dynamo/GreetingsPorts/Handlers/AddPersonHandlerAsync.cs index c6eee8a174..90df9aa8ad 100644 --- a/samples/WebAPI_Dynamo/GreetingsPorts/Handlers/AddPersonHandlerAsync.cs +++ b/samples/WebAPI_Dynamo/GreetingsPorts/Handlers/AddPersonHandlerAsync.cs @@ -13,18 +13,18 @@ namespace GreetingsPorts.Handlers { public class AddPersonHandlerAsync : RequestHandlerAsync { - private readonly DynamoDbUnitOfWork _dynamoDbUnitOfWork; + private readonly IAmADynamoDbConnectionProvider _dynamoDbConnectionProvider; - public AddPersonHandlerAsync(IAmABoxTransactionProvider 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 8a300aa5ff..af1fdbecc2 100644 --- a/samples/WebAPI_Dynamo/GreetingsPorts/Handlers/DeletePersonHandlerAsync.cs +++ b/samples/WebAPI_Dynamo/GreetingsPorts/Handlers/DeletePersonHandlerAsync.cs @@ -12,18 +12,18 @@ namespace GreetingsPorts.Handlers { public class DeletePersonHandlerAsync : RequestHandlerAsync { - private readonly DynamoDbUnitOfWork _unitOfWork; + private readonly IAmADynamoDbConnectionProvider _dynamoDbConnectionProvider; - public DeletePersonHandlerAsync(IAmABoxTransactionProvider 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 67bded1dd0..feb9290b80 100644 --- a/samples/WebAPI_Dynamo/GreetingsPorts/Handlers/FIndGreetingsForPersonHandlerAsync.cs +++ b/samples/WebAPI_Dynamo/GreetingsPorts/Handlers/FIndGreetingsForPersonHandlerAsync.cs @@ -17,18 +17,18 @@ namespace GreetingsPorts.Handlers { public class FIndGreetingsForPersonHandlerAsync : QueryHandlerAsync { - private readonly DynamoDbUnitOfWork _unitOfWork; + private readonly IAmADynamoDbConnectionProvider _dynamoDbConnectionProvider; - public FIndGreetingsForPersonHandlerAsync(IAmABoxTransactionProvider 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 b5d23ce38d..b12ab3abc6 100644 --- a/samples/WebAPI_Dynamo/GreetingsPorts/Handlers/FindPersonByNameHandlerAsync.cs +++ b/samples/WebAPI_Dynamo/GreetingsPorts/Handlers/FindPersonByNameHandlerAsync.cs @@ -18,18 +18,18 @@ namespace GreetingsPorts.Handlers { public class FindPersonByNameHandlerAsync : QueryHandlerAsync { - private readonly DynamoDbUnitOfWork _unitOfWork; + private readonly IAmADynamoDbConnectionProvider _dynamoDbConnectionProvider; - public FindPersonByNameHandlerAsync(IAmABoxTransactionProvider 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 153780462e..5784b301fe 100644 --- a/samples/WebAPI_Dynamo/GreetingsWeb/Startup.cs +++ b/samples/WebAPI_Dynamo/GreetingsWeb/Startup.cs @@ -191,6 +191,7 @@ private void ConfigureBrighter(IServiceCollection services) { 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"); }) diff --git a/samples/WebAPI_Dynamo/SalutationAnalytics/Program.cs b/samples/WebAPI_Dynamo/SalutationAnalytics/Program.cs index fe7c780cb1..c6679a9ecd 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 @@ -124,6 +124,7 @@ private static void ConfigureBrighter( { configure.ProducerRegistry = producerRegistry; configure.Outbox = ConfigureOutbox(awsCredentials, dynamoDb); + configure.ConnectionProvider = typeof(DynamoDbUnitOfWork); configure.TransactionProvider = typeof(DynamoDbUnitOfWork); } ) diff --git a/samples/WebAPI_Dynamo/SalutationPorts/Handlers/GreetingMadeHandler.cs b/samples/WebAPI_Dynamo/SalutationPorts/Handlers/GreetingMadeHandler.cs index fce98f0035..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,53 +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(IAmABoxTransactionProvider 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 = await _uow.GetTransactionAsync(cancellationToken); + 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), - _uow, - 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"); - await _uow.RollbackAsync(cancellationToken); - - 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/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 96c2b3af01..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; @@ -46,14 +47,22 @@ public void Commit() /// /// Commit a transaction, performing all associated write actions /// - /// A response indicating the status of the transaction - public async Task CommitAsync(CancellationToken ct = default) + /// A cancellation token + /// + public async Task CommitAsync(CancellationToken ct = default) { if (!HasOpenTransaction) throw new InvalidOperationException("No transaction to commit"); - - LastResponse = await DynamoDb.TransactWriteItemsAsync(_tx, ct); - return LastResponse; + 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}"); + } } 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 02a09ea14b..0000000000 --- a/src/Paramore.Brighter.DynamoDb/IDynamoDbClientProvider.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; -using Amazon.DynamoDBv2; -using Amazon.DynamoDBv2.Model; - -namespace Paramore.Brighter.DynamoDb -{ - public interface IDynamoDbClientProvider - { - /// - /// Commits the transaction to Dynamo - /// - /// A cancellation token - /// - Task CommitAsync(CancellationToken ct = default); - - /// - /// The AWS client for dynamoDb - /// - IAmazonDynamoDB DynamoDb { get; } - - /// - /// The response for the last transaction commit - /// - TransactWriteItemsResponse LastResponse { get; set; } - } -} diff --git a/src/Paramore.Brighter.DynamoDb/IDynamoDbTransactionProvider.cs b/src/Paramore.Brighter.DynamoDb/IDynamoDbTransactionProvider.cs deleted file mode 100644 index a979d57246..0000000000 --- a/src/Paramore.Brighter.DynamoDb/IDynamoDbTransactionProvider.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Amazon.DynamoDBv2.Model; - -namespace Paramore.Brighter.DynamoDb -{ - public interface IDynamoDbClientTransactionProvider : IDynamoDbClientProvider, IAmABoxTransactionProvider - { - - } -} 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/ServiceCollectionExtensions.cs b/src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionExtensions.cs index 3f1362e821..bcf76d72b8 100644 --- a/src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Paramore.Brighter.Extensions.DependencyInjection/ServiceCollectionExtensions.cs @@ -31,6 +31,7 @@ THE SOFTWARE. */ using Paramore.Brighter.Logging; using System.Text.Json; using System.Transactions; +using Paramore.Brighter.DynamoDb; using Polly.Registry; namespace Paramore.Brighter.Extensions.DependencyInjection @@ -167,7 +168,9 @@ public static IBrighterBuilder UseExternalBus( 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); } @@ -360,6 +363,28 @@ public static MessageMapperRegistry MessageMapperRegistry(IServiceProvider provi return messageMapperRegistry; } + + private static void RegisterDynamoProviderServicesMaybe( + IBrighterBuilder brighterBuilder, + Type connectionProvider, + Type transactionProvider, + ServiceLifetime serviceLifetime) + { + //not all box transaction providers are also relational connection providers + if (typeof(IAmADynamoDbConnectionProvider).IsAssignableFrom(connectionProvider)) + { + brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmADynamoDbConnectionProvider), + connectionProvider, serviceLifetime)); + } + + //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 static void RegisterRelationalProviderServicesMaybe( IBrighterBuilder brighterBuilder, @@ -369,14 +394,14 @@ ServiceLifetime serviceLifetime ) { //not all box transaction providers are also relational connection providers - if (connectionProvider != null) + if (typeof(IAmARelationalDbConnectionProvider).IsAssignableFrom(connectionProvider)) { brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmARelationalDbConnectionProvider), connectionProvider, serviceLifetime)); } //not all box transaction providers are also relational connection providers - if (typeof(IAmARelationalDbConnectionProvider).IsAssignableFrom(transactionProvider)) + if (typeof(IAmATransactionConnectionProvider).IsAssignableFrom(transactionProvider)) { //register the combined interface just in case brighterBuilder.Services.Add(new ServiceDescriptor(typeof(IAmATransactionConnectionProvider), diff --git a/src/Paramore.Brighter/CommittableTransactionProvider.cs b/src/Paramore.Brighter/CommittableTransactionProvider.cs index 4861696d82..ede5a4a17e 100644 --- a/src/Paramore.Brighter/CommittableTransactionProvider.cs +++ b/src/Paramore.Brighter/CommittableTransactionProvider.cs @@ -21,6 +21,11 @@ public void 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) diff --git a/src/Paramore.Brighter/IAmABoxTransactionProvider.cs b/src/Paramore.Brighter/IAmABoxTransactionProvider.cs index 82405e2701..8c5a0f22f2 100644 --- a/src/Paramore.Brighter/IAmABoxTransactionProvider.cs +++ b/src/Paramore.Brighter/IAmABoxTransactionProvider.cs @@ -18,6 +18,11 @@ public interface IAmABoxTransactionProvider /// 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 diff --git a/src/Paramore.Brighter/IAmARelationalDbConnectionProvider.cs b/src/Paramore.Brighter/IAmARelationalDbConnectionProvider.cs index 1a801dc8ee..e9ee5b64b3 100644 --- a/src/Paramore.Brighter/IAmARelationalDbConnectionProvider.cs +++ b/src/Paramore.Brighter/IAmARelationalDbConnectionProvider.cs @@ -10,13 +10,6 @@ namespace Paramore.Brighter /// public interface IAmARelationalDbConnectionProvider { - /// - /// Commit any transaction that we are managing - /// - /// A cancellation token - /// - Task CommitAsync(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. @@ -30,10 +23,5 @@ public interface IAmARelationalDbConnectionProvider /// /// A database connection Task GetConnectionAsync(CancellationToken cancellationToken); - - - - - } } 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 6069008478..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 @@ -76,7 +76,8 @@ public async void When_There_Is_A_Transaction_Between_Outbox_And_Entity() 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) { From 70b126c35692afed8442293a05988e39cbd45019 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Fri, 7 Jul 2023 20:07:21 +0100 Subject: [PATCH 88/89] Remove out of date readmes; switch Dapper default to RMQ --- .../.idea/httpRequests/http-requests-log.http | 72 +++++++++---------- .../Properties/launchSettings.json | 2 +- src/Paramore.Brighter.Inbox.Sqlite/README.md | 47 ------------ src/Paramore.Brighter.Inbox.Sqlite/README.txt | 47 ------------ src/Paramore.Brighter.Outbox.MsSql/README.md | 47 ------------ src/Paramore.Brighter.Outbox.MsSql/README.txt | 47 ------------ src/Paramore.Brighter.Outbox.Sqlite/README.md | 47 ------------ .../README.txt | 47 ------------ 8 files changed, 34 insertions(+), 322 deletions(-) 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.Outbox.MsSql/README.md delete mode 100644 src/Paramore.Brighter.Outbox.MsSql/README.txt delete mode 100644 src/Paramore.Brighter.Outbox.Sqlite/README.md delete mode 100644 src/Paramore.Brighter.Outbox.Sqlite/README.txt diff --git a/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http b/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http index 3666dfe013..5e66a350f8 100644 --- a/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http +++ b/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http @@ -1,3 +1,36 @@ +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 + +{ + "Greeting" : "I drink, and I know things" +} + +<> 2023-07-07T200403.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-07T200359.200.json + +### + POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json Content-Length: 50 @@ -551,42 +584,3 @@ Accept-Encoding: br,deflate,gzip,x-gzip ### -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" -} - -<> 2023-06-30T203936.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-06-30T203932.404.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-06-28T192431.200.json - -### - diff --git a/samples/WebAPI_Dapper/GreetingsWeb/Properties/launchSettings.json b/samples/WebAPI_Dapper/GreetingsWeb/Properties/launchSettings.json index 14641d72fe..b8d93cfd6d 100644 --- a/samples/WebAPI_Dapper/GreetingsWeb/Properties/launchSettings.json +++ b/samples/WebAPI_Dapper/GreetingsWeb/Properties/launchSettings.json @@ -18,7 +18,7 @@ "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", "BRIGHTER_GREETINGS_DATABASE": "Sqlite", - "BRIGHTER_TRANSPORT": "Kafka" + "BRIGHTER_TRANSPORT": "RabbitMQ" } }, "ProductionMySql": { 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.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.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` From eabdb9aa1c7c657e1e67bece3a9c5b834d362348 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Tue, 11 Jul 2023 08:00:01 +0100 Subject: [PATCH 89/89] ephemera that probably need removing from source control --- .../.idea/httpRequests/http-requests-log.http | 300 +++++++++++------- Docker/dynamodb/shared-local-instance.db | Bin 552960 -> 552960 bytes .../Properties/launchSettings.json | 2 +- 3 files changed, 188 insertions(+), 114 deletions(-) diff --git a/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http b/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http index 5e66a350f8..10e80c8e3b 100644 --- a/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http +++ b/.idea/.idea.Brighter/.idea/httpRequests/http-requests-log.http @@ -1,9 +1,15 @@ -GET http://localhost:5000/Greetings/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 -<> 2023-07-07T200406.200.json +{ + "Greeting" : "I drink, and I know things" +} + +<> 2023-07-10T160215.200.json ### @@ -18,59 +24,67 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-07-07T200403.200.json +<> 2023-07-10T160214.200.json ### -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 -<> 2023-07-07T200359.200.json +{ + "Greeting" : "I drink, and I know things" +} + +<> 2023-07-10T160213.200.json ### POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json -Content-Length: 50 +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 #1" + "Greeting" : "I drink, and I know things" } +<> 2023-07-10T160212.200.json + ### POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json -Content-Length: 50 +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 #1" + "Greeting" : "I drink, and I know things" } -<> 2023-07-07T195459.200.json +<> 2023-07-10T160211-1.200.json ### POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json -Content-Length: 52 +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 more things" + "Greeting" : "I drink, and I know things" } -<> 2023-07-07T192903.200.json +<> 2023-07-10T160211.200.json ### @@ -85,16 +99,22 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-07-07T190420.200.json +<> 2023-07-10T160210.200.json ### -GET http://localhost:5000/Greetings/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 -<> 2023-07-07T190355.200.json +{ + "Greeting" : "I drink, and I know things" +} + +<> 2023-07-10T160209.200.json ### @@ -109,25 +129,37 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-07-07T190352.200.json +<> 2023-07-10T124554.200.json ### -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 -<> 2023-07-07T190349.200.json +{ + "Greeting" : "I drink, and I know things" +} + +<> 2023-07-10T124553-1.200.json ### -GET http://localhost:5000/Greetings/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 -<> 2023-07-07T165603.200.json +{ + "Greeting" : "I drink, and I know things" +} + +<> 2023-07-10T124553.200.json ### @@ -142,7 +174,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-07-07T165600.200.json +<> 2023-07-10T124551.200.json ### @@ -157,7 +189,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-07-07T165559.200.json +<> 2023-07-10T124550.200.json ### @@ -172,7 +204,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-07-07T165557.200.json +<> 2023-07-10T124549.200.json ### @@ -187,25 +219,37 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-07-07T165555.200.json +<> 2023-07-10T124547.200.json ### -GET http://localhost:5000/Greetings/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 -<> 2023-07-07T165552.200.json +{ + "Greeting" : "I drink, and I know things" +} + +<> 2023-07-10T124546.200.json ### -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 -<> 2023-07-07T165549.200.json +{ + "Greeting" : "I drink, and I know things" +} + +<> 2023-07-10T124544.200.json ### @@ -214,22 +258,31 @@ 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 +<> 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: 23 +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" } -<> 2023-07-07T165401.200.json +<> 2023-07-07T200403.200.json ### @@ -238,52 +291,50 @@ 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 +<> 2023-07-07T200359.200.json ### POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json -Content-Length: 47 +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" } -<> 2023-07-04T212401.200.json - ### POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json -Content-Length: 47 +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" } -<> 2023-07-04T212358.200.json +<> 2023-07-07T195459.200.json ### POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json -Content-Length: 47 +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" } -<> 2023-07-04T212356.200.json +<> 2023-07-07T192903.200.json ### @@ -298,22 +349,16 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-07-04T211455.200.json +<> 2023-07-07T190420.200.json ### -POST http://localhost:5000/Greetings/Tyrion/new -Content-Type: application/json -Content-Length: 47 +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-04T202333.200.json +<> 2023-07-07T190355.200.json ### @@ -328,77 +373,95 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-07-04T202332.200.json +<> 2023-07-07T190352.200.json ### -POST http://localhost:5000/Greetings/Tyrion/new -Content-Type: application/json -Content-Length: 47 +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-04T202330.200.json +<> 2023-07-07T190349.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 -<> 2023-07-04T202327.200.json +<> 2023-07-07T165603.200.json ### -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 -<> 2023-07-04T201903.200.json +{ + "Greeting" : "I drink, and I know things" +} + +<> 2023-07-07T165600.200.json ### -POST http://localhost:5000/People/new +POST http://localhost:5000/Greetings/Tyrion/new Content-Type: application/json -Content-Length: 23 +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" } -<> 2023-07-04T201859.200.json +<> 2023-07-07T165559.200.json ### -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 +{ + "Greeting" : "I drink, and I know things" +} + +<> 2023-07-07T165557.200.json + ### -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 +{ + "Greeting" : "I drink, and I know things" +} + +<> 2023-07-07T165555.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 +<> 2023-07-07T165552.200.json + ### GET http://localhost:5000/People/Tyrion @@ -406,6 +469,8 @@ 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 + ### GET http://localhost:5000/People/Tyrion @@ -413,7 +478,7 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-07-03T093743.500.json +<> 2023-07-07T165406.200.json ### @@ -428,7 +493,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Name" : "Tyrion" } -<> 2023-07-02T190050.500.json +<> 2023-07-07T165401.200.json ### @@ -437,25 +502,37 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-07-02T190040.500.json +<> 2023-07-07T165354.500.json ### -GET http://localhost:5000/Greetings/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 -<> 2023-07-01T185039.200.json +{ + "Greeting" : "I drink, and I know things" +} + +<> 2023-07-04T212401.200.json ### -GET http://localhost:5000/Greetings/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 -<> 2023-07-01T185036.200.json +{ + "Greeting" : "I drink, and I know things" +} + +<> 2023-07-04T212358.200.json ### @@ -470,7 +547,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-07-01T185033.200.json +<> 2023-07-04T212356.200.json ### @@ -485,7 +562,7 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-07-01T185032.200.json +<> 2023-07-04T211455.200.json ### @@ -500,24 +577,38 @@ Accept-Encoding: br,deflate,gzip,x-gzip "Greeting" : "I drink, and I know things" } -<> 2023-07-01T185030.200.json +<> 2023-07-04T202333.200.json ### -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 -<> 2023-07-01T185021.200.json +{ + "Greeting" : "I drink, and I know things" +} + +<> 2023-07-04T202332.200.json ### -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 +{ + "Greeting" : "I drink, and I know things" +} + +<> 2023-07-04T202330.200.json + ### GET http://localhost:5000/People/Tyrion @@ -525,54 +616,39 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip +<> 2023-07-04T202327.200.json + ### -GET http://localhost:5000/Greetings/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 -<> 2023-06-30T204010.200.json +<> 2023-07-04T201903.200.json ### -POST http://localhost:5000/Greetings/Tyrion/new +POST http://localhost:5000/People/new Content-Type: application/json -Content-Length: 47 +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" } -<> 2023-06-30T204007.200.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 - -<> 2023-06-30T203955.200.json +<> 2023-07-04T201859.200.json ### -POST http://localhost:5000/Greetings/Tyrion/new -Content-Type: application/json -Content-Length: 47 +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-06-30T203951.200.json - ### GET http://localhost:5000/People/Tyrion @@ -580,7 +656,5 @@ Connection: Keep-Alive User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.6) Accept-Encoding: br,deflate,gzip,x-gzip -<> 2023-06-30T203938.200.json - ### diff --git a/Docker/dynamodb/shared-local-instance.db b/Docker/dynamodb/shared-local-instance.db index b9617ae6bd9cdb74d7ca538aa41336f6f16e6937..43008023c6c8cdebec00a1056063ac31524a8916 100644 GIT binary patch delta 2932 zcmeHJ-ES0C6yMpe?#|41cWG-|3fm zbWn<$C?bWfWCi5P+e;|uXY38*pgBglghRD3jwiQc)hZSOkzANa7F>~GKS zH}{-5=XdVh?abWm%;}ccU7yc)l6)Q_pIxu?K<@5^vDcrHL*yqHe?Y!{R{Q~fGxAA9 z;D>9rg-?ZUgp$Fzz|}y&zm=Q!-H7qg7sSVF7n0NAgLXQR4O{B^s3qH*##MXjC7jZc zjx+^n5;SlT%24LY^4EcHxU$NhJJi~0zcF5Kr;jEB*6=#}*zOs7dLnEeofT?H*mnPS zTv-`x_;JlT8kRai!iHMxjpK)$u!Wglg0;eS|K~L5+hj{6C~W^V84IqBT0`^@=O690 z6MO0jqcK7wKTE7#iG0=+!q!N0)Y|66E`XuHmNQck`_$f~8+#!hUy0qgTM(_ADv3m3LVo{59s-@HOo`igI`h8t-PsS|ZeiPBGjV9Gf}Vy|jf4$)+20IL{43G1ai zHXx)Yx|OC6nVv{4y*KeBM`{r#ItLeCULOkE>0>cFJ*fxNQEQWiK*8HU2C=$#3SHqW71QiSto=Y__ou zsWK#u1QAIKYEV&%vWqLaCS#x(QVE(WHcb<-xN1lRRmO6$s135XqD!WR3fP3k0E^3t zByYC+E_*z9+4U;xGWLjp$5zzC>)EDjV&g}7*7tH}5{7aI1CtWS3c?s5+)iPLGz1hP0ztlnb(CwPu-j_Pd(JR0(}8IQ zW_moe3bVb}N@0&btX~?NedlIqlynQaBtFJH&2jllGds8!KOJcBT@2LD#y7{Vihm04 z@^?j(?~YDJ7HfX3*%dw?`aaYiECv?+Kl^3wfbXK6OdKbplyOEf$>i}z|!Kx&VTh#E%e{crV^j}Xdd|IL0}#P=Rs&5gx?1>!5bZk z;HXddkbl5G96cWSI1;OQBz!t_GgKEGtvwsN6YCQX#CH&r(t~94^{;L|ltv!k;L7@j z>nc|^V9QQPa$K_BGvwBAA8FV_Y2%H9U+wOuWRFQ$L!HzK zJe8v4!<2lwtBaoAvyUy*QC-dYl&ixOit6mAIww0j86CzitdaKWVnSDjDHN5-QJI;J zj#U}fJS?@1EvBbXM1w!)q?Fx1*WOMwJl<^SV0Hg*YDS*TOW$XgOxSfz-VV03LG}{U zOpZraH?yLtK#QGwu6~)%^sprlR9~p-mXE+JbE=RWbdJzaX**`+T1X-{p~K5OX4z9- maDX{F+pbo>>{QRW%T2w6n!R@sI@P-rhrY%;`EMs8V}AqIkU)t5 delta 2022 zcmY+FTZ~;*8OP7w``l*6L(fzKrG<26+UYH+f%!Ia{@6Y1~kn)G-^?s=z}p)1oX|Myr{uwKzxD3)bE@#n$5#{IA@>z zTi^ft{{QcszA=6J#`O73oi}T>+H-JjgEN0&hsxf(GQV(O`~Cjz`4?BGy4Q-;$*@$fZyf9GO{CLi>3etQA4^j+ zd9#1oPiEczyK#2)-Y%tw4Uaz%btVcfIsAaXZaCg4D|HQdR8uS5*UazA4-BtKQ`_^&Y+bsrE1xZT z_jmf&{Zjh&U5$2s?&!zscaHV;4PLnS^6p%v`RV+l>7A{4Gh07`o|;Ja-;-Z&jYDzW z&V0Z1?CkG0jAswlZuw?s z9kl0;_DiiDjaTX_zm`1+Z|}0Q|}`SNGemG9(d$<(bMA_=cb9T@U50*P~r zoGCTJMjGraCd{RtFZQIBFBRL;tIrm*slGp##p3d3hJ}%-yow8gW2SwHm}VMrf)%oq zIuv}V9F^F6p2)H0j)WHrntrsszAe2cTPk1vbUHLyoFo6L!W7ZM2pNzlqd?qJj*JQp zxsn^Ayd{K^^oP5O$F`7AG9$P^On3&@x`2!gQ=0& zlqGUrLe0ubq;1g1amSf-G-#0?xYXK_R{Ajd@0;ef)L&od?BP)ZHvhQ_i%Lc)&mf{P zz?>3D8}1QyhAA(lbRoU-cyTzbY|OT<4d$Vd_SdT%Tv`cbTf|C%K@o+bun)*v7JML1 zQpShc(}7I`(B&K^JU3GJn<~f|uT0Ps1!AEf89j1R1jLlJf;bnXPm`A#FyOg-Hnsl& zQy!}O)KDGX{EI4#atxCc6p>PwP?Z9h9k5GbE#^X)wD7Cu&e3!)-uZuppByRtah1g* zW!{E}NYpSh77WrJ8{`So)+x`NB}0WN?FATG`slC#gZTzr)PyUl^A7pL&Fl$ebCOT8A z)>$fLFd-VnXgJiVz$ny!!)2h_$qa9+dJfk}{_A zFSK?JBA~ZuxQZx=f^~2$iw43r$R6wwLL=sslQlygTPypI)w@dOjHW6e=?R!fNw78; z5h)VEL0}5U^vK%x{TxoG2koOWuUPa`f7f5kyJui(FY4T6S zuxgcl@YfMc7RQ$t(;?FO7W?k>&#E})C`{HFgaxcYE^vt~Gmtr_BMIDls(G?Y#TV1j zN3)&jFH42U7SbroQ~2Z>>7^lLDo2!