From 03fad35e7f815e461d4480009f646be77bfd6922 Mon Sep 17 00:00:00 2001 From: undrcrxwn Date: Wed, 28 Jun 2023 23:06:13 +0300 Subject: [PATCH 01/24] feat: inherit sources from the Users' code base --- CrowdParlay.Communication.sln | 23 +++++++++++ ...cation.RabbitMq.DependencyInjection.csproj | 17 +++++++++ .../ServiceCollectionExtensions.cs | 12 ++++++ .../CrowdParlay.Communication.RabbitMq.csproj | 19 ++++++++++ .../RabbitMqConstants.cs | 16 ++++++++ .../RabbitMqExchange.cs | 38 +++++++++++++++++++ .../RabbitMqMessageBroker.cs | 12 ++++++ .../Abstractions/IMessageBroker.cs | 6 +++ .../Abstractions/IMessageDestination.cs | 6 +++ .../Abstractions/Message.cs | 6 +++ .../CrowdParlay.Communication.csproj | 9 +++++ src/CrowdParlay.Communication/Messages.cs | 7 ++++ 12 files changed, 171 insertions(+) create mode 100644 src/CrowdParlay.Communication.RabbitMq.DependencyInjection/CrowdParlay.Communication.RabbitMq.DependencyInjection.csproj create mode 100644 src/CrowdParlay.Communication.RabbitMq.DependencyInjection/ServiceCollectionExtensions.cs create mode 100644 src/CrowdParlay.Communication.RabbitMq/CrowdParlay.Communication.RabbitMq.csproj create mode 100644 src/CrowdParlay.Communication.RabbitMq/RabbitMqConstants.cs create mode 100644 src/CrowdParlay.Communication.RabbitMq/RabbitMqExchange.cs create mode 100644 src/CrowdParlay.Communication.RabbitMq/RabbitMqMessageBroker.cs create mode 100644 src/CrowdParlay.Communication/Abstractions/IMessageBroker.cs create mode 100644 src/CrowdParlay.Communication/Abstractions/IMessageDestination.cs create mode 100644 src/CrowdParlay.Communication/Abstractions/Message.cs create mode 100644 src/CrowdParlay.Communication/CrowdParlay.Communication.csproj create mode 100644 src/CrowdParlay.Communication/Messages.cs diff --git a/CrowdParlay.Communication.sln b/CrowdParlay.Communication.sln index 6a2fd00..4427f8c 100644 --- a/CrowdParlay.Communication.sln +++ b/CrowdParlay.Communication.sln @@ -2,11 +2,34 @@ Microsoft Visual Studio Solution File, Format Version 12.00 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{6397F4A6-059A-4591-AB2F-9E0D28F8E619}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CrowdParlay.Communication", "src\CrowdParlay.Communication\CrowdParlay.Communication.csproj", "{2A29A80A-0CB5-4914-B4D4-9595E667F874}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CrowdParlay.Communication.RabbitMq", "src\CrowdParlay.Communication.RabbitMq\CrowdParlay.Communication.RabbitMq.csproj", "{7D90AF9B-2534-456C-95B1-864D4B533A1E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CrowdParlay.Communication.RabbitMq.DependencyInjection", "src\CrowdParlay.Communication.RabbitMq.DependencyInjection\CrowdParlay.Communication.RabbitMq.DependencyInjection.csproj", "{D2B54C2E-4360-43D0-8964-9FF11C34C4C1}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution + {2A29A80A-0CB5-4914-B4D4-9595E667F874}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2A29A80A-0CB5-4914-B4D4-9595E667F874}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2A29A80A-0CB5-4914-B4D4-9595E667F874}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2A29A80A-0CB5-4914-B4D4-9595E667F874}.Release|Any CPU.Build.0 = Release|Any CPU + {7D90AF9B-2534-456C-95B1-864D4B533A1E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7D90AF9B-2534-456C-95B1-864D4B533A1E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7D90AF9B-2534-456C-95B1-864D4B533A1E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7D90AF9B-2534-456C-95B1-864D4B533A1E}.Release|Any CPU.Build.0 = Release|Any CPU + {D2B54C2E-4360-43D0-8964-9FF11C34C4C1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D2B54C2E-4360-43D0-8964-9FF11C34C4C1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D2B54C2E-4360-43D0-8964-9FF11C34C4C1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D2B54C2E-4360-43D0-8964-9FF11C34C4C1}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {2A29A80A-0CB5-4914-B4D4-9595E667F874} = {6397F4A6-059A-4591-AB2F-9E0D28F8E619} + {7D90AF9B-2534-456C-95B1-864D4B533A1E} = {6397F4A6-059A-4591-AB2F-9E0D28F8E619} + {D2B54C2E-4360-43D0-8964-9FF11C34C4C1} = {6397F4A6-059A-4591-AB2F-9E0D28F8E619} EndGlobalSection EndGlobal diff --git a/src/CrowdParlay.Communication.RabbitMq.DependencyInjection/CrowdParlay.Communication.RabbitMq.DependencyInjection.csproj b/src/CrowdParlay.Communication.RabbitMq.DependencyInjection/CrowdParlay.Communication.RabbitMq.DependencyInjection.csproj new file mode 100644 index 0000000..8b5d14d --- /dev/null +++ b/src/CrowdParlay.Communication.RabbitMq.DependencyInjection/CrowdParlay.Communication.RabbitMq.DependencyInjection.csproj @@ -0,0 +1,17 @@ + + + + net7.0 + enable + enable + + + + + + + + + + + diff --git a/src/CrowdParlay.Communication.RabbitMq.DependencyInjection/ServiceCollectionExtensions.cs b/src/CrowdParlay.Communication.RabbitMq.DependencyInjection/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..e2445de --- /dev/null +++ b/src/CrowdParlay.Communication.RabbitMq.DependencyInjection/ServiceCollectionExtensions.cs @@ -0,0 +1,12 @@ +using CrowdParlay.Communication.Abstractions; +using Microsoft.Extensions.DependencyInjection; +using RabbitMQ.Client; + +namespace CrowdParlay.Communication.RabbitMq.DependencyInjection; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection ConfigureRabbitMqCommunication(this IServiceCollection services, string amqpServerUrl) => services + .AddSingleton(new ConnectionFactory { Uri = new Uri(amqpServerUrl) }) + .AddScoped(); +} \ No newline at end of file diff --git a/src/CrowdParlay.Communication.RabbitMq/CrowdParlay.Communication.RabbitMq.csproj b/src/CrowdParlay.Communication.RabbitMq/CrowdParlay.Communication.RabbitMq.csproj new file mode 100644 index 0000000..f7e4c97 --- /dev/null +++ b/src/CrowdParlay.Communication.RabbitMq/CrowdParlay.Communication.RabbitMq.csproj @@ -0,0 +1,19 @@ + + + + net7.0 + enable + enable + CrowdParlay.Communication.RabbitMq + + + + + + + + + + + + diff --git a/src/CrowdParlay.Communication.RabbitMq/RabbitMqConstants.cs b/src/CrowdParlay.Communication.RabbitMq/RabbitMqConstants.cs new file mode 100644 index 0000000..b4d8a5e --- /dev/null +++ b/src/CrowdParlay.Communication.RabbitMq/RabbitMqConstants.cs @@ -0,0 +1,16 @@ +namespace CrowdParlay.Communication.RabbitMq; + +public static class RabbitMqConstants +{ + public static class Exchanges + { + public const string Users = "users"; + } + + public static class RoutingKeys + { + public const string UserCreated = "users.created"; + public const string UserUpdated = "users.updated"; + public const string UserDeleted = "users.deleted"; + } +} \ No newline at end of file diff --git a/src/CrowdParlay.Communication.RabbitMq/RabbitMqExchange.cs b/src/CrowdParlay.Communication.RabbitMq/RabbitMqExchange.cs new file mode 100644 index 0000000..f71996e --- /dev/null +++ b/src/CrowdParlay.Communication.RabbitMq/RabbitMqExchange.cs @@ -0,0 +1,38 @@ +using System.Text; +using CrowdParlay.Communication.Abstractions; +using Newtonsoft.Json; +using RabbitMQ.Client; + +namespace CrowdParlay.Communication.RabbitMq; + +public class RabbitMqExchange : IMessageDestination +{ + private readonly string _exchange; + private readonly IConnectionFactory _connectionFactory; + + public RabbitMqExchange(string exchange, IConnectionFactory connectionFactory) + { + _exchange = exchange; + _connectionFactory = connectionFactory; + } + + public void Publish(Message message) + { + var connection = _connectionFactory.CreateConnection(); + using var channel = connection.CreateModel(); + + var json = JsonConvert.SerializeObject(message); + var body = Encoding.UTF8.GetBytes(json); + var routingKey = ResolveRoutingKey(message); + + channel.BasicPublish(_exchange, routingKey, body: body); + } + + private string ResolveRoutingKey(Message message) => message switch + { + UserCreatedEvent => RabbitMqConstants.RoutingKeys.UserCreated, + UserUpdatedEvent => RabbitMqConstants.RoutingKeys.UserUpdated, + UserDeletedEvent => RabbitMqConstants.RoutingKeys.UserDeleted, + _ => throw new NotSupportedException($"No corresponding RabbitMQ routing key found for message of type '{message.GetType().FullName}'.") + }; +} \ No newline at end of file diff --git a/src/CrowdParlay.Communication.RabbitMq/RabbitMqMessageBroker.cs b/src/CrowdParlay.Communication.RabbitMq/RabbitMqMessageBroker.cs new file mode 100644 index 0000000..d6b27e9 --- /dev/null +++ b/src/CrowdParlay.Communication.RabbitMq/RabbitMqMessageBroker.cs @@ -0,0 +1,12 @@ +using CrowdParlay.Communication.Abstractions; +using RabbitMQ.Client; + +namespace CrowdParlay.Communication.RabbitMq; + +public sealed class RabbitMqMessageBroker : IMessageBroker +{ + public IMessageDestination Users { get; } + + public RabbitMqMessageBroker(IConnectionFactory connectionFactory) => + Users = new RabbitMqExchange(RabbitMqConstants.Exchanges.Users, connectionFactory); +} \ No newline at end of file diff --git a/src/CrowdParlay.Communication/Abstractions/IMessageBroker.cs b/src/CrowdParlay.Communication/Abstractions/IMessageBroker.cs new file mode 100644 index 0000000..6c032e2 --- /dev/null +++ b/src/CrowdParlay.Communication/Abstractions/IMessageBroker.cs @@ -0,0 +1,6 @@ +namespace CrowdParlay.Communication.Abstractions; + +public interface IMessageBroker +{ + public IMessageDestination Users { get; } +} \ No newline at end of file diff --git a/src/CrowdParlay.Communication/Abstractions/IMessageDestination.cs b/src/CrowdParlay.Communication/Abstractions/IMessageDestination.cs new file mode 100644 index 0000000..bcba6cc --- /dev/null +++ b/src/CrowdParlay.Communication/Abstractions/IMessageDestination.cs @@ -0,0 +1,6 @@ +namespace CrowdParlay.Communication.Abstractions; + +public interface IMessageDestination +{ + public void Publish(Message message); +} \ No newline at end of file diff --git a/src/CrowdParlay.Communication/Abstractions/Message.cs b/src/CrowdParlay.Communication/Abstractions/Message.cs new file mode 100644 index 0000000..6c06452 --- /dev/null +++ b/src/CrowdParlay.Communication/Abstractions/Message.cs @@ -0,0 +1,6 @@ +namespace CrowdParlay.Communication.Abstractions; + +public abstract record Message +{ + internal Message() { } +} \ No newline at end of file diff --git a/src/CrowdParlay.Communication/CrowdParlay.Communication.csproj b/src/CrowdParlay.Communication/CrowdParlay.Communication.csproj new file mode 100644 index 0000000..6836c68 --- /dev/null +++ b/src/CrowdParlay.Communication/CrowdParlay.Communication.csproj @@ -0,0 +1,9 @@ + + + + net7.0 + enable + enable + + + diff --git a/src/CrowdParlay.Communication/Messages.cs b/src/CrowdParlay.Communication/Messages.cs new file mode 100644 index 0000000..c3eb054 --- /dev/null +++ b/src/CrowdParlay.Communication/Messages.cs @@ -0,0 +1,7 @@ +using CrowdParlay.Communication.Abstractions; + +namespace CrowdParlay.Communication; + +public record UserCreatedEvent(string UserId, string Username, string DisplayName) : Message; +public record UserUpdatedEvent(string UserId, string Username, string DisplayName) : Message; +public record UserDeletedEvent(string UserId) : Message; \ No newline at end of file From 0142d92cc67b153e2feccdb1013037322c8fc530 Mon Sep 17 00:00:00 2001 From: undrcrxwn Date: Thu, 29 Jun 2023 00:07:00 +0300 Subject: [PATCH 02/24] chore: add NuGet-specific project properties --- ...Parlay.Communication.RabbitMq.DependencyInjection.csproj | 5 +++++ .../CrowdParlay.Communication.RabbitMq.csproj | 6 +++++- .../CrowdParlay.Communication.csproj | 5 +++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/CrowdParlay.Communication.RabbitMq.DependencyInjection/CrowdParlay.Communication.RabbitMq.DependencyInjection.csproj b/src/CrowdParlay.Communication.RabbitMq.DependencyInjection/CrowdParlay.Communication.RabbitMq.DependencyInjection.csproj index 8b5d14d..201f3d3 100644 --- a/src/CrowdParlay.Communication.RabbitMq.DependencyInjection/CrowdParlay.Communication.RabbitMq.DependencyInjection.csproj +++ b/src/CrowdParlay.Communication.RabbitMq.DependencyInjection/CrowdParlay.Communication.RabbitMq.DependencyInjection.csproj @@ -4,6 +4,11 @@ net7.0 enable enable + CrowdParlay + 1.0.0 + CrowdParlay's RabbitMQ communication integrated with Microsoft's dependency injection implementation. + true + https://gitlab.otter.su/crowdparlay/dotnet-communication diff --git a/src/CrowdParlay.Communication.RabbitMq/CrowdParlay.Communication.RabbitMq.csproj b/src/CrowdParlay.Communication.RabbitMq/CrowdParlay.Communication.RabbitMq.csproj index f7e4c97..667088e 100644 --- a/src/CrowdParlay.Communication.RabbitMq/CrowdParlay.Communication.RabbitMq.csproj +++ b/src/CrowdParlay.Communication.RabbitMq/CrowdParlay.Communication.RabbitMq.csproj @@ -4,7 +4,11 @@ net7.0 enable enable - CrowdParlay.Communication.RabbitMq + CrowdParlay + 1.0.0 + CrowdParlay's RabbitMQ communication API implementation. + true + https://gitlab.otter.su/crowdparlay/dotnet-communication diff --git a/src/CrowdParlay.Communication/CrowdParlay.Communication.csproj b/src/CrowdParlay.Communication/CrowdParlay.Communication.csproj index 6836c68..bb1b8f7 100644 --- a/src/CrowdParlay.Communication/CrowdParlay.Communication.csproj +++ b/src/CrowdParlay.Communication/CrowdParlay.Communication.csproj @@ -4,6 +4,11 @@ net7.0 enable enable + CrowdParlay + 1.0.0 + CrowdParlay's common .NET API for platform-wide communication. + true + https://gitlab.otter.su/crowdparlay/dotnet-communication From f9a9782109db83ab7181c3a8b095b0fd44319ecb Mon Sep 17 00:00:00 2001 From: undrcrxwn Date: Thu, 29 Jun 2023 00:42:24 +0300 Subject: [PATCH 03/24] chore: add CrowdParlay logo as NuGet packages icon --- docs/logo.png | Bin 0 -> 26897 bytes ...nication.RabbitMq.DependencyInjection.csproj | 11 ++++++++++- .../CrowdParlay.Communication.RabbitMq.csproj | 11 ++++++++++- .../CrowdParlay.Communication.csproj | 11 ++++++++++- 4 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 docs/logo.png diff --git a/docs/logo.png b/docs/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..0db373a5213ea34704e04ee70254bec2007db3c5 GIT binary patch literal 26897 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 gt&}MRLiCLOnYjjT)#n=lV!Z literal 0 HcmV?d00001 diff --git a/src/CrowdParlay.Communication.RabbitMq.DependencyInjection/CrowdParlay.Communication.RabbitMq.DependencyInjection.csproj b/src/CrowdParlay.Communication.RabbitMq.DependencyInjection/CrowdParlay.Communication.RabbitMq.DependencyInjection.csproj index 201f3d3..c350821 100644 --- a/src/CrowdParlay.Communication.RabbitMq.DependencyInjection/CrowdParlay.Communication.RabbitMq.DependencyInjection.csproj +++ b/src/CrowdParlay.Communication.RabbitMq.DependencyInjection/CrowdParlay.Communication.RabbitMq.DependencyInjection.csproj @@ -5,10 +5,11 @@ enable enable CrowdParlay - 1.0.0 + 1.0.1 CrowdParlay's RabbitMQ communication integrated with Microsoft's dependency injection implementation. true https://gitlab.otter.su/crowdparlay/dotnet-communication + logo.png @@ -19,4 +20,12 @@ + + + True + + logo.png + + + diff --git a/src/CrowdParlay.Communication.RabbitMq/CrowdParlay.Communication.RabbitMq.csproj b/src/CrowdParlay.Communication.RabbitMq/CrowdParlay.Communication.RabbitMq.csproj index 667088e..9ce15e7 100644 --- a/src/CrowdParlay.Communication.RabbitMq/CrowdParlay.Communication.RabbitMq.csproj +++ b/src/CrowdParlay.Communication.RabbitMq/CrowdParlay.Communication.RabbitMq.csproj @@ -5,10 +5,11 @@ enable enable CrowdParlay - 1.0.0 + 1.0.1 CrowdParlay's RabbitMQ communication API implementation. true https://gitlab.otter.su/crowdparlay/dotnet-communication + logo.png @@ -20,4 +21,12 @@ + + + True + + logo.png + + + diff --git a/src/CrowdParlay.Communication/CrowdParlay.Communication.csproj b/src/CrowdParlay.Communication/CrowdParlay.Communication.csproj index bb1b8f7..47a3ac7 100644 --- a/src/CrowdParlay.Communication/CrowdParlay.Communication.csproj +++ b/src/CrowdParlay.Communication/CrowdParlay.Communication.csproj @@ -5,10 +5,19 @@ enable enable CrowdParlay - 1.0.0 + 1.0.1 CrowdParlay's common .NET API for platform-wide communication. true https://gitlab.otter.su/crowdparlay/dotnet-communication + logo.png + + + True + + logo.png + + + From bec17bad7a04f374ba2097b9ec028e7fbe287745 Mon Sep 17 00:00:00 2001 From: undrcrxwn Date: Thu, 29 Jun 2023 17:40:59 +0300 Subject: [PATCH 04/24] feat: add message consumption API --- .../RabbitMqExchange.cs | 59 ++++++++++++++----- .../Abstractions/IMessageDestination.cs | 1 + .../Abstractions/IMessageListener.cs | 6 ++ 3 files changed, 52 insertions(+), 14 deletions(-) create mode 100644 src/CrowdParlay.Communication/Abstractions/IMessageListener.cs diff --git a/src/CrowdParlay.Communication.RabbitMq/RabbitMqExchange.cs b/src/CrowdParlay.Communication.RabbitMq/RabbitMqExchange.cs index f71996e..99bdad6 100644 --- a/src/CrowdParlay.Communication.RabbitMq/RabbitMqExchange.cs +++ b/src/CrowdParlay.Communication.RabbitMq/RabbitMqExchange.cs @@ -2,37 +2,68 @@ using CrowdParlay.Communication.Abstractions; using Newtonsoft.Json; using RabbitMQ.Client; +using RabbitMQ.Client.Events; namespace CrowdParlay.Communication.RabbitMq; -public class RabbitMqExchange : IMessageDestination +public class RabbitMqExchange : IMessageDestination, IDisposable { private readonly string _exchange; - private readonly IConnectionFactory _connectionFactory; + private readonly IModel _channel; + + private readonly Dictionary _routingKeysByMessageType = new() + { + [typeof(UserCreatedEvent)] = RabbitMqConstants.RoutingKeys.UserCreated, + [typeof(UserUpdatedEvent)] = RabbitMqConstants.RoutingKeys.UserUpdated, + [typeof(UserDeletedEvent)] = RabbitMqConstants.RoutingKeys.UserDeleted + }; public RabbitMqExchange(string exchange, IConnectionFactory connectionFactory) { _exchange = exchange; - _connectionFactory = connectionFactory; + var connection = connectionFactory.CreateConnection(); + _channel = connection.CreateModel(); } public void Publish(Message message) { - var connection = _connectionFactory.CreateConnection(); - using var channel = connection.CreateModel(); + _channel.ExchangeDeclare(_exchange, ExchangeType.Topic); var json = JsonConvert.SerializeObject(message); var body = Encoding.UTF8.GetBytes(json); - var routingKey = ResolveRoutingKey(message); + var routingKey = ResolveRoutingKey(message.GetType()); + + _channel.BasicPublish(_exchange, routingKey, body: body); + } + + public void Subscribe(IMessageListener listener) where TMessage : Message + { + _channel.ExchangeDeclare(_exchange, ExchangeType.Topic); + + var routingKey = ResolveRoutingKey(typeof(TMessage)); + var queue = _channel.QueueDeclare().QueueName; + _channel.QueueBind(queue, _exchange, routingKey); - channel.BasicPublish(_exchange, routingKey, body: body); + var consumer = new EventingBasicConsumer(_channel); + consumer.Received += async (_, args) => + { + _channel.BasicAck(args.DeliveryTag, multiple: false); + + var body = Encoding.UTF8.GetString(args.Body.ToArray()); + var message = JsonConvert.DeserializeObject(body)!; + await listener.HandleAsync(message); + }; + + _channel.BasicConsume(queue, autoAck: false, consumer); } - - private string ResolveRoutingKey(Message message) => message switch + + private string ResolveRoutingKey(Type messageType) { - UserCreatedEvent => RabbitMqConstants.RoutingKeys.UserCreated, - UserUpdatedEvent => RabbitMqConstants.RoutingKeys.UserUpdated, - UserDeletedEvent => RabbitMqConstants.RoutingKeys.UserDeleted, - _ => throw new NotSupportedException($"No corresponding RabbitMQ routing key found for message of type '{message.GetType().FullName}'.") - }; + if (!_routingKeysByMessageType.TryGetValue(messageType, out var routingKey)) + throw new NotSupportedException($"No corresponding RabbitMQ routing key found for message of type '{messageType.FullName}'."); + + return routingKey; + } + + public void Dispose() => _channel.Dispose(); } \ No newline at end of file diff --git a/src/CrowdParlay.Communication/Abstractions/IMessageDestination.cs b/src/CrowdParlay.Communication/Abstractions/IMessageDestination.cs index bcba6cc..587f5a8 100644 --- a/src/CrowdParlay.Communication/Abstractions/IMessageDestination.cs +++ b/src/CrowdParlay.Communication/Abstractions/IMessageDestination.cs @@ -3,4 +3,5 @@ namespace CrowdParlay.Communication.Abstractions; public interface IMessageDestination { public void Publish(Message message); + public void Subscribe(IMessageListener listener) where TMessage : Message; } \ No newline at end of file diff --git a/src/CrowdParlay.Communication/Abstractions/IMessageListener.cs b/src/CrowdParlay.Communication/Abstractions/IMessageListener.cs new file mode 100644 index 0000000..fb593b5 --- /dev/null +++ b/src/CrowdParlay.Communication/Abstractions/IMessageListener.cs @@ -0,0 +1,6 @@ +namespace CrowdParlay.Communication.Abstractions; + +public interface IMessageListener where TMessage : Message +{ + public Task HandleAsync(TMessage message); +} \ No newline at end of file From ccc193210d28e56f70291e9218e6372921d1c2e3 Mon Sep 17 00:00:00 2001 From: undrcrxwn Date: Thu, 29 Jun 2023 17:42:22 +0300 Subject: [PATCH 05/24] tests: add RabbitMQ integration tests project --- CrowdParlay.Communication.sln | 9 +++++ ...unication.RabbitMq.IntegrationTests.csproj | 34 +++++++++++++++++++ .../MessageTransferTests.cs | 32 +++++++++++++++++ .../Services/UserEventsAwaitableConsumer.cs | 33 ++++++++++++++++++ .../Setups/RabbitMqSetup.cs | 19 +++++++++++ .../Usings.cs | 3 ++ 6 files changed, 130 insertions(+) create mode 100644 tests/CrowdParlay.Communication.RabbitMq.IntegrationTests/CrowdParlay.Communication.RabbitMq.IntegrationTests.csproj create mode 100644 tests/CrowdParlay.Communication.RabbitMq.IntegrationTests/MessageTransferTests.cs create mode 100644 tests/CrowdParlay.Communication.RabbitMq.IntegrationTests/Services/UserEventsAwaitableConsumer.cs create mode 100644 tests/CrowdParlay.Communication.RabbitMq.IntegrationTests/Setups/RabbitMqSetup.cs create mode 100644 tests/CrowdParlay.Communication.RabbitMq.IntegrationTests/Usings.cs diff --git a/CrowdParlay.Communication.sln b/CrowdParlay.Communication.sln index 4427f8c..a3cf265 100644 --- a/CrowdParlay.Communication.sln +++ b/CrowdParlay.Communication.sln @@ -8,6 +8,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CrowdParlay.Communication.R EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CrowdParlay.Communication.RabbitMq.DependencyInjection", "src\CrowdParlay.Communication.RabbitMq.DependencyInjection\CrowdParlay.Communication.RabbitMq.DependencyInjection.csproj", "{D2B54C2E-4360-43D0-8964-9FF11C34C4C1}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{6A145F8A-0D41-4541-B913-FC811851FB7B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CrowdParlay.Communication.RabbitMq.IntegrationTests", "tests\CrowdParlay.Communication.RabbitMq.IntegrationTests\CrowdParlay.Communication.RabbitMq.IntegrationTests.csproj", "{AD554EBB-28C1-4273-97F5-3B1FDDE61FED}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -26,10 +30,15 @@ Global {D2B54C2E-4360-43D0-8964-9FF11C34C4C1}.Debug|Any CPU.Build.0 = Debug|Any CPU {D2B54C2E-4360-43D0-8964-9FF11C34C4C1}.Release|Any CPU.ActiveCfg = Release|Any CPU {D2B54C2E-4360-43D0-8964-9FF11C34C4C1}.Release|Any CPU.Build.0 = Release|Any CPU + {AD554EBB-28C1-4273-97F5-3B1FDDE61FED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AD554EBB-28C1-4273-97F5-3B1FDDE61FED}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AD554EBB-28C1-4273-97F5-3B1FDDE61FED}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AD554EBB-28C1-4273-97F5-3B1FDDE61FED}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {2A29A80A-0CB5-4914-B4D4-9595E667F874} = {6397F4A6-059A-4591-AB2F-9E0D28F8E619} {7D90AF9B-2534-456C-95B1-864D4B533A1E} = {6397F4A6-059A-4591-AB2F-9E0D28F8E619} {D2B54C2E-4360-43D0-8964-9FF11C34C4C1} = {6397F4A6-059A-4591-AB2F-9E0D28F8E619} + {AD554EBB-28C1-4273-97F5-3B1FDDE61FED} = {6A145F8A-0D41-4541-B913-FC811851FB7B} EndGlobalSection EndGlobal diff --git a/tests/CrowdParlay.Communication.RabbitMq.IntegrationTests/CrowdParlay.Communication.RabbitMq.IntegrationTests.csproj b/tests/CrowdParlay.Communication.RabbitMq.IntegrationTests/CrowdParlay.Communication.RabbitMq.IntegrationTests.csproj new file mode 100644 index 0000000..6d01abd --- /dev/null +++ b/tests/CrowdParlay.Communication.RabbitMq.IntegrationTests/CrowdParlay.Communication.RabbitMq.IntegrationTests.csproj @@ -0,0 +1,34 @@ + + + + net7.0 + enable + enable + + false + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + diff --git a/tests/CrowdParlay.Communication.RabbitMq.IntegrationTests/MessageTransferTests.cs b/tests/CrowdParlay.Communication.RabbitMq.IntegrationTests/MessageTransferTests.cs new file mode 100644 index 0000000..30d8881 --- /dev/null +++ b/tests/CrowdParlay.Communication.RabbitMq.IntegrationTests/MessageTransferTests.cs @@ -0,0 +1,32 @@ +using CrowdParlay.Communication.RabbitMq.IntegrationTests.Services; +using CrowdParlay.Communication.RabbitMq.IntegrationTests.Setups; +using FluentAssertions; + +namespace CrowdParlay.Communication.RabbitMq.IntegrationTests; + +public class InjectRabbitMqAttribute : AutoDataAttribute +{ + public InjectRabbitMqAttribute() : base(() => new Fixture() + .Customize(new RabbitMqSetup())) { } +} + +public class MessageTransferTests +{ + [Theory(Timeout = 5000), InjectRabbitMq] + public async Task OutcomeEvent_ShouldBeTransferredInto_EquivalentIncomeEvent(RabbitMqMessageBroker broker, UserUpdatedEvent outcomeEvent) + { + // Arrange + var consumer = new UserEventsAwaitableConsumer(); + + broker.Users.Subscribe(consumer); + broker.Users.Subscribe(consumer); + broker.Users.Subscribe(consumer); + + // Act + broker.Users.Publish(outcomeEvent); + var incomeEvent = await consumer.ConsumeOne(); + + // Assert + incomeEvent.Should().Be(outcomeEvent); + } +} \ No newline at end of file diff --git a/tests/CrowdParlay.Communication.RabbitMq.IntegrationTests/Services/UserEventsAwaitableConsumer.cs b/tests/CrowdParlay.Communication.RabbitMq.IntegrationTests/Services/UserEventsAwaitableConsumer.cs new file mode 100644 index 0000000..f9a51f4 --- /dev/null +++ b/tests/CrowdParlay.Communication.RabbitMq.IntegrationTests/Services/UserEventsAwaitableConsumer.cs @@ -0,0 +1,33 @@ +using CrowdParlay.Communication.Abstractions; + +namespace CrowdParlay.Communication.RabbitMq.IntegrationTests.Services; + +public class UserEventsAwaitableConsumer : IMessageListener, IMessageListener, IMessageListener +{ + private TaskCompletionSource _tcs = new(); + + public async Task ConsumeOne() + { + var message = await _tcs.Task; + _tcs = new TaskCompletionSource(); + return message; + } + + public Task HandleAsync(UserCreatedEvent message) + { + _tcs.SetResult(message); + return Task.CompletedTask; + } + + public Task HandleAsync(UserUpdatedEvent message) + { + _tcs.SetResult(message); + return Task.CompletedTask; + } + + public Task HandleAsync(UserDeletedEvent message) + { + _tcs.SetResult(message); + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/tests/CrowdParlay.Communication.RabbitMq.IntegrationTests/Setups/RabbitMqSetup.cs b/tests/CrowdParlay.Communication.RabbitMq.IntegrationTests/Setups/RabbitMqSetup.cs new file mode 100644 index 0000000..f4dcf16 --- /dev/null +++ b/tests/CrowdParlay.Communication.RabbitMq.IntegrationTests/Setups/RabbitMqSetup.cs @@ -0,0 +1,19 @@ +using Nito.AsyncEx; +using RabbitMQ.Client; +using Testcontainers.RabbitMq; + +namespace CrowdParlay.Communication.RabbitMq.IntegrationTests.Setups; + +public class RabbitMqSetup : ICustomization +{ + public void Customize(IFixture fixture) + { + var rabbitMqContainer = new RabbitMqBuilder().Build(); + AsyncContext.Run(async () => await rabbitMqContainer.StartAsync()); + + var amqpServerUrl = rabbitMqContainer.GetConnectionString(); + var connectionFactory = new ConnectionFactory { Uri = new Uri(amqpServerUrl) }; + var broker = new RabbitMqMessageBroker(connectionFactory); + fixture.Inject(broker); + } +} \ No newline at end of file diff --git a/tests/CrowdParlay.Communication.RabbitMq.IntegrationTests/Usings.cs b/tests/CrowdParlay.Communication.RabbitMq.IntegrationTests/Usings.cs new file mode 100644 index 0000000..7ec92a4 --- /dev/null +++ b/tests/CrowdParlay.Communication.RabbitMq.IntegrationTests/Usings.cs @@ -0,0 +1,3 @@ +global using Xunit; +global using AutoFixture; +global using AutoFixture.Xunit2; \ No newline at end of file From f96e08f3535650d9dc509504ec30b358521fd783 Mon Sep 17 00:00:00 2001 From: undrcrxwn Date: Sun, 2 Jul 2023 22:33:42 +0300 Subject: [PATCH 06/24] feat: implement Microsoft DI integration for RabbitMQ communication --- .../MessageListenersConfiguration.cs | 6 +++ .../MessageListenersInitializer.cs | 29 +++++++++++ .../MicrosoftDiMessageListener.cs | 22 ++++++++ .../RabbitMqCommunicationOptions.cs | 9 ++++ .../RabbitMqCommunicationOptionsBuilder.cs | 30 +++++++++++ .../ServiceCollectionExtensions.cs | 51 +++++++++++++++++-- 6 files changed, 144 insertions(+), 3 deletions(-) create mode 100644 src/CrowdParlay.Communication.RabbitMq.DependencyInjection/MessageListenersConfiguration.cs create mode 100644 src/CrowdParlay.Communication.RabbitMq.DependencyInjection/MessageListenersInitializer.cs create mode 100644 src/CrowdParlay.Communication.RabbitMq.DependencyInjection/MicrosoftDiMessageListener.cs create mode 100644 src/CrowdParlay.Communication.RabbitMq.DependencyInjection/RabbitMqCommunicationOptions.cs create mode 100644 src/CrowdParlay.Communication.RabbitMq.DependencyInjection/RabbitMqCommunicationOptionsBuilder.cs diff --git a/src/CrowdParlay.Communication.RabbitMq.DependencyInjection/MessageListenersConfiguration.cs b/src/CrowdParlay.Communication.RabbitMq.DependencyInjection/MessageListenersConfiguration.cs new file mode 100644 index 0000000..6c539cd --- /dev/null +++ b/src/CrowdParlay.Communication.RabbitMq.DependencyInjection/MessageListenersConfiguration.cs @@ -0,0 +1,6 @@ +namespace CrowdParlay.Communication.RabbitMq.DependencyInjection; + +public sealed class MessageListenersConfiguration +{ + public required IEnumerable SubscribedMessageTypes; +} \ No newline at end of file diff --git a/src/CrowdParlay.Communication.RabbitMq.DependencyInjection/MessageListenersInitializer.cs b/src/CrowdParlay.Communication.RabbitMq.DependencyInjection/MessageListenersInitializer.cs new file mode 100644 index 0000000..7211c9a --- /dev/null +++ b/src/CrowdParlay.Communication.RabbitMq.DependencyInjection/MessageListenersInitializer.cs @@ -0,0 +1,29 @@ +using CrowdParlay.Communication.Abstractions; +using Microsoft.Extensions.Hosting; + +namespace CrowdParlay.Communication.RabbitMq.DependencyInjection; + +public sealed class MessageListenersInitializer : IHostedService +{ + private readonly MessageListenersConfiguration _configuration; + private readonly IMessageBroker _broker; + private readonly IMessageListener _commonListener; + + public MessageListenersInitializer( + MessageListenersConfiguration configuration, IMessageBroker broker, IMessageListener commonListener) + { + _configuration = configuration; + _broker = broker; + _commonListener = commonListener; + } + + public Task StartAsync(CancellationToken cancellationToken) + { + foreach (var messageType in _configuration.SubscribedMessageTypes) + _broker.Users.Subscribe(messageType, _commonListener.HandleAsync); + + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; +} \ No newline at end of file diff --git a/src/CrowdParlay.Communication.RabbitMq.DependencyInjection/MicrosoftDiMessageListener.cs b/src/CrowdParlay.Communication.RabbitMq.DependencyInjection/MicrosoftDiMessageListener.cs new file mode 100644 index 0000000..f7d3857 --- /dev/null +++ b/src/CrowdParlay.Communication.RabbitMq.DependencyInjection/MicrosoftDiMessageListener.cs @@ -0,0 +1,22 @@ +using CrowdParlay.Communication.Abstractions; +using Microsoft.Extensions.DependencyInjection; + +namespace CrowdParlay.Communication.RabbitMq.DependencyInjection; + +internal sealed class MicrosoftDiMessageListener : IMessageListener +{ + private readonly IServiceScopeFactory _scopeFactory; + + public MicrosoftDiMessageListener(IServiceScopeFactory scopeFactory) => _scopeFactory = scopeFactory; + + public async Task HandleAsync(Message message) + { + var closedGenericListenerInterface = typeof(IMessageListener<>).MakeGenericType(message.GetType()); + var listenerHandleAsyncMethod = closedGenericListenerInterface.GetMethod(nameof(IMessageListener.HandleAsync))!; + + using var scope = _scopeFactory.CreateScope(); + var listener = scope.ServiceProvider.GetRequiredService(closedGenericListenerInterface); + + await (Task)listenerHandleAsyncMethod.Invoke(listener, new object?[] { message })!; + } +} \ No newline at end of file diff --git a/src/CrowdParlay.Communication.RabbitMq.DependencyInjection/RabbitMqCommunicationOptions.cs b/src/CrowdParlay.Communication.RabbitMq.DependencyInjection/RabbitMqCommunicationOptions.cs new file mode 100644 index 0000000..e91daa6 --- /dev/null +++ b/src/CrowdParlay.Communication.RabbitMq.DependencyInjection/RabbitMqCommunicationOptions.cs @@ -0,0 +1,9 @@ +using System.Reflection; + +namespace CrowdParlay.Communication.RabbitMq.DependencyInjection; + +public sealed class RabbitMqCommunicationOptions +{ + public Uri? AmqpServerUrl; + public readonly HashSet MessageListenersAssemblies = new(); +} \ No newline at end of file diff --git a/src/CrowdParlay.Communication.RabbitMq.DependencyInjection/RabbitMqCommunicationOptionsBuilder.cs b/src/CrowdParlay.Communication.RabbitMq.DependencyInjection/RabbitMqCommunicationOptionsBuilder.cs new file mode 100644 index 0000000..16b1ec8 --- /dev/null +++ b/src/CrowdParlay.Communication.RabbitMq.DependencyInjection/RabbitMqCommunicationOptionsBuilder.cs @@ -0,0 +1,30 @@ +using System.Reflection; + +namespace CrowdParlay.Communication.RabbitMq.DependencyInjection; + +public sealed class RabbitMqCommunicationOptionsBuilder +{ + private readonly RabbitMqCommunicationOptions _options = new(); + + public RabbitMqCommunicationOptionsBuilder UseAmqpServer(string amqpServerUrl) + { + _options.AmqpServerUrl = new Uri(amqpServerUrl); + return this; + } + + public RabbitMqCommunicationOptionsBuilder UseMessageListenersFromAssembly(Assembly assembly) + { + _options.MessageListenersAssemblies.Add(assembly); + return this; + } + + public RabbitMqCommunicationOptions Build() + { + if (_options.AmqpServerUrl is null) + throw new InvalidOperationException( + "RabbitMQ communication services require AMQP server URL to be configured. Consider calling " + + $"'{nameof(RabbitMqCommunicationOptionsBuilder)}.{nameof(UseAmqpServer)}' method while configuring your DI container."); + + return _options; + } +} \ No newline at end of file diff --git a/src/CrowdParlay.Communication.RabbitMq.DependencyInjection/ServiceCollectionExtensions.cs b/src/CrowdParlay.Communication.RabbitMq.DependencyInjection/ServiceCollectionExtensions.cs index e2445de..1569d6d 100644 --- a/src/CrowdParlay.Communication.RabbitMq.DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/CrowdParlay.Communication.RabbitMq.DependencyInjection/ServiceCollectionExtensions.cs @@ -6,7 +6,52 @@ namespace CrowdParlay.Communication.RabbitMq.DependencyInjection; public static class ServiceCollectionExtensions { - public static IServiceCollection ConfigureRabbitMqCommunication(this IServiceCollection services, string amqpServerUrl) => services - .AddSingleton(new ConnectionFactory { Uri = new Uri(amqpServerUrl) }) - .AddScoped(); + public static IServiceCollection AddRabbitMqCommunication( + this IServiceCollection services, Action configureOptions) + { + var builder = new RabbitMqCommunicationOptionsBuilder(); + configureOptions(builder); + var options = builder.Build(); + + // Example of what the dictionary will look like after assembly scanning: + // { MessageA, MessageAListener }, + // { MessageB, MessageBListener }, + // { MessageC, MessageCListener } + + var listenerImplementations = new Dictionary(); + foreach (var type in options.MessageListenersAssemblies.SelectMany(assembly => assembly.GetTypes())) + { + var listenerInterfaces = type.GetInterfaces().Where(@interface => + @interface.IsGenericType && @interface.GetGenericTypeDefinition() == typeof(IMessageListener<>)); + + foreach (var listenerInterface in listenerInterfaces) + { + var messageType = listenerInterface.GetGenericArguments().Single(); + + if (messageType == typeof(Message)) + throw new InvalidOperationException( + $"Message listener of type '{type}' cannot be registered as handler for messages of type '{messageType}', " + + $"since '{typeof(IMessageListener)}' listener type is reserved for the default message router."); + + if (listenerImplementations.TryAdd(messageType, type)) + { + services.AddScoped(listenerInterface, type); + continue; + } + + var originalListenerType = listenerImplementations[listenerInterface]; + throw new InvalidOperationException( + $"Message listener of type '{type}' cannot be registered as handler for messages of type '{messageType}', " + + $"since messages of this type are already handled by message listener of type '{originalListenerType}'."); + } + } + + return services + .AddSingleton(new ConnectionFactory { Uri = options.AmqpServerUrl }) + .AddSingleton(new MessageListenersConfiguration { SubscribedMessageTypes = listenerImplementations.Keys }) + .AddSingleton, MicrosoftDiMessageListener>() + .AddScoped() + .AddHostedService() + .AddSingleton(); + } } \ No newline at end of file From 7264e93ef07626869b389f90896ca1481fa0e94f Mon Sep 17 00:00:00 2001 From: undrcrxwn Date: Sun, 2 Jul 2023 22:34:49 +0300 Subject: [PATCH 07/24] chore: update project version --- ...arlay.Communication.RabbitMq.DependencyInjection.csproj | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/CrowdParlay.Communication.RabbitMq.DependencyInjection/CrowdParlay.Communication.RabbitMq.DependencyInjection.csproj b/src/CrowdParlay.Communication.RabbitMq.DependencyInjection/CrowdParlay.Communication.RabbitMq.DependencyInjection.csproj index c350821..1ab51ee 100644 --- a/src/CrowdParlay.Communication.RabbitMq.DependencyInjection/CrowdParlay.Communication.RabbitMq.DependencyInjection.csproj +++ b/src/CrowdParlay.Communication.RabbitMq.DependencyInjection/CrowdParlay.Communication.RabbitMq.DependencyInjection.csproj @@ -5,12 +5,16 @@ enable enable CrowdParlay - 1.0.1 + 1.0.2 CrowdParlay's RabbitMQ communication integrated with Microsoft's dependency injection implementation. true https://gitlab.otter.su/crowdparlay/dotnet-communication logo.png + + + + @@ -18,6 +22,7 @@ + From 5f7cec64295debac26a73dd8dc7a00970e91ee83 Mon Sep 17 00:00:00 2001 From: undrcrxwn Date: Sun, 2 Jul 2023 22:36:34 +0300 Subject: [PATCH 08/24] feat(destination): improve exchange subscription API --- .../RabbitMqExchange.cs | 29 ++++++++++++------- .../Abstractions/IMessageDestination.cs | 2 ++ 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/src/CrowdParlay.Communication.RabbitMq/RabbitMqExchange.cs b/src/CrowdParlay.Communication.RabbitMq/RabbitMqExchange.cs index 99bdad6..894a952 100644 --- a/src/CrowdParlay.Communication.RabbitMq/RabbitMqExchange.cs +++ b/src/CrowdParlay.Communication.RabbitMq/RabbitMqExchange.cs @@ -28,7 +28,7 @@ public RabbitMqExchange(string exchange, IConnectionFactory connectionFactory) public void Publish(Message message) { _channel.ExchangeDeclare(_exchange, ExchangeType.Topic); - + var json = JsonConvert.SerializeObject(message); var body = Encoding.UTF8.GetBytes(json); var routingKey = ResolveRoutingKey(message.GetType()); @@ -36,27 +36,36 @@ public void Publish(Message message) _channel.BasicPublish(_exchange, routingKey, body: body); } - public void Subscribe(IMessageListener listener) where TMessage : Message + public void Subscribe(IMessageListener listener) where TMessage : Message => + Subscribe(async message => await listener.HandleAsync(message)); + + public void Subscribe(Func handleMessage) where TMessage : Message => + Subscribe(typeof(TMessage), async message => await handleMessage((TMessage)message)); + + public void Subscribe(Type messageType, Func handleMessage) { - _channel.ExchangeDeclare(_exchange, ExchangeType.Topic); + if (!messageType.IsAssignableTo(typeof(Message))) + throw new ArgumentException($"Only types derived from '{typeof(Message)}' can be used as message types.", nameof(messageType)); - var routingKey = ResolveRoutingKey(typeof(TMessage)); + _channel.ExchangeDeclare(_exchange, ExchangeType.Topic); + + var routingKey = ResolveRoutingKey(messageType); var queue = _channel.QueueDeclare().QueueName; _channel.QueueBind(queue, _exchange, routingKey); - + var consumer = new EventingBasicConsumer(_channel); consumer.Received += async (_, args) => { - _channel.BasicAck(args.DeliveryTag, multiple: false); - var body = Encoding.UTF8.GetString(args.Body.ToArray()); - var message = JsonConvert.DeserializeObject(body)!; - await listener.HandleAsync(message); + var message = (Message)JsonConvert.DeserializeObject(body, messageType)!; + await handleMessage(message); + + _channel.BasicAck(args.DeliveryTag, multiple: false); }; _channel.BasicConsume(queue, autoAck: false, consumer); } - + private string ResolveRoutingKey(Type messageType) { if (!_routingKeysByMessageType.TryGetValue(messageType, out var routingKey)) diff --git a/src/CrowdParlay.Communication/Abstractions/IMessageDestination.cs b/src/CrowdParlay.Communication/Abstractions/IMessageDestination.cs index 587f5a8..a73da61 100644 --- a/src/CrowdParlay.Communication/Abstractions/IMessageDestination.cs +++ b/src/CrowdParlay.Communication/Abstractions/IMessageDestination.cs @@ -4,4 +4,6 @@ public interface IMessageDestination { public void Publish(Message message); public void Subscribe(IMessageListener listener) where TMessage : Message; + public void Subscribe(Func handleMessage) where TMessage : Message; + public void Subscribe(Type messageType, Func handleMessage); } \ No newline at end of file From b3025445f670cfe22650fd17ac84a397ad368fd9 Mon Sep 17 00:00:00 2001 From: undrcrxwn Date: Sun, 2 Jul 2023 22:38:29 +0300 Subject: [PATCH 09/24] chore: update project version --- src/CrowdParlay.Communication/CrowdParlay.Communication.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CrowdParlay.Communication/CrowdParlay.Communication.csproj b/src/CrowdParlay.Communication/CrowdParlay.Communication.csproj index 47a3ac7..59e3835 100644 --- a/src/CrowdParlay.Communication/CrowdParlay.Communication.csproj +++ b/src/CrowdParlay.Communication/CrowdParlay.Communication.csproj @@ -5,7 +5,7 @@ enable enable CrowdParlay - 1.0.1 + 1.0.2 CrowdParlay's common .NET API for platform-wide communication. true https://gitlab.otter.su/crowdparlay/dotnet-communication From a84a8c21cdad7f97506a10fea5249fc81e478a00 Mon Sep 17 00:00:00 2001 From: undrcrxwn Date: Sun, 2 Jul 2023 22:38:47 +0300 Subject: [PATCH 10/24] chore: update project version --- .../CrowdParlay.Communication.RabbitMq.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CrowdParlay.Communication.RabbitMq/CrowdParlay.Communication.RabbitMq.csproj b/src/CrowdParlay.Communication.RabbitMq/CrowdParlay.Communication.RabbitMq.csproj index 9ce15e7..2c7fce1 100644 --- a/src/CrowdParlay.Communication.RabbitMq/CrowdParlay.Communication.RabbitMq.csproj +++ b/src/CrowdParlay.Communication.RabbitMq/CrowdParlay.Communication.RabbitMq.csproj @@ -5,7 +5,7 @@ enable enable CrowdParlay - 1.0.1 + 1.0.2 CrowdParlay's RabbitMQ communication API implementation. true https://gitlab.otter.su/crowdparlay/dotnet-communication From 85358e080c25a9c82eb39c5b23b723b1efb3ab87 Mon Sep 17 00:00:00 2001 From: undrcrxwn Date: Sun, 2 Jul 2023 22:41:55 +0300 Subject: [PATCH 11/24] refactor: move UserEventsAwaitableConsumer to props folder --- .../{Services => Props}/UserEventsAwaitableConsumer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename tests/CrowdParlay.Communication.RabbitMq.IntegrationTests/{Services => Props}/UserEventsAwaitableConsumer.cs (92%) diff --git a/tests/CrowdParlay.Communication.RabbitMq.IntegrationTests/Services/UserEventsAwaitableConsumer.cs b/tests/CrowdParlay.Communication.RabbitMq.IntegrationTests/Props/UserEventsAwaitableConsumer.cs similarity index 92% rename from tests/CrowdParlay.Communication.RabbitMq.IntegrationTests/Services/UserEventsAwaitableConsumer.cs rename to tests/CrowdParlay.Communication.RabbitMq.IntegrationTests/Props/UserEventsAwaitableConsumer.cs index f9a51f4..50e30bb 100644 --- a/tests/CrowdParlay.Communication.RabbitMq.IntegrationTests/Services/UserEventsAwaitableConsumer.cs +++ b/tests/CrowdParlay.Communication.RabbitMq.IntegrationTests/Props/UserEventsAwaitableConsumer.cs @@ -1,6 +1,6 @@ using CrowdParlay.Communication.Abstractions; -namespace CrowdParlay.Communication.RabbitMq.IntegrationTests.Services; +namespace CrowdParlay.Communication.RabbitMq.IntegrationTests.Props; public class UserEventsAwaitableConsumer : IMessageListener, IMessageListener, IMessageListener { From 2e2a69975d2ddfabe946a36b4dda8dd2d5e022b8 Mon Sep 17 00:00:00 2001 From: undrcrxwn Date: Sun, 2 Jul 2023 22:43:04 +0300 Subject: [PATCH 12/24] feat: add generic test data injection attribute for setups --- .../Attributes/SetupAttribute.cs | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 tests/CrowdParlay.Communication.RabbitMq.IntegrationTests/Attributes/SetupAttribute.cs diff --git a/tests/CrowdParlay.Communication.RabbitMq.IntegrationTests/Attributes/SetupAttribute.cs b/tests/CrowdParlay.Communication.RabbitMq.IntegrationTests/Attributes/SetupAttribute.cs new file mode 100644 index 0000000..fbf41cf --- /dev/null +++ b/tests/CrowdParlay.Communication.RabbitMq.IntegrationTests/Attributes/SetupAttribute.cs @@ -0,0 +1,7 @@ +namespace CrowdParlay.Communication.RabbitMq.IntegrationTests.Attributes; + +public class SetupAttribute : AutoDataAttribute where TSetup : ICustomization, new() +{ + public SetupAttribute() : base(() => new Fixture() + .Customize(new TSetup())) { } +} \ No newline at end of file From 9444ee1063f1aca9b0602d12f7251d1954d40e3e Mon Sep 17 00:00:00 2001 From: undrcrxwn Date: Sun, 2 Jul 2023 22:43:59 +0300 Subject: [PATCH 13/24] chore(packages): reference Microsoft DI NuGets --- .../CrowdParlay.Communication.RabbitMq.IntegrationTests.csproj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/CrowdParlay.Communication.RabbitMq.IntegrationTests/CrowdParlay.Communication.RabbitMq.IntegrationTests.csproj b/tests/CrowdParlay.Communication.RabbitMq.IntegrationTests/CrowdParlay.Communication.RabbitMq.IntegrationTests.csproj index 6d01abd..95fea54 100644 --- a/tests/CrowdParlay.Communication.RabbitMq.IntegrationTests/CrowdParlay.Communication.RabbitMq.IntegrationTests.csproj +++ b/tests/CrowdParlay.Communication.RabbitMq.IntegrationTests/CrowdParlay.Communication.RabbitMq.IntegrationTests.csproj @@ -12,6 +12,8 @@ + + From b54407674377caaff696997938aad86449e5761c Mon Sep 17 00:00:00 2001 From: undrcrxwn Date: Sun, 2 Jul 2023 22:44:41 +0300 Subject: [PATCH 14/24] refactor: replace custom setup attribute with generic implementation --- .../MessageTransferTests.cs | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/tests/CrowdParlay.Communication.RabbitMq.IntegrationTests/MessageTransferTests.cs b/tests/CrowdParlay.Communication.RabbitMq.IntegrationTests/MessageTransferTests.cs index 30d8881..f0c4c18 100644 --- a/tests/CrowdParlay.Communication.RabbitMq.IntegrationTests/MessageTransferTests.cs +++ b/tests/CrowdParlay.Communication.RabbitMq.IntegrationTests/MessageTransferTests.cs @@ -1,18 +1,14 @@ -using CrowdParlay.Communication.RabbitMq.IntegrationTests.Services; +using CrowdParlay.Communication.Abstractions; +using CrowdParlay.Communication.RabbitMq.IntegrationTests.Attributes; +using CrowdParlay.Communication.RabbitMq.IntegrationTests.Props; using CrowdParlay.Communication.RabbitMq.IntegrationTests.Setups; using FluentAssertions; namespace CrowdParlay.Communication.RabbitMq.IntegrationTests; -public class InjectRabbitMqAttribute : AutoDataAttribute +public sealed class MessageTransferTests { - public InjectRabbitMqAttribute() : base(() => new Fixture() - .Customize(new RabbitMqSetup())) { } -} - -public class MessageTransferTests -{ - [Theory(Timeout = 5000), InjectRabbitMq] + [Theory(Timeout = 5000), Setup] public async Task OutcomeEvent_ShouldBeTransferredInto_EquivalentIncomeEvent(RabbitMqMessageBroker broker, UserUpdatedEvent outcomeEvent) { // Arrange From f9648cdbc9523d5ea92a8a9dedb9214857c7cee0 Mon Sep 17 00:00:00 2001 From: undrcrxwn Date: Sun, 2 Jul 2023 22:46:17 +0300 Subject: [PATCH 15/24] tests: create unit tests project for Microsoft DI integration over RabbitMQ communication --- CrowdParlay.Communication.sln | 7 ++++ .../AssemblyScanningTests.cs | 40 +++++++++++++++++++ ...bitMq.DependencyInjection.UnitTests.csproj | 31 ++++++++++++++ .../Extensions/ServiceCollectionExtensions.cs | 9 +++++ .../Props/Listeners.cs | 13 ++++++ .../Usings.cs | 1 + 6 files changed, 101 insertions(+) create mode 100644 tests/CrowdParlay.Communication.RabbitMq.DependencyInjection.UnitTests/AssemblyScanningTests.cs create mode 100644 tests/CrowdParlay.Communication.RabbitMq.DependencyInjection.UnitTests/CrowdParlay.Communication.RabbitMq.DependencyInjection.UnitTests.csproj create mode 100644 tests/CrowdParlay.Communication.RabbitMq.DependencyInjection.UnitTests/Extensions/ServiceCollectionExtensions.cs create mode 100644 tests/CrowdParlay.Communication.RabbitMq.DependencyInjection.UnitTests/Props/Listeners.cs create mode 100644 tests/CrowdParlay.Communication.RabbitMq.DependencyInjection.UnitTests/Usings.cs diff --git a/CrowdParlay.Communication.sln b/CrowdParlay.Communication.sln index a3cf265..41d2645 100644 --- a/CrowdParlay.Communication.sln +++ b/CrowdParlay.Communication.sln @@ -12,6 +12,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{6A145F8A EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CrowdParlay.Communication.RabbitMq.IntegrationTests", "tests\CrowdParlay.Communication.RabbitMq.IntegrationTests\CrowdParlay.Communication.RabbitMq.IntegrationTests.csproj", "{AD554EBB-28C1-4273-97F5-3B1FDDE61FED}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CrowdParlay.Communication.RabbitMq.DependencyInjection.UnitTests", "tests\CrowdParlay.Communication.RabbitMq.DependencyInjection.UnitTests\CrowdParlay.Communication.RabbitMq.DependencyInjection.UnitTests.csproj", "{17A2E90F-1B74-4E0E-BF57-ED8EC81F851E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -34,11 +36,16 @@ Global {AD554EBB-28C1-4273-97F5-3B1FDDE61FED}.Debug|Any CPU.Build.0 = Debug|Any CPU {AD554EBB-28C1-4273-97F5-3B1FDDE61FED}.Release|Any CPU.ActiveCfg = Release|Any CPU {AD554EBB-28C1-4273-97F5-3B1FDDE61FED}.Release|Any CPU.Build.0 = Release|Any CPU + {17A2E90F-1B74-4E0E-BF57-ED8EC81F851E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {17A2E90F-1B74-4E0E-BF57-ED8EC81F851E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {17A2E90F-1B74-4E0E-BF57-ED8EC81F851E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {17A2E90F-1B74-4E0E-BF57-ED8EC81F851E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {2A29A80A-0CB5-4914-B4D4-9595E667F874} = {6397F4A6-059A-4591-AB2F-9E0D28F8E619} {7D90AF9B-2534-456C-95B1-864D4B533A1E} = {6397F4A6-059A-4591-AB2F-9E0D28F8E619} {D2B54C2E-4360-43D0-8964-9FF11C34C4C1} = {6397F4A6-059A-4591-AB2F-9E0D28F8E619} {AD554EBB-28C1-4273-97F5-3B1FDDE61FED} = {6A145F8A-0D41-4541-B913-FC811851FB7B} + {17A2E90F-1B74-4E0E-BF57-ED8EC81F851E} = {6A145F8A-0D41-4541-B913-FC811851FB7B} EndGlobalSection EndGlobal diff --git a/tests/CrowdParlay.Communication.RabbitMq.DependencyInjection.UnitTests/AssemblyScanningTests.cs b/tests/CrowdParlay.Communication.RabbitMq.DependencyInjection.UnitTests/AssemblyScanningTests.cs new file mode 100644 index 0000000..ad8454e --- /dev/null +++ b/tests/CrowdParlay.Communication.RabbitMq.DependencyInjection.UnitTests/AssemblyScanningTests.cs @@ -0,0 +1,40 @@ +using System.Reflection; +using CrowdParlay.Communication.Abstractions; +using CrowdParlay.Communication.RabbitMq.DependencyInjection.UnitTests.Props; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; + +namespace CrowdParlay.Communication.RabbitMq.DependencyInjection.UnitTests; + +public sealed class AssemblyScanningTests +{ + [Fact] + public void ListenerImplementationsInAssembly_ShouldBe_Registered() + { + // Arrange + var assembly = Assembly.GetExecutingAssembly(); + var services = new ServiceCollection(); + + // Act + services.AddRabbitMqCommunication(options => options + .UseAmqpServer("amqp:") + .UseMessageListenersFromAssembly(assembly)); + + // Assert + services + .FirstOrDefault(x => x.ServiceType == typeof(IMessageListener))?.ImplementationType + .Should().Be(typeof(MicrosoftDiMessageListener), "base message type is reserved for default message routing listener"); + + services + .FirstOrDefault(x => x.ServiceType == typeof(IMessageListener))?.ImplementationType + .Should().Be(typeof(UserCreatedEventListener), "the listener implementation type is public"); + + services + .FirstOrDefault(x => x.ServiceType == typeof(IMessageListener))?.ImplementationType + .Should().Be(typeof(UserUpdatedEventListener), "the listener implementation type is internal"); + + services + .FirstOrDefault(x => x.ServiceType == typeof(IMessageListener))?.ImplementationType + .Should().BeNull("there is no suitable listener implementation type"); + } +} \ No newline at end of file diff --git a/tests/CrowdParlay.Communication.RabbitMq.DependencyInjection.UnitTests/CrowdParlay.Communication.RabbitMq.DependencyInjection.UnitTests.csproj b/tests/CrowdParlay.Communication.RabbitMq.DependencyInjection.UnitTests/CrowdParlay.Communication.RabbitMq.DependencyInjection.UnitTests.csproj new file mode 100644 index 0000000..c99e573 --- /dev/null +++ b/tests/CrowdParlay.Communication.RabbitMq.DependencyInjection.UnitTests/CrowdParlay.Communication.RabbitMq.DependencyInjection.UnitTests.csproj @@ -0,0 +1,31 @@ + + + + net7.0 + enable + enable + + false + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/tests/CrowdParlay.Communication.RabbitMq.DependencyInjection.UnitTests/Extensions/ServiceCollectionExtensions.cs b/tests/CrowdParlay.Communication.RabbitMq.DependencyInjection.UnitTests/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..bc1f5be --- /dev/null +++ b/tests/CrowdParlay.Communication.RabbitMq.DependencyInjection.UnitTests/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,9 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace CrowdParlay.Communication.RabbitMq.DependencyInjection.UnitTests.Extensions; + +public static class ServiceCollectionExtensions +{ + public static bool Contains(this IServiceCollection services) => + services.Any(descriptor => descriptor.ServiceType == typeof(TService)); +} \ No newline at end of file diff --git a/tests/CrowdParlay.Communication.RabbitMq.DependencyInjection.UnitTests/Props/Listeners.cs b/tests/CrowdParlay.Communication.RabbitMq.DependencyInjection.UnitTests/Props/Listeners.cs new file mode 100644 index 0000000..524b0af --- /dev/null +++ b/tests/CrowdParlay.Communication.RabbitMq.DependencyInjection.UnitTests/Props/Listeners.cs @@ -0,0 +1,13 @@ +using CrowdParlay.Communication.Abstractions; + +namespace CrowdParlay.Communication.RabbitMq.DependencyInjection.UnitTests.Props; + +public class UserCreatedEventListener : IMessageListener +{ + public Task HandleAsync(UserCreatedEvent message) => throw new NotSupportedException(); +} + +internal class UserUpdatedEventListener : IMessageListener +{ + public Task HandleAsync(UserUpdatedEvent message) => throw new NotSupportedException(); +} \ No newline at end of file diff --git a/tests/CrowdParlay.Communication.RabbitMq.DependencyInjection.UnitTests/Usings.cs b/tests/CrowdParlay.Communication.RabbitMq.DependencyInjection.UnitTests/Usings.cs new file mode 100644 index 0000000..8c927eb --- /dev/null +++ b/tests/CrowdParlay.Communication.RabbitMq.DependencyInjection.UnitTests/Usings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file From c1adb56a6ff71ef764cda4c7bce80afc84a2600a Mon Sep 17 00:00:00 2001 From: undrcrxwn Date: Mon, 3 Jul 2023 21:49:14 +0300 Subject: [PATCH 16/24] fix(listener-initializer): replace scoped broker injection with scope factory injection in listener initializer hosted service --- .../MessageListenersInitializer.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/CrowdParlay.Communication.RabbitMq.DependencyInjection/MessageListenersInitializer.cs b/src/CrowdParlay.Communication.RabbitMq.DependencyInjection/MessageListenersInitializer.cs index 7211c9a..b368270 100644 --- a/src/CrowdParlay.Communication.RabbitMq.DependencyInjection/MessageListenersInitializer.cs +++ b/src/CrowdParlay.Communication.RabbitMq.DependencyInjection/MessageListenersInitializer.cs @@ -1,4 +1,5 @@ using CrowdParlay.Communication.Abstractions; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; namespace CrowdParlay.Communication.RabbitMq.DependencyInjection; @@ -6,21 +7,24 @@ namespace CrowdParlay.Communication.RabbitMq.DependencyInjection; public sealed class MessageListenersInitializer : IHostedService { private readonly MessageListenersConfiguration _configuration; - private readonly IMessageBroker _broker; private readonly IMessageListener _commonListener; + private readonly IServiceScopeFactory _scopeFactory; public MessageListenersInitializer( - MessageListenersConfiguration configuration, IMessageBroker broker, IMessageListener commonListener) + MessageListenersConfiguration configuration, IMessageListener commonListener, IServiceScopeFactory scopeFactory) { _configuration = configuration; - _broker = broker; _commonListener = commonListener; + _scopeFactory = scopeFactory; } public Task StartAsync(CancellationToken cancellationToken) { + using var scope = _scopeFactory.CreateScope(); + var broker = scope.ServiceProvider.GetRequiredService(); + foreach (var messageType in _configuration.SubscribedMessageTypes) - _broker.Users.Subscribe(messageType, _commonListener.HandleAsync); + broker.Users.Subscribe(messageType, _commonListener.HandleAsync); return Task.CompletedTask; } From af9d2aba04097f5a4e4201c98d105e2cedfe4edb Mon Sep 17 00:00:00 2001 From: undrcrxwn Date: Mon, 3 Jul 2023 21:50:43 +0300 Subject: [PATCH 17/24] chore: update project version --- ...rowdParlay.Communication.RabbitMq.DependencyInjection.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CrowdParlay.Communication.RabbitMq.DependencyInjection/CrowdParlay.Communication.RabbitMq.DependencyInjection.csproj b/src/CrowdParlay.Communication.RabbitMq.DependencyInjection/CrowdParlay.Communication.RabbitMq.DependencyInjection.csproj index 1ab51ee..85bc0e7 100644 --- a/src/CrowdParlay.Communication.RabbitMq.DependencyInjection/CrowdParlay.Communication.RabbitMq.DependencyInjection.csproj +++ b/src/CrowdParlay.Communication.RabbitMq.DependencyInjection/CrowdParlay.Communication.RabbitMq.DependencyInjection.csproj @@ -5,7 +5,7 @@ enable enable CrowdParlay - 1.0.2 + 1.0.3 CrowdParlay's RabbitMQ communication integrated with Microsoft's dependency injection implementation. true https://gitlab.otter.su/crowdparlay/dotnet-communication From 2248778a348bcdf4ce83e15bb65fcc98e316c82f Mon Sep 17 00:00:00 2001 From: undrcrxwn Date: Thu, 13 Jul 2023 13:57:46 +0300 Subject: [PATCH 18/24] feat: add AvatarUrl property to user events --- src/CrowdParlay.Communication/Messages.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/CrowdParlay.Communication/Messages.cs b/src/CrowdParlay.Communication/Messages.cs index c3eb054..ba269c7 100644 --- a/src/CrowdParlay.Communication/Messages.cs +++ b/src/CrowdParlay.Communication/Messages.cs @@ -2,6 +2,6 @@ namespace CrowdParlay.Communication; -public record UserCreatedEvent(string UserId, string Username, string DisplayName) : Message; -public record UserUpdatedEvent(string UserId, string Username, string DisplayName) : Message; +public record UserCreatedEvent(string UserId, string Username, string DisplayName, string? AvatarUrl) : Message; +public record UserUpdatedEvent(string UserId, string Username, string DisplayName, string? AvatarUrl) : Message; public record UserDeletedEvent(string UserId) : Message; \ No newline at end of file From 44ad7fc1ce304d87691f7f652f50d45632d884cd Mon Sep 17 00:00:00 2001 From: undrcrxwn Date: Fri, 14 Jul 2023 14:36:19 +0300 Subject: [PATCH 19/24] tests: remove redundant using directive --- .../MessageTransferTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/CrowdParlay.Communication.RabbitMq.IntegrationTests/MessageTransferTests.cs b/tests/CrowdParlay.Communication.RabbitMq.IntegrationTests/MessageTransferTests.cs index f0c4c18..e4be617 100644 --- a/tests/CrowdParlay.Communication.RabbitMq.IntegrationTests/MessageTransferTests.cs +++ b/tests/CrowdParlay.Communication.RabbitMq.IntegrationTests/MessageTransferTests.cs @@ -1,4 +1,3 @@ -using CrowdParlay.Communication.Abstractions; using CrowdParlay.Communication.RabbitMq.IntegrationTests.Attributes; using CrowdParlay.Communication.RabbitMq.IntegrationTests.Props; using CrowdParlay.Communication.RabbitMq.IntegrationTests.Setups; From 82c171f1f3717f6ca2cf201f0d1d5601b237894b Mon Sep 17 00:00:00 2001 From: undrcrxwn Date: Fri, 14 Jul 2023 14:48:38 +0300 Subject: [PATCH 20/24] chore: update project version --- src/CrowdParlay.Communication/CrowdParlay.Communication.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CrowdParlay.Communication/CrowdParlay.Communication.csproj b/src/CrowdParlay.Communication/CrowdParlay.Communication.csproj index 59e3835..14ee557 100644 --- a/src/CrowdParlay.Communication/CrowdParlay.Communication.csproj +++ b/src/CrowdParlay.Communication/CrowdParlay.Communication.csproj @@ -5,7 +5,7 @@ enable enable CrowdParlay - 1.0.2 + 1.0.3 CrowdParlay's common .NET API for platform-wide communication. true https://gitlab.otter.su/crowdparlay/dotnet-communication From 755222e56099546f4f512c53d58960e6b6f8a02f Mon Sep 17 00:00:00 2001 From: undrcrxwn Date: Sat, 5 Aug 2023 19:18:46 +0300 Subject: [PATCH 21/24] feat: replace custom RabbitMQ wrapper with MassTransit --- CrowdParlay.Communication.sln | 30 ------- ...cation.RabbitMq.DependencyInjection.csproj | 36 --------- .../MessageListenersConfiguration.cs | 6 -- .../MessageListenersInitializer.cs | 33 -------- .../MicrosoftDiMessageListener.cs | 22 ------ .../RabbitMqCommunicationOptions.cs | 9 --- .../RabbitMqCommunicationOptionsBuilder.cs | 30 ------- .../ServiceCollectionExtensions.cs | 57 -------------- .../CrowdParlay.Communication.RabbitMq.csproj | 32 -------- .../RabbitMqConstants.cs | 16 ---- .../RabbitMqExchange.cs | 78 ------------------- .../RabbitMqMessageBroker.cs | 12 --- .../Abstractions/IMessageBroker.cs | 6 -- .../Abstractions/IMessageDestination.cs | 9 --- .../Abstractions/IMessageListener.cs | 6 -- .../Abstractions/Message.cs | 6 -- .../CrowdParlay.Communication.csproj | 4 + .../MassTransitExtensions.cs | 18 +++++ src/CrowdParlay.Communication/Messages.cs | 8 +- .../AssemblyScanningTests.cs | 40 ---------- ...bitMq.DependencyInjection.UnitTests.csproj | 31 -------- .../Extensions/ServiceCollectionExtensions.cs | 9 --- .../Props/Listeners.cs | 13 ---- .../Usings.cs | 1 - .../Attributes/SetupAttribute.cs | 7 -- ...unication.RabbitMq.IntegrationTests.csproj | 36 --------- .../MessageTransferTests.cs | 27 ------- .../Props/UserEventsAwaitableConsumer.cs | 33 -------- .../Setups/RabbitMqSetup.cs | 19 ----- .../Usings.cs | 3 - 30 files changed, 25 insertions(+), 612 deletions(-) delete mode 100644 src/CrowdParlay.Communication.RabbitMq.DependencyInjection/CrowdParlay.Communication.RabbitMq.DependencyInjection.csproj delete mode 100644 src/CrowdParlay.Communication.RabbitMq.DependencyInjection/MessageListenersConfiguration.cs delete mode 100644 src/CrowdParlay.Communication.RabbitMq.DependencyInjection/MessageListenersInitializer.cs delete mode 100644 src/CrowdParlay.Communication.RabbitMq.DependencyInjection/MicrosoftDiMessageListener.cs delete mode 100644 src/CrowdParlay.Communication.RabbitMq.DependencyInjection/RabbitMqCommunicationOptions.cs delete mode 100644 src/CrowdParlay.Communication.RabbitMq.DependencyInjection/RabbitMqCommunicationOptionsBuilder.cs delete mode 100644 src/CrowdParlay.Communication.RabbitMq.DependencyInjection/ServiceCollectionExtensions.cs delete mode 100644 src/CrowdParlay.Communication.RabbitMq/CrowdParlay.Communication.RabbitMq.csproj delete mode 100644 src/CrowdParlay.Communication.RabbitMq/RabbitMqConstants.cs delete mode 100644 src/CrowdParlay.Communication.RabbitMq/RabbitMqExchange.cs delete mode 100644 src/CrowdParlay.Communication.RabbitMq/RabbitMqMessageBroker.cs delete mode 100644 src/CrowdParlay.Communication/Abstractions/IMessageBroker.cs delete mode 100644 src/CrowdParlay.Communication/Abstractions/IMessageDestination.cs delete mode 100644 src/CrowdParlay.Communication/Abstractions/IMessageListener.cs delete mode 100644 src/CrowdParlay.Communication/Abstractions/Message.cs create mode 100644 src/CrowdParlay.Communication/MassTransitExtensions.cs delete mode 100644 tests/CrowdParlay.Communication.RabbitMq.DependencyInjection.UnitTests/AssemblyScanningTests.cs delete mode 100644 tests/CrowdParlay.Communication.RabbitMq.DependencyInjection.UnitTests/CrowdParlay.Communication.RabbitMq.DependencyInjection.UnitTests.csproj delete mode 100644 tests/CrowdParlay.Communication.RabbitMq.DependencyInjection.UnitTests/Extensions/ServiceCollectionExtensions.cs delete mode 100644 tests/CrowdParlay.Communication.RabbitMq.DependencyInjection.UnitTests/Props/Listeners.cs delete mode 100644 tests/CrowdParlay.Communication.RabbitMq.DependencyInjection.UnitTests/Usings.cs delete mode 100644 tests/CrowdParlay.Communication.RabbitMq.IntegrationTests/Attributes/SetupAttribute.cs delete mode 100644 tests/CrowdParlay.Communication.RabbitMq.IntegrationTests/CrowdParlay.Communication.RabbitMq.IntegrationTests.csproj delete mode 100644 tests/CrowdParlay.Communication.RabbitMq.IntegrationTests/MessageTransferTests.cs delete mode 100644 tests/CrowdParlay.Communication.RabbitMq.IntegrationTests/Props/UserEventsAwaitableConsumer.cs delete mode 100644 tests/CrowdParlay.Communication.RabbitMq.IntegrationTests/Setups/RabbitMqSetup.cs delete mode 100644 tests/CrowdParlay.Communication.RabbitMq.IntegrationTests/Usings.cs diff --git a/CrowdParlay.Communication.sln b/CrowdParlay.Communication.sln index 41d2645..0e33e1b 100644 --- a/CrowdParlay.Communication.sln +++ b/CrowdParlay.Communication.sln @@ -4,16 +4,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{6397F4A6-059 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CrowdParlay.Communication", "src\CrowdParlay.Communication\CrowdParlay.Communication.csproj", "{2A29A80A-0CB5-4914-B4D4-9595E667F874}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CrowdParlay.Communication.RabbitMq", "src\CrowdParlay.Communication.RabbitMq\CrowdParlay.Communication.RabbitMq.csproj", "{7D90AF9B-2534-456C-95B1-864D4B533A1E}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CrowdParlay.Communication.RabbitMq.DependencyInjection", "src\CrowdParlay.Communication.RabbitMq.DependencyInjection\CrowdParlay.Communication.RabbitMq.DependencyInjection.csproj", "{D2B54C2E-4360-43D0-8964-9FF11C34C4C1}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{6A145F8A-0D41-4541-B913-FC811851FB7B}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CrowdParlay.Communication.RabbitMq.IntegrationTests", "tests\CrowdParlay.Communication.RabbitMq.IntegrationTests\CrowdParlay.Communication.RabbitMq.IntegrationTests.csproj", "{AD554EBB-28C1-4273-97F5-3B1FDDE61FED}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CrowdParlay.Communication.RabbitMq.DependencyInjection.UnitTests", "tests\CrowdParlay.Communication.RabbitMq.DependencyInjection.UnitTests\CrowdParlay.Communication.RabbitMq.DependencyInjection.UnitTests.csproj", "{17A2E90F-1B74-4E0E-BF57-ED8EC81F851E}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -24,28 +14,8 @@ Global {2A29A80A-0CB5-4914-B4D4-9595E667F874}.Debug|Any CPU.Build.0 = Debug|Any CPU {2A29A80A-0CB5-4914-B4D4-9595E667F874}.Release|Any CPU.ActiveCfg = Release|Any CPU {2A29A80A-0CB5-4914-B4D4-9595E667F874}.Release|Any CPU.Build.0 = Release|Any CPU - {7D90AF9B-2534-456C-95B1-864D4B533A1E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7D90AF9B-2534-456C-95B1-864D4B533A1E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7D90AF9B-2534-456C-95B1-864D4B533A1E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7D90AF9B-2534-456C-95B1-864D4B533A1E}.Release|Any CPU.Build.0 = Release|Any CPU - {D2B54C2E-4360-43D0-8964-9FF11C34C4C1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D2B54C2E-4360-43D0-8964-9FF11C34C4C1}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D2B54C2E-4360-43D0-8964-9FF11C34C4C1}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D2B54C2E-4360-43D0-8964-9FF11C34C4C1}.Release|Any CPU.Build.0 = Release|Any CPU - {AD554EBB-28C1-4273-97F5-3B1FDDE61FED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AD554EBB-28C1-4273-97F5-3B1FDDE61FED}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AD554EBB-28C1-4273-97F5-3B1FDDE61FED}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AD554EBB-28C1-4273-97F5-3B1FDDE61FED}.Release|Any CPU.Build.0 = Release|Any CPU - {17A2E90F-1B74-4E0E-BF57-ED8EC81F851E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {17A2E90F-1B74-4E0E-BF57-ED8EC81F851E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {17A2E90F-1B74-4E0E-BF57-ED8EC81F851E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {17A2E90F-1B74-4E0E-BF57-ED8EC81F851E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {2A29A80A-0CB5-4914-B4D4-9595E667F874} = {6397F4A6-059A-4591-AB2F-9E0D28F8E619} - {7D90AF9B-2534-456C-95B1-864D4B533A1E} = {6397F4A6-059A-4591-AB2F-9E0D28F8E619} - {D2B54C2E-4360-43D0-8964-9FF11C34C4C1} = {6397F4A6-059A-4591-AB2F-9E0D28F8E619} - {AD554EBB-28C1-4273-97F5-3B1FDDE61FED} = {6A145F8A-0D41-4541-B913-FC811851FB7B} - {17A2E90F-1B74-4E0E-BF57-ED8EC81F851E} = {6A145F8A-0D41-4541-B913-FC811851FB7B} EndGlobalSection EndGlobal diff --git a/src/CrowdParlay.Communication.RabbitMq.DependencyInjection/CrowdParlay.Communication.RabbitMq.DependencyInjection.csproj b/src/CrowdParlay.Communication.RabbitMq.DependencyInjection/CrowdParlay.Communication.RabbitMq.DependencyInjection.csproj deleted file mode 100644 index 85bc0e7..0000000 --- a/src/CrowdParlay.Communication.RabbitMq.DependencyInjection/CrowdParlay.Communication.RabbitMq.DependencyInjection.csproj +++ /dev/null @@ -1,36 +0,0 @@ - - - - net7.0 - enable - enable - CrowdParlay - 1.0.3 - CrowdParlay's RabbitMQ communication integrated with Microsoft's dependency injection implementation. - true - https://gitlab.otter.su/crowdparlay/dotnet-communication - logo.png - - - - - - - - - - - - - - - - - - True - - logo.png - - - - diff --git a/src/CrowdParlay.Communication.RabbitMq.DependencyInjection/MessageListenersConfiguration.cs b/src/CrowdParlay.Communication.RabbitMq.DependencyInjection/MessageListenersConfiguration.cs deleted file mode 100644 index 6c539cd..0000000 --- a/src/CrowdParlay.Communication.RabbitMq.DependencyInjection/MessageListenersConfiguration.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace CrowdParlay.Communication.RabbitMq.DependencyInjection; - -public sealed class MessageListenersConfiguration -{ - public required IEnumerable SubscribedMessageTypes; -} \ No newline at end of file diff --git a/src/CrowdParlay.Communication.RabbitMq.DependencyInjection/MessageListenersInitializer.cs b/src/CrowdParlay.Communication.RabbitMq.DependencyInjection/MessageListenersInitializer.cs deleted file mode 100644 index b368270..0000000 --- a/src/CrowdParlay.Communication.RabbitMq.DependencyInjection/MessageListenersInitializer.cs +++ /dev/null @@ -1,33 +0,0 @@ -using CrowdParlay.Communication.Abstractions; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; - -namespace CrowdParlay.Communication.RabbitMq.DependencyInjection; - -public sealed class MessageListenersInitializer : IHostedService -{ - private readonly MessageListenersConfiguration _configuration; - private readonly IMessageListener _commonListener; - private readonly IServiceScopeFactory _scopeFactory; - - public MessageListenersInitializer( - MessageListenersConfiguration configuration, IMessageListener commonListener, IServiceScopeFactory scopeFactory) - { - _configuration = configuration; - _commonListener = commonListener; - _scopeFactory = scopeFactory; - } - - public Task StartAsync(CancellationToken cancellationToken) - { - using var scope = _scopeFactory.CreateScope(); - var broker = scope.ServiceProvider.GetRequiredService(); - - foreach (var messageType in _configuration.SubscribedMessageTypes) - broker.Users.Subscribe(messageType, _commonListener.HandleAsync); - - return Task.CompletedTask; - } - - public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; -} \ No newline at end of file diff --git a/src/CrowdParlay.Communication.RabbitMq.DependencyInjection/MicrosoftDiMessageListener.cs b/src/CrowdParlay.Communication.RabbitMq.DependencyInjection/MicrosoftDiMessageListener.cs deleted file mode 100644 index f7d3857..0000000 --- a/src/CrowdParlay.Communication.RabbitMq.DependencyInjection/MicrosoftDiMessageListener.cs +++ /dev/null @@ -1,22 +0,0 @@ -using CrowdParlay.Communication.Abstractions; -using Microsoft.Extensions.DependencyInjection; - -namespace CrowdParlay.Communication.RabbitMq.DependencyInjection; - -internal sealed class MicrosoftDiMessageListener : IMessageListener -{ - private readonly IServiceScopeFactory _scopeFactory; - - public MicrosoftDiMessageListener(IServiceScopeFactory scopeFactory) => _scopeFactory = scopeFactory; - - public async Task HandleAsync(Message message) - { - var closedGenericListenerInterface = typeof(IMessageListener<>).MakeGenericType(message.GetType()); - var listenerHandleAsyncMethod = closedGenericListenerInterface.GetMethod(nameof(IMessageListener.HandleAsync))!; - - using var scope = _scopeFactory.CreateScope(); - var listener = scope.ServiceProvider.GetRequiredService(closedGenericListenerInterface); - - await (Task)listenerHandleAsyncMethod.Invoke(listener, new object?[] { message })!; - } -} \ No newline at end of file diff --git a/src/CrowdParlay.Communication.RabbitMq.DependencyInjection/RabbitMqCommunicationOptions.cs b/src/CrowdParlay.Communication.RabbitMq.DependencyInjection/RabbitMqCommunicationOptions.cs deleted file mode 100644 index e91daa6..0000000 --- a/src/CrowdParlay.Communication.RabbitMq.DependencyInjection/RabbitMqCommunicationOptions.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Reflection; - -namespace CrowdParlay.Communication.RabbitMq.DependencyInjection; - -public sealed class RabbitMqCommunicationOptions -{ - public Uri? AmqpServerUrl; - public readonly HashSet MessageListenersAssemblies = new(); -} \ No newline at end of file diff --git a/src/CrowdParlay.Communication.RabbitMq.DependencyInjection/RabbitMqCommunicationOptionsBuilder.cs b/src/CrowdParlay.Communication.RabbitMq.DependencyInjection/RabbitMqCommunicationOptionsBuilder.cs deleted file mode 100644 index 16b1ec8..0000000 --- a/src/CrowdParlay.Communication.RabbitMq.DependencyInjection/RabbitMqCommunicationOptionsBuilder.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Reflection; - -namespace CrowdParlay.Communication.RabbitMq.DependencyInjection; - -public sealed class RabbitMqCommunicationOptionsBuilder -{ - private readonly RabbitMqCommunicationOptions _options = new(); - - public RabbitMqCommunicationOptionsBuilder UseAmqpServer(string amqpServerUrl) - { - _options.AmqpServerUrl = new Uri(amqpServerUrl); - return this; - } - - public RabbitMqCommunicationOptionsBuilder UseMessageListenersFromAssembly(Assembly assembly) - { - _options.MessageListenersAssemblies.Add(assembly); - return this; - } - - public RabbitMqCommunicationOptions Build() - { - if (_options.AmqpServerUrl is null) - throw new InvalidOperationException( - "RabbitMQ communication services require AMQP server URL to be configured. Consider calling " + - $"'{nameof(RabbitMqCommunicationOptionsBuilder)}.{nameof(UseAmqpServer)}' method while configuring your DI container."); - - return _options; - } -} \ No newline at end of file diff --git a/src/CrowdParlay.Communication.RabbitMq.DependencyInjection/ServiceCollectionExtensions.cs b/src/CrowdParlay.Communication.RabbitMq.DependencyInjection/ServiceCollectionExtensions.cs deleted file mode 100644 index 1569d6d..0000000 --- a/src/CrowdParlay.Communication.RabbitMq.DependencyInjection/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,57 +0,0 @@ -using CrowdParlay.Communication.Abstractions; -using Microsoft.Extensions.DependencyInjection; -using RabbitMQ.Client; - -namespace CrowdParlay.Communication.RabbitMq.DependencyInjection; - -public static class ServiceCollectionExtensions -{ - public static IServiceCollection AddRabbitMqCommunication( - this IServiceCollection services, Action configureOptions) - { - var builder = new RabbitMqCommunicationOptionsBuilder(); - configureOptions(builder); - var options = builder.Build(); - - // Example of what the dictionary will look like after assembly scanning: - // { MessageA, MessageAListener }, - // { MessageB, MessageBListener }, - // { MessageC, MessageCListener } - - var listenerImplementations = new Dictionary(); - foreach (var type in options.MessageListenersAssemblies.SelectMany(assembly => assembly.GetTypes())) - { - var listenerInterfaces = type.GetInterfaces().Where(@interface => - @interface.IsGenericType && @interface.GetGenericTypeDefinition() == typeof(IMessageListener<>)); - - foreach (var listenerInterface in listenerInterfaces) - { - var messageType = listenerInterface.GetGenericArguments().Single(); - - if (messageType == typeof(Message)) - throw new InvalidOperationException( - $"Message listener of type '{type}' cannot be registered as handler for messages of type '{messageType}', " + - $"since '{typeof(IMessageListener)}' listener type is reserved for the default message router."); - - if (listenerImplementations.TryAdd(messageType, type)) - { - services.AddScoped(listenerInterface, type); - continue; - } - - var originalListenerType = listenerImplementations[listenerInterface]; - throw new InvalidOperationException( - $"Message listener of type '{type}' cannot be registered as handler for messages of type '{messageType}', " + - $"since messages of this type are already handled by message listener of type '{originalListenerType}'."); - } - } - - return services - .AddSingleton(new ConnectionFactory { Uri = options.AmqpServerUrl }) - .AddSingleton(new MessageListenersConfiguration { SubscribedMessageTypes = listenerImplementations.Keys }) - .AddSingleton, MicrosoftDiMessageListener>() - .AddScoped() - .AddHostedService() - .AddSingleton(); - } -} \ No newline at end of file diff --git a/src/CrowdParlay.Communication.RabbitMq/CrowdParlay.Communication.RabbitMq.csproj b/src/CrowdParlay.Communication.RabbitMq/CrowdParlay.Communication.RabbitMq.csproj deleted file mode 100644 index 2c7fce1..0000000 --- a/src/CrowdParlay.Communication.RabbitMq/CrowdParlay.Communication.RabbitMq.csproj +++ /dev/null @@ -1,32 +0,0 @@ - - - - net7.0 - enable - enable - CrowdParlay - 1.0.2 - CrowdParlay's RabbitMQ communication API implementation. - true - https://gitlab.otter.su/crowdparlay/dotnet-communication - logo.png - - - - - - - - - - - - - - True - - logo.png - - - - diff --git a/src/CrowdParlay.Communication.RabbitMq/RabbitMqConstants.cs b/src/CrowdParlay.Communication.RabbitMq/RabbitMqConstants.cs deleted file mode 100644 index b4d8a5e..0000000 --- a/src/CrowdParlay.Communication.RabbitMq/RabbitMqConstants.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace CrowdParlay.Communication.RabbitMq; - -public static class RabbitMqConstants -{ - public static class Exchanges - { - public const string Users = "users"; - } - - public static class RoutingKeys - { - public const string UserCreated = "users.created"; - public const string UserUpdated = "users.updated"; - public const string UserDeleted = "users.deleted"; - } -} \ No newline at end of file diff --git a/src/CrowdParlay.Communication.RabbitMq/RabbitMqExchange.cs b/src/CrowdParlay.Communication.RabbitMq/RabbitMqExchange.cs deleted file mode 100644 index 894a952..0000000 --- a/src/CrowdParlay.Communication.RabbitMq/RabbitMqExchange.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System.Text; -using CrowdParlay.Communication.Abstractions; -using Newtonsoft.Json; -using RabbitMQ.Client; -using RabbitMQ.Client.Events; - -namespace CrowdParlay.Communication.RabbitMq; - -public class RabbitMqExchange : IMessageDestination, IDisposable -{ - private readonly string _exchange; - private readonly IModel _channel; - - private readonly Dictionary _routingKeysByMessageType = new() - { - [typeof(UserCreatedEvent)] = RabbitMqConstants.RoutingKeys.UserCreated, - [typeof(UserUpdatedEvent)] = RabbitMqConstants.RoutingKeys.UserUpdated, - [typeof(UserDeletedEvent)] = RabbitMqConstants.RoutingKeys.UserDeleted - }; - - public RabbitMqExchange(string exchange, IConnectionFactory connectionFactory) - { - _exchange = exchange; - var connection = connectionFactory.CreateConnection(); - _channel = connection.CreateModel(); - } - - public void Publish(Message message) - { - _channel.ExchangeDeclare(_exchange, ExchangeType.Topic); - - var json = JsonConvert.SerializeObject(message); - var body = Encoding.UTF8.GetBytes(json); - var routingKey = ResolveRoutingKey(message.GetType()); - - _channel.BasicPublish(_exchange, routingKey, body: body); - } - - public void Subscribe(IMessageListener listener) where TMessage : Message => - Subscribe(async message => await listener.HandleAsync(message)); - - public void Subscribe(Func handleMessage) where TMessage : Message => - Subscribe(typeof(TMessage), async message => await handleMessage((TMessage)message)); - - public void Subscribe(Type messageType, Func handleMessage) - { - if (!messageType.IsAssignableTo(typeof(Message))) - throw new ArgumentException($"Only types derived from '{typeof(Message)}' can be used as message types.", nameof(messageType)); - - _channel.ExchangeDeclare(_exchange, ExchangeType.Topic); - - var routingKey = ResolveRoutingKey(messageType); - var queue = _channel.QueueDeclare().QueueName; - _channel.QueueBind(queue, _exchange, routingKey); - - var consumer = new EventingBasicConsumer(_channel); - consumer.Received += async (_, args) => - { - var body = Encoding.UTF8.GetString(args.Body.ToArray()); - var message = (Message)JsonConvert.DeserializeObject(body, messageType)!; - await handleMessage(message); - - _channel.BasicAck(args.DeliveryTag, multiple: false); - }; - - _channel.BasicConsume(queue, autoAck: false, consumer); - } - - private string ResolveRoutingKey(Type messageType) - { - if (!_routingKeysByMessageType.TryGetValue(messageType, out var routingKey)) - throw new NotSupportedException($"No corresponding RabbitMQ routing key found for message of type '{messageType.FullName}'."); - - return routingKey; - } - - public void Dispose() => _channel.Dispose(); -} \ No newline at end of file diff --git a/src/CrowdParlay.Communication.RabbitMq/RabbitMqMessageBroker.cs b/src/CrowdParlay.Communication.RabbitMq/RabbitMqMessageBroker.cs deleted file mode 100644 index d6b27e9..0000000 --- a/src/CrowdParlay.Communication.RabbitMq/RabbitMqMessageBroker.cs +++ /dev/null @@ -1,12 +0,0 @@ -using CrowdParlay.Communication.Abstractions; -using RabbitMQ.Client; - -namespace CrowdParlay.Communication.RabbitMq; - -public sealed class RabbitMqMessageBroker : IMessageBroker -{ - public IMessageDestination Users { get; } - - public RabbitMqMessageBroker(IConnectionFactory connectionFactory) => - Users = new RabbitMqExchange(RabbitMqConstants.Exchanges.Users, connectionFactory); -} \ No newline at end of file diff --git a/src/CrowdParlay.Communication/Abstractions/IMessageBroker.cs b/src/CrowdParlay.Communication/Abstractions/IMessageBroker.cs deleted file mode 100644 index 6c032e2..0000000 --- a/src/CrowdParlay.Communication/Abstractions/IMessageBroker.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace CrowdParlay.Communication.Abstractions; - -public interface IMessageBroker -{ - public IMessageDestination Users { get; } -} \ No newline at end of file diff --git a/src/CrowdParlay.Communication/Abstractions/IMessageDestination.cs b/src/CrowdParlay.Communication/Abstractions/IMessageDestination.cs deleted file mode 100644 index a73da61..0000000 --- a/src/CrowdParlay.Communication/Abstractions/IMessageDestination.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace CrowdParlay.Communication.Abstractions; - -public interface IMessageDestination -{ - public void Publish(Message message); - public void Subscribe(IMessageListener listener) where TMessage : Message; - public void Subscribe(Func handleMessage) where TMessage : Message; - public void Subscribe(Type messageType, Func handleMessage); -} \ No newline at end of file diff --git a/src/CrowdParlay.Communication/Abstractions/IMessageListener.cs b/src/CrowdParlay.Communication/Abstractions/IMessageListener.cs deleted file mode 100644 index fb593b5..0000000 --- a/src/CrowdParlay.Communication/Abstractions/IMessageListener.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace CrowdParlay.Communication.Abstractions; - -public interface IMessageListener where TMessage : Message -{ - public Task HandleAsync(TMessage message); -} \ No newline at end of file diff --git a/src/CrowdParlay.Communication/Abstractions/Message.cs b/src/CrowdParlay.Communication/Abstractions/Message.cs deleted file mode 100644 index 6c06452..0000000 --- a/src/CrowdParlay.Communication/Abstractions/Message.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace CrowdParlay.Communication.Abstractions; - -public abstract record Message -{ - internal Message() { } -} \ No newline at end of file diff --git a/src/CrowdParlay.Communication/CrowdParlay.Communication.csproj b/src/CrowdParlay.Communication/CrowdParlay.Communication.csproj index 14ee557..b78c73d 100644 --- a/src/CrowdParlay.Communication/CrowdParlay.Communication.csproj +++ b/src/CrowdParlay.Communication/CrowdParlay.Communication.csproj @@ -20,4 +20,8 @@ + + + + diff --git a/src/CrowdParlay.Communication/MassTransitExtensions.cs b/src/CrowdParlay.Communication/MassTransitExtensions.cs new file mode 100644 index 0000000..e17484d --- /dev/null +++ b/src/CrowdParlay.Communication/MassTransitExtensions.cs @@ -0,0 +1,18 @@ +using MassTransit; + +namespace CrowdParlay.Communication; + +public static class MassTransitExtensions +{ + public static void ConfigureTopology(this IRabbitMqBusFactoryConfigurator configurator) + { + configurator.Message(x => x.SetEntityName("user")); + configurator.Send(x => x.UseRoutingKeyFormatter(_ => "user.created")); + + configurator.Message(x => x.SetEntityName("user")); + configurator.Send(x => x.UseRoutingKeyFormatter(_ => "user.updated")); + + configurator.Message(x => x.SetEntityName("user")); + configurator.Send(x => x.UseRoutingKeyFormatter(_ => "user.deleted")); + } +} \ No newline at end of file diff --git a/src/CrowdParlay.Communication/Messages.cs b/src/CrowdParlay.Communication/Messages.cs index ba269c7..1b3671b 100644 --- a/src/CrowdParlay.Communication/Messages.cs +++ b/src/CrowdParlay.Communication/Messages.cs @@ -1,7 +1,5 @@ -using CrowdParlay.Communication.Abstractions; - namespace CrowdParlay.Communication; -public record UserCreatedEvent(string UserId, string Username, string DisplayName, string? AvatarUrl) : Message; -public record UserUpdatedEvent(string UserId, string Username, string DisplayName, string? AvatarUrl) : Message; -public record UserDeletedEvent(string UserId) : Message; \ No newline at end of file +public record UserCreatedEvent(string UserId, string Username, string DisplayName, string? AvatarUrl); +public record UserUpdatedEvent(string UserId, string Username, string DisplayName, string? AvatarUrl); +public record UserDeletedEvent(string UserId); \ No newline at end of file diff --git a/tests/CrowdParlay.Communication.RabbitMq.DependencyInjection.UnitTests/AssemblyScanningTests.cs b/tests/CrowdParlay.Communication.RabbitMq.DependencyInjection.UnitTests/AssemblyScanningTests.cs deleted file mode 100644 index ad8454e..0000000 --- a/tests/CrowdParlay.Communication.RabbitMq.DependencyInjection.UnitTests/AssemblyScanningTests.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Reflection; -using CrowdParlay.Communication.Abstractions; -using CrowdParlay.Communication.RabbitMq.DependencyInjection.UnitTests.Props; -using FluentAssertions; -using Microsoft.Extensions.DependencyInjection; - -namespace CrowdParlay.Communication.RabbitMq.DependencyInjection.UnitTests; - -public sealed class AssemblyScanningTests -{ - [Fact] - public void ListenerImplementationsInAssembly_ShouldBe_Registered() - { - // Arrange - var assembly = Assembly.GetExecutingAssembly(); - var services = new ServiceCollection(); - - // Act - services.AddRabbitMqCommunication(options => options - .UseAmqpServer("amqp:") - .UseMessageListenersFromAssembly(assembly)); - - // Assert - services - .FirstOrDefault(x => x.ServiceType == typeof(IMessageListener))?.ImplementationType - .Should().Be(typeof(MicrosoftDiMessageListener), "base message type is reserved for default message routing listener"); - - services - .FirstOrDefault(x => x.ServiceType == typeof(IMessageListener))?.ImplementationType - .Should().Be(typeof(UserCreatedEventListener), "the listener implementation type is public"); - - services - .FirstOrDefault(x => x.ServiceType == typeof(IMessageListener))?.ImplementationType - .Should().Be(typeof(UserUpdatedEventListener), "the listener implementation type is internal"); - - services - .FirstOrDefault(x => x.ServiceType == typeof(IMessageListener))?.ImplementationType - .Should().BeNull("there is no suitable listener implementation type"); - } -} \ No newline at end of file diff --git a/tests/CrowdParlay.Communication.RabbitMq.DependencyInjection.UnitTests/CrowdParlay.Communication.RabbitMq.DependencyInjection.UnitTests.csproj b/tests/CrowdParlay.Communication.RabbitMq.DependencyInjection.UnitTests/CrowdParlay.Communication.RabbitMq.DependencyInjection.UnitTests.csproj deleted file mode 100644 index c99e573..0000000 --- a/tests/CrowdParlay.Communication.RabbitMq.DependencyInjection.UnitTests/CrowdParlay.Communication.RabbitMq.DependencyInjection.UnitTests.csproj +++ /dev/null @@ -1,31 +0,0 @@ - - - - net7.0 - enable - enable - - false - - - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - diff --git a/tests/CrowdParlay.Communication.RabbitMq.DependencyInjection.UnitTests/Extensions/ServiceCollectionExtensions.cs b/tests/CrowdParlay.Communication.RabbitMq.DependencyInjection.UnitTests/Extensions/ServiceCollectionExtensions.cs deleted file mode 100644 index bc1f5be..0000000 --- a/tests/CrowdParlay.Communication.RabbitMq.DependencyInjection.UnitTests/Extensions/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; - -namespace CrowdParlay.Communication.RabbitMq.DependencyInjection.UnitTests.Extensions; - -public static class ServiceCollectionExtensions -{ - public static bool Contains(this IServiceCollection services) => - services.Any(descriptor => descriptor.ServiceType == typeof(TService)); -} \ No newline at end of file diff --git a/tests/CrowdParlay.Communication.RabbitMq.DependencyInjection.UnitTests/Props/Listeners.cs b/tests/CrowdParlay.Communication.RabbitMq.DependencyInjection.UnitTests/Props/Listeners.cs deleted file mode 100644 index 524b0af..0000000 --- a/tests/CrowdParlay.Communication.RabbitMq.DependencyInjection.UnitTests/Props/Listeners.cs +++ /dev/null @@ -1,13 +0,0 @@ -using CrowdParlay.Communication.Abstractions; - -namespace CrowdParlay.Communication.RabbitMq.DependencyInjection.UnitTests.Props; - -public class UserCreatedEventListener : IMessageListener -{ - public Task HandleAsync(UserCreatedEvent message) => throw new NotSupportedException(); -} - -internal class UserUpdatedEventListener : IMessageListener -{ - public Task HandleAsync(UserUpdatedEvent message) => throw new NotSupportedException(); -} \ No newline at end of file diff --git a/tests/CrowdParlay.Communication.RabbitMq.DependencyInjection.UnitTests/Usings.cs b/tests/CrowdParlay.Communication.RabbitMq.DependencyInjection.UnitTests/Usings.cs deleted file mode 100644 index 8c927eb..0000000 --- a/tests/CrowdParlay.Communication.RabbitMq.DependencyInjection.UnitTests/Usings.cs +++ /dev/null @@ -1 +0,0 @@ -global using Xunit; \ No newline at end of file diff --git a/tests/CrowdParlay.Communication.RabbitMq.IntegrationTests/Attributes/SetupAttribute.cs b/tests/CrowdParlay.Communication.RabbitMq.IntegrationTests/Attributes/SetupAttribute.cs deleted file mode 100644 index fbf41cf..0000000 --- a/tests/CrowdParlay.Communication.RabbitMq.IntegrationTests/Attributes/SetupAttribute.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace CrowdParlay.Communication.RabbitMq.IntegrationTests.Attributes; - -public class SetupAttribute : AutoDataAttribute where TSetup : ICustomization, new() -{ - public SetupAttribute() : base(() => new Fixture() - .Customize(new TSetup())) { } -} \ No newline at end of file diff --git a/tests/CrowdParlay.Communication.RabbitMq.IntegrationTests/CrowdParlay.Communication.RabbitMq.IntegrationTests.csproj b/tests/CrowdParlay.Communication.RabbitMq.IntegrationTests/CrowdParlay.Communication.RabbitMq.IntegrationTests.csproj deleted file mode 100644 index 95fea54..0000000 --- a/tests/CrowdParlay.Communication.RabbitMq.IntegrationTests/CrowdParlay.Communication.RabbitMq.IntegrationTests.csproj +++ /dev/null @@ -1,36 +0,0 @@ - - - - net7.0 - enable - enable - - false - - - - - - - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - - diff --git a/tests/CrowdParlay.Communication.RabbitMq.IntegrationTests/MessageTransferTests.cs b/tests/CrowdParlay.Communication.RabbitMq.IntegrationTests/MessageTransferTests.cs deleted file mode 100644 index e4be617..0000000 --- a/tests/CrowdParlay.Communication.RabbitMq.IntegrationTests/MessageTransferTests.cs +++ /dev/null @@ -1,27 +0,0 @@ -using CrowdParlay.Communication.RabbitMq.IntegrationTests.Attributes; -using CrowdParlay.Communication.RabbitMq.IntegrationTests.Props; -using CrowdParlay.Communication.RabbitMq.IntegrationTests.Setups; -using FluentAssertions; - -namespace CrowdParlay.Communication.RabbitMq.IntegrationTests; - -public sealed class MessageTransferTests -{ - [Theory(Timeout = 5000), Setup] - public async Task OutcomeEvent_ShouldBeTransferredInto_EquivalentIncomeEvent(RabbitMqMessageBroker broker, UserUpdatedEvent outcomeEvent) - { - // Arrange - var consumer = new UserEventsAwaitableConsumer(); - - broker.Users.Subscribe(consumer); - broker.Users.Subscribe(consumer); - broker.Users.Subscribe(consumer); - - // Act - broker.Users.Publish(outcomeEvent); - var incomeEvent = await consumer.ConsumeOne(); - - // Assert - incomeEvent.Should().Be(outcomeEvent); - } -} \ No newline at end of file diff --git a/tests/CrowdParlay.Communication.RabbitMq.IntegrationTests/Props/UserEventsAwaitableConsumer.cs b/tests/CrowdParlay.Communication.RabbitMq.IntegrationTests/Props/UserEventsAwaitableConsumer.cs deleted file mode 100644 index 50e30bb..0000000 --- a/tests/CrowdParlay.Communication.RabbitMq.IntegrationTests/Props/UserEventsAwaitableConsumer.cs +++ /dev/null @@ -1,33 +0,0 @@ -using CrowdParlay.Communication.Abstractions; - -namespace CrowdParlay.Communication.RabbitMq.IntegrationTests.Props; - -public class UserEventsAwaitableConsumer : IMessageListener, IMessageListener, IMessageListener -{ - private TaskCompletionSource _tcs = new(); - - public async Task ConsumeOne() - { - var message = await _tcs.Task; - _tcs = new TaskCompletionSource(); - return message; - } - - public Task HandleAsync(UserCreatedEvent message) - { - _tcs.SetResult(message); - return Task.CompletedTask; - } - - public Task HandleAsync(UserUpdatedEvent message) - { - _tcs.SetResult(message); - return Task.CompletedTask; - } - - public Task HandleAsync(UserDeletedEvent message) - { - _tcs.SetResult(message); - return Task.CompletedTask; - } -} \ No newline at end of file diff --git a/tests/CrowdParlay.Communication.RabbitMq.IntegrationTests/Setups/RabbitMqSetup.cs b/tests/CrowdParlay.Communication.RabbitMq.IntegrationTests/Setups/RabbitMqSetup.cs deleted file mode 100644 index f4dcf16..0000000 --- a/tests/CrowdParlay.Communication.RabbitMq.IntegrationTests/Setups/RabbitMqSetup.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Nito.AsyncEx; -using RabbitMQ.Client; -using Testcontainers.RabbitMq; - -namespace CrowdParlay.Communication.RabbitMq.IntegrationTests.Setups; - -public class RabbitMqSetup : ICustomization -{ - public void Customize(IFixture fixture) - { - var rabbitMqContainer = new RabbitMqBuilder().Build(); - AsyncContext.Run(async () => await rabbitMqContainer.StartAsync()); - - var amqpServerUrl = rabbitMqContainer.GetConnectionString(); - var connectionFactory = new ConnectionFactory { Uri = new Uri(amqpServerUrl) }; - var broker = new RabbitMqMessageBroker(connectionFactory); - fixture.Inject(broker); - } -} \ No newline at end of file diff --git a/tests/CrowdParlay.Communication.RabbitMq.IntegrationTests/Usings.cs b/tests/CrowdParlay.Communication.RabbitMq.IntegrationTests/Usings.cs deleted file mode 100644 index 7ec92a4..0000000 --- a/tests/CrowdParlay.Communication.RabbitMq.IntegrationTests/Usings.cs +++ /dev/null @@ -1,3 +0,0 @@ -global using Xunit; -global using AutoFixture; -global using AutoFixture.Xunit2; \ No newline at end of file From c604c6fd2776677e221b012de1c9046e6e7a0717 Mon Sep 17 00:00:00 2001 From: undrcrxwn Date: Sat, 5 Aug 2023 19:20:55 +0300 Subject: [PATCH 22/24] chore: update project version --- src/CrowdParlay.Communication/CrowdParlay.Communication.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CrowdParlay.Communication/CrowdParlay.Communication.csproj b/src/CrowdParlay.Communication/CrowdParlay.Communication.csproj index b78c73d..8919143 100644 --- a/src/CrowdParlay.Communication/CrowdParlay.Communication.csproj +++ b/src/CrowdParlay.Communication/CrowdParlay.Communication.csproj @@ -5,7 +5,7 @@ enable enable CrowdParlay - 1.0.3 + 2.0.0 CrowdParlay's common .NET API for platform-wide communication. true https://gitlab.otter.su/crowdparlay/dotnet-communication From 823069a1d5a6ae3dfed03ad5cb8e3a6e505d8ffe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D1=82=D0=B5=D0=BF=D0=BD=D0=BE=D0=B9=20=D0=B8=D1=88?= =?UTF-8?q?=D0=B0=D0=BA?= Date: Sat, 5 Aug 2023 18:09:48 +0000 Subject: [PATCH 23/24] chore: add README.md --- README.md | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..59876b4 --- /dev/null +++ b/README.md @@ -0,0 +1,38 @@ +# Crowd Parlay's .NET *communication* tools + +### Technologies +`C#` `.NET 7` `MassTransit` `RabbitMQ` + +### Responsibilities +- Platform-wide messaging types +- Common MassTransit configuration +- RabbitMQ topology maintenance + +### Integration +Given `MassTransit.RabbitMQ` NuGet package and `IServiceCollection services` instance, `CrowdParlay.Communication` gets integrated as follows: + +```csharp +using CrowdParlay.Communication; +using MassTransit; +``` + +```csharp +services.AddMassTransit(bus => bus.UsingRabbitMq((context, configurator) => +{ + var amqpServerUrl = + configuration["RABBITMQ_AMQP_SERVER_URL"] + ?? throw new InvalidOperationException("Missing required configuration 'RABBITMQ_AMQP_SERVER_URL'."); + + configurator.Host(amqpServerUrl); + configurator.ConfigureEndpoints(context); + configurator.ConfigureTopology(); // This extension method is defined in CrowdParlay.Communication +})); +``` + +Now, inject `IPublishEndpoint _broker` to publish messages like that: + +```csharp +// UserCreatedEvent type is defined in CrowdParlay.Communication +var @event = new UserCreatedEvent(user.Id.ToString(), user.Username, user.DisplayName, user.AvatarUrl); +await _broker.Publish(@event, cancellationToken); +``` From 2f29dc13325f8285f2e290a8b907b158dd978007 Mon Sep 17 00:00:00 2001 From: undrcrxwn <69521267+undrcrxwn@users.noreply.github.com> Date: Tue, 24 Jun 2025 14:28:31 +0300 Subject: [PATCH 24/24] chore: upgrade to .NET 9 --- .../CrowdParlay.Communication.csproj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/CrowdParlay.Communication/CrowdParlay.Communication.csproj b/src/CrowdParlay.Communication/CrowdParlay.Communication.csproj index 8919143..fcd2659 100644 --- a/src/CrowdParlay.Communication/CrowdParlay.Communication.csproj +++ b/src/CrowdParlay.Communication/CrowdParlay.Communication.csproj @@ -1,11 +1,11 @@ - net7.0 + net9.0 enable enable CrowdParlay - 2.0.0 + 2.0.2 CrowdParlay's common .NET API for platform-wide communication. true https://gitlab.otter.su/crowdparlay/dotnet-communication @@ -21,7 +21,7 @@ - +