diff --git a/CommunityToolkit.Aspire.sln b/CommunityToolkit.Aspire.sln index 670ea438..ac0f029b 100644 --- a/CommunityToolkit.Aspire.sln +++ b/CommunityToolkit.Aspire.sln @@ -325,6 +325,18 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "redis-ext", "redis-ext", "{ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.Hosting.Redis.Extensions.Tests", "tests\CommunityToolkit.Aspire.Hosting.Redis.Extensions.Tests\CommunityToolkit.Aspire.Hosting.Redis.Extensions.Tests.csproj", "{958A251E-9D2A-4D67-9D1C-47D6A9E36B3A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.Hosting.LavinMQ", "src\CommunityToolkit.Aspire.Hosting.LavinMQ\CommunityToolkit.Aspire.Hosting.LavinMQ.csproj", "{F218A6C1-3D4F-4351-9580-C99DBC3774FF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.Hosting.LavinMQ.Tests", "tests\CommunityToolkit.Aspire.Hosting.LavinMQ.Tests\CommunityToolkit.Aspire.Hosting.LavinMQ.Tests.csproj", "{E0B23172-E795-4AC5-B9E2-6922E4F167BB}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "lavinmq", "lavinmq", "{35E5A4C8-219B-44AC-AB93-B07A67BCC810}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.Hosting.LavinMQ.AppHost", "examples\lavinmq\CommunityToolkit.Aspire.Hosting.LavinMQ.AppHost\CommunityToolkit.Aspire.Hosting.LavinMQ.AppHost.csproj", "{7068DDA8-71B9-428E-BCC3-89658670B830}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.Hosting.LavinMQ.MassTransit", "examples\lavinmq\CommunityToolkit.Aspire.Hosting.LavinMQ.MassTransit\CommunityToolkit.Aspire.Hosting.LavinMQ.MassTransit.csproj", "{8C463360-0156-461C-A065-CC30FE3B0595}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.Hosting.LavinMQ.ServiceDefaults", "examples\lavinmq\CommunityToolkit.Aspire.Hosting.LavinMQ.ServiceDefaults\CommunityToolkit.Aspire.Hosting.LavinMQ.ServiceDefaults.csproj", "{A5C87DA6-6793-4824-88E5-4F8310FE55B8}" +EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "mailpit", "mailpit", "{734EF69D-EE4D-4E9B-96BD-A44E53C5D2EC}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityToolkit.Aspire.Hosting.MailPit", "src\CommunityToolkit.Aspire.Hosting.MailPit\CommunityToolkit.Aspire.Hosting.MailPit.csproj", "{73E7BC19-A626-4C55-990B-A878BF84D4CA}" @@ -867,6 +879,26 @@ Global {958A251E-9D2A-4D67-9D1C-47D6A9E36B3A}.Debug|Any CPU.Build.0 = Debug|Any CPU {958A251E-9D2A-4D67-9D1C-47D6A9E36B3A}.Release|Any CPU.ActiveCfg = Release|Any CPU {958A251E-9D2A-4D67-9D1C-47D6A9E36B3A}.Release|Any CPU.Build.0 = Release|Any CPU + {F218A6C1-3D4F-4351-9580-C99DBC3774FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F218A6C1-3D4F-4351-9580-C99DBC3774FF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F218A6C1-3D4F-4351-9580-C99DBC3774FF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F218A6C1-3D4F-4351-9580-C99DBC3774FF}.Release|Any CPU.Build.0 = Release|Any CPU + {E0B23172-E795-4AC5-B9E2-6922E4F167BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E0B23172-E795-4AC5-B9E2-6922E4F167BB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E0B23172-E795-4AC5-B9E2-6922E4F167BB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E0B23172-E795-4AC5-B9E2-6922E4F167BB}.Release|Any CPU.Build.0 = Release|Any CPU + {7068DDA8-71B9-428E-BCC3-89658670B830}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7068DDA8-71B9-428E-BCC3-89658670B830}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7068DDA8-71B9-428E-BCC3-89658670B830}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7068DDA8-71B9-428E-BCC3-89658670B830}.Release|Any CPU.Build.0 = Release|Any CPU + {8C463360-0156-461C-A065-CC30FE3B0595}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8C463360-0156-461C-A065-CC30FE3B0595}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8C463360-0156-461C-A065-CC30FE3B0595}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8C463360-0156-461C-A065-CC30FE3B0595}.Release|Any CPU.Build.0 = Release|Any CPU + {A5C87DA6-6793-4824-88E5-4F8310FE55B8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A5C87DA6-6793-4824-88E5-4F8310FE55B8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A5C87DA6-6793-4824-88E5-4F8310FE55B8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A5C87DA6-6793-4824-88E5-4F8310FE55B8}.Release|Any CPU.Build.0 = Release|Any CPU {73E7BC19-A626-4C55-990B-A878BF84D4CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {73E7BC19-A626-4C55-990B-A878BF84D4CA}.Debug|Any CPU.Build.0 = Debug|Any CPU {73E7BC19-A626-4C55-990B-A878BF84D4CA}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -1050,6 +1082,12 @@ Global {CB491C52-A3D5-4F36-97C4-75C482F90A30} = {734EF69D-EE4D-4E9B-96BD-A44E53C5D1EC} {734EF69D-EE4D-4E9B-96BD-A44E53C5D1EC} = {8519CC01-1370-47C8-AD94-B0F326B1563F} {958A251E-9D2A-4D67-9D1C-47D6A9E36B3A} = {899F0713-7FC6-4750-BAFC-AC650B35B453} + {F218A6C1-3D4F-4351-9580-C99DBC3774FF} = {414151D4-7009-4E78-A5C6-D99EBD1E67D1} + {E0B23172-E795-4AC5-B9E2-6922E4F167BB} = {899F0713-7FC6-4750-BAFC-AC650B35B453} + {35E5A4C8-219B-44AC-AB93-B07A67BCC810} = {8519CC01-1370-47C8-AD94-B0F326B1563F} + {7068DDA8-71B9-428E-BCC3-89658670B830} = {35E5A4C8-219B-44AC-AB93-B07A67BCC810} + {8C463360-0156-461C-A065-CC30FE3B0595} = {35E5A4C8-219B-44AC-AB93-B07A67BCC810} + {A5C87DA6-6793-4824-88E5-4F8310FE55B8} = {35E5A4C8-219B-44AC-AB93-B07A67BCC810} {73E7BC19-A626-4C55-990B-A878BF84D4CA} = {414151D4-7009-4E78-A5C6-D99EBD1E67D1} {9C66ECFF-8077-4D5A-9CEB-6E9AB6CCBE94} = {899F0713-7FC6-4750-BAFC-AC650B35B453} {734EF69D-EE4D-4E9B-96BD-A44E53C5D2EC} = {8519CC01-1370-47C8-AD94-B0F326B1563F} diff --git a/Directory.Packages.props b/Directory.Packages.props index 0e3060ef..b2eaed91 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -17,6 +17,7 @@ + diff --git a/README.md b/README.md index e812d532..6292efb4 100644 --- a/README.md +++ b/README.md @@ -43,9 +43,9 @@ This repository contains the source code for the .NET Aspire Community Toolkit, | - **Learn More**: [`Hosting.MongoDB.Extensions`][mongodb-ext-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.MongoDB.Extensions][mongodb-ext-shields]][mongodb-ext-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.MongoDB.Extensions][mongodb-ext-shields-preview]][mongodb-ext-nuget-preview] | An integration that contains some additional extensions for hosting MongoDB container. | | - **Learn More**: [`Hosting.PostgreSQL.Extensions`][postgres-ext-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.PostgreSQL.Extensions][postgres-ext-shields]][postgres-ext-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.PostgreSQL.Extensions][postgres-ext-shields-preview]][postgres-ext-nuget-preview] | An integration that contains some additional extensions for hosting PostgreSQL container. | | - **Learn More**: [`Hosting.Redis.Extensions`][redis-ext-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Redis.Extensions][redis-ext-shields]][redis-ext-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.Redis.Extensions][redis-ext-shields-preview]][redis-ext-nuget-preview] | An integration that contains some additional extensions for hosting Redis container. | +| - **Learn More**: [`Hosting.LavinMQ`][lavinmq-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.LavinMQ][lavinmq-shields]][lavinmq-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.LavinMQ][lavinmq-shields-preview]][lavinmq-nuget-preview] | An Aspire hosting integration for [LavinMQ](https://www.lavinmq.com). | | - **Learn More**: [`Hosting.MailPit`][mailpit-ext-integration-docs]
- Stable 📦: [![CommunityToolkit.Aspire.Hosting.MailPit][mailpit-ext-shields]][mailpit-ext-nuget]
- Preview 📦: [![CommunityToolkit.Aspire.Hosting.MailPit][mailpit-ext-shields-preview]][mailpit-ext-nuget-preview] | An Aspire integration leveraging the [MailPit](https://mailpit.axllent.org/) container. | - ## 🙌 Getting Started Each of the integrations in the toolkit is available as a NuGet package, and can be added to your .NET project. Refer to the table above for the available integrations and the documentation on how to use them. @@ -224,8 +224,13 @@ This project is supported by the [.NET Foundation](https://dotnetfoundation.org) [redis-ext-nuget]: https://nuget.org/packages/CommunityToolkit.Aspire.Hosting.Redis.Extensions/ [redis-ext-shields-preview]: https://img.shields.io/nuget/vpre/CommunityToolkit.Aspire.Hosting.Redis.Extensions?label=nuget%20(preview) [redis-ext-nuget-preview]: https://nuget.org/packages/CommunityToolkit.Aspire.Hosting.Redis.Extensions/absoluteLatest +[lavinmq-integration-docs]: https://learn.microsoft.com/dotnet/aspire/community-toolkit/hosting-lavinmq +[lavinmq-shields]: https://img.shields.io/nuget/v/CommunityToolkit.Aspire.Hosting.LavinMQ +[lavinmq-nuget]: https://nuget.org/packages/CommunityToolkit.Aspire.Hosting.LavinMQ/ +[lavinmq-shields-preview]: https://img.shields.io/nuget/vpre/CommunityToolkit.Aspire.Hosting.LavinMQ?label=nuget%20(preview) +[lavinmq-nuget-preview]: https://nuget.org/packages/CommunityToolkit.Aspire.Hosting.LavinMQ/absoluteLatest [mailpit-ext-integration-docs]: https://learn.microsoft.com/dotnet/aspire/community-toolkit/hosting-mailpit [mailpit-ext-shields]: https://img.shields.io/nuget/v/CommunityToolkit.Aspire.Hosting.MailPit [mailpit-ext-nuget]: https://nuget.org/packages/CommunityToolkit.Aspire.Hosting.MailPit/ [mailpit-ext-shields-preview]: https://img.shields.io/nuget/vpre/CommunityToolkit.Aspire.Hosting.MailPit?label=nuget%20(preview) -[mailpit-ext-nuget-preview]: https://nuget.org/packages/CommunityToolkit.Aspire.Hosting.MailPit/absoluteLatest \ No newline at end of file +[mailpit-ext-nuget-preview]: https://nuget.org/packages/CommunityToolkit.Aspire.Hosting.MailPit/absoluteLatest diff --git a/examples/lavinmq/CommunityToolkit.Aspire.Hosting.LavinMQ.AppHost/CommunityToolkit.Aspire.Hosting.LavinMQ.AppHost.csproj b/examples/lavinmq/CommunityToolkit.Aspire.Hosting.LavinMQ.AppHost/CommunityToolkit.Aspire.Hosting.LavinMQ.AppHost.csproj new file mode 100644 index 00000000..650d15fb --- /dev/null +++ b/examples/lavinmq/CommunityToolkit.Aspire.Hosting.LavinMQ.AppHost/CommunityToolkit.Aspire.Hosting.LavinMQ.AppHost.csproj @@ -0,0 +1,23 @@ + + + + + + Exe + enable + enable + true + cb846a57-bdaf-4abd-a7fb-0299aa095f5e + + + + + + + + + + + + + diff --git a/examples/lavinmq/CommunityToolkit.Aspire.Hosting.LavinMQ.AppHost/Program.cs b/examples/lavinmq/CommunityToolkit.Aspire.Hosting.LavinMQ.AppHost/Program.cs new file mode 100644 index 00000000..6a614d1e --- /dev/null +++ b/examples/lavinmq/CommunityToolkit.Aspire.Hosting.LavinMQ.AppHost/Program.cs @@ -0,0 +1,13 @@ +using Projects; + +var builder = DistributedApplication.CreateBuilder(args); + +var lavinmq = builder.AddLavinMQ("lavinmq") + .PublishAsConnectionString(); + +builder.AddProject("masstransitExample") + .WithReference(lavinmq) + .WithHttpHealthCheck(path: "/health") + .WaitFor(lavinmq); + +builder.Build().Run(); diff --git a/examples/lavinmq/CommunityToolkit.Aspire.Hosting.LavinMQ.AppHost/Properties/launchSettings.json b/examples/lavinmq/CommunityToolkit.Aspire.Hosting.LavinMQ.AppHost/Properties/launchSettings.json new file mode 100644 index 00000000..ed873279 --- /dev/null +++ b/examples/lavinmq/CommunityToolkit.Aspire.Hosting.LavinMQ.AppHost/Properties/launchSettings.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:17276;http://localhost:15246", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21269", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22000" + } + }, + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:15246", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19099", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20012" + } + } + } +} diff --git a/examples/lavinmq/CommunityToolkit.Aspire.Hosting.LavinMQ.AppHost/appsettings.json b/examples/lavinmq/CommunityToolkit.Aspire.Hosting.LavinMQ.AppHost/appsettings.json new file mode 100644 index 00000000..31c092aa --- /dev/null +++ b/examples/lavinmq/CommunityToolkit.Aspire.Hosting.LavinMQ.AppHost/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" + } + } +} diff --git a/examples/lavinmq/CommunityToolkit.Aspire.Hosting.LavinMQ.MassTransit/CommunityToolkit.Aspire.Hosting.LavinMQ.MassTransit.csproj b/examples/lavinmq/CommunityToolkit.Aspire.Hosting.LavinMQ.MassTransit/CommunityToolkit.Aspire.Hosting.LavinMQ.MassTransit.csproj new file mode 100644 index 00000000..2fc7d9fd --- /dev/null +++ b/examples/lavinmq/CommunityToolkit.Aspire.Hosting.LavinMQ.MassTransit/CommunityToolkit.Aspire.Hosting.LavinMQ.MassTransit.csproj @@ -0,0 +1,18 @@ + + + + enable + enable + + + + + + + + + + + + + diff --git a/examples/lavinmq/CommunityToolkit.Aspire.Hosting.LavinMQ.MassTransit/CommunityToolkit.Aspire.Hosting.LavinMQ.MassTransit.http b/examples/lavinmq/CommunityToolkit.Aspire.Hosting.LavinMQ.MassTransit/CommunityToolkit.Aspire.Hosting.LavinMQ.MassTransit.http new file mode 100644 index 00000000..c6943b9e --- /dev/null +++ b/examples/lavinmq/CommunityToolkit.Aspire.Hosting.LavinMQ.MassTransit/CommunityToolkit.Aspire.Hosting.LavinMQ.MassTransit.http @@ -0,0 +1,13 @@ +@CommunityToolkit.Aspire.Hosting.LavinMQ.MassTransit_HostAddress = http://localhost:5004 + +POST http://localhost:5004/send/Hello%20World +Accept: application/json + +### +GET http://localhost:5004/alive +Accept: application/json + +### +GET http://localhost:5004/received +Accept: application/json + diff --git a/examples/lavinmq/CommunityToolkit.Aspire.Hosting.LavinMQ.MassTransit/HelloWorldConsumer.cs b/examples/lavinmq/CommunityToolkit.Aspire.Hosting.LavinMQ.MassTransit/HelloWorldConsumer.cs new file mode 100644 index 00000000..56ef18d3 --- /dev/null +++ b/examples/lavinmq/CommunityToolkit.Aspire.Hosting.LavinMQ.MassTransit/HelloWorldConsumer.cs @@ -0,0 +1,16 @@ +using MassTransit; + +namespace CommunityToolkit.Aspire.Hosting.LavinMQ.MassTransit; + +public class HelloWorldConsumer(ILogger logger, MessageCounter messageCounter) : IConsumer +{ + public async Task Consume(ConsumeContext context) + { + logger.LogInformation("Received message: {Text}", context.Message.Text); + messageCounter.ReceivedMessages++; + await context.RespondAsync(new + { + Reply = "I've received your message: " + context.Message.Text + }); + } +} \ No newline at end of file diff --git a/examples/lavinmq/CommunityToolkit.Aspire.Hosting.LavinMQ.MassTransit/Message.cs b/examples/lavinmq/CommunityToolkit.Aspire.Hosting.LavinMQ.MassTransit/Message.cs new file mode 100644 index 00000000..d09087b1 --- /dev/null +++ b/examples/lavinmq/CommunityToolkit.Aspire.Hosting.LavinMQ.MassTransit/Message.cs @@ -0,0 +1,6 @@ +namespace CommunityToolkit.Aspire.Hosting.LavinMQ.MassTransit; + +public record Message +{ + public required string Text { get; set; } +} \ No newline at end of file diff --git a/examples/lavinmq/CommunityToolkit.Aspire.Hosting.LavinMQ.MassTransit/MessageCounter.cs b/examples/lavinmq/CommunityToolkit.Aspire.Hosting.LavinMQ.MassTransit/MessageCounter.cs new file mode 100644 index 00000000..7c6e2122 --- /dev/null +++ b/examples/lavinmq/CommunityToolkit.Aspire.Hosting.LavinMQ.MassTransit/MessageCounter.cs @@ -0,0 +1,6 @@ +namespace CommunityToolkit.Aspire.Hosting.LavinMQ.MassTransit; + +public class MessageCounter +{ + public int ReceivedMessages { get; set; } +} \ No newline at end of file diff --git a/examples/lavinmq/CommunityToolkit.Aspire.Hosting.LavinMQ.MassTransit/MessageReply.cs b/examples/lavinmq/CommunityToolkit.Aspire.Hosting.LavinMQ.MassTransit/MessageReply.cs new file mode 100644 index 00000000..5998854f --- /dev/null +++ b/examples/lavinmq/CommunityToolkit.Aspire.Hosting.LavinMQ.MassTransit/MessageReply.cs @@ -0,0 +1,6 @@ +namespace CommunityToolkit.Aspire.Hosting.LavinMQ.MassTransit; + +public record MessageReply +{ + public required string Reply { get; set; } +} \ No newline at end of file diff --git a/examples/lavinmq/CommunityToolkit.Aspire.Hosting.LavinMQ.MassTransit/Program.cs b/examples/lavinmq/CommunityToolkit.Aspire.Hosting.LavinMQ.MassTransit/Program.cs new file mode 100644 index 00000000..07bb78c5 --- /dev/null +++ b/examples/lavinmq/CommunityToolkit.Aspire.Hosting.LavinMQ.MassTransit/Program.cs @@ -0,0 +1,39 @@ +using CommunityToolkit.Aspire.Hosting.LavinMQ.MassTransit; +using MassTransit; +using Microsoft.AspNetCore.Mvc; + +var builder = WebApplication.CreateBuilder(args); + +builder.AddServiceDefaults(); +builder.Services + .AddSingleton(KebabCaseEndpointNameFormatter.Instance) + .AddSingleton(); +builder.Services.AddMassTransit(x => +{ + x.UsingRabbitMq((context, cfg) => + { + string connectionString = builder.Configuration.GetConnectionString("lavinmq")!; + cfg.ConfigureEndpoints(context, new KebabCaseEndpointNameFormatter("aspire", false)); + cfg.Host(new Uri(connectionString), _ => {}); + }); + x.AddConsumers(typeof(HelloWorldConsumer).Assembly); +}); + +WebApplication app = builder.Build(); + +app.MapPost("/send/{text}", async (string text, + [FromServices] IRequestClient requestClient, + [FromServices] ILogger logger) => + { + logger.LogInformation("Send message: {Text}", text); + Response response = await requestClient.GetResponse(new { Text = text }); + logger.LogInformation("Sent message: {Text}", text); + return response.Message.Reply; + }) + .WithName("SendMessage"); + +app.MapGet("/received", ([FromServices] MessageCounter messageCounter) => messageCounter) + .WithName("ReceivedMessages"); + +app.MapDefaultEndpoints(); +app.Run(); diff --git a/examples/lavinmq/CommunityToolkit.Aspire.Hosting.LavinMQ.MassTransit/Properties/launchSettings.json b/examples/lavinmq/CommunityToolkit.Aspire.Hosting.LavinMQ.MassTransit/Properties/launchSettings.json new file mode 100644 index 00000000..54f0fa7a --- /dev/null +++ b/examples/lavinmq/CommunityToolkit.Aspire.Hosting.LavinMQ.MassTransit/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5004", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7038;http://localhost:5004", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/examples/lavinmq/CommunityToolkit.Aspire.Hosting.LavinMQ.MassTransit/appsettings.json b/examples/lavinmq/CommunityToolkit.Aspire.Hosting.LavinMQ.MassTransit/appsettings.json new file mode 100644 index 00000000..4db6d5d4 --- /dev/null +++ b/examples/lavinmq/CommunityToolkit.Aspire.Hosting.LavinMQ.MassTransit/appsettings.json @@ -0,0 +1,12 @@ +{ + "ConnectionStrings": { + "lavinmq": "amqp://localhost:5672" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/examples/lavinmq/CommunityToolkit.Aspire.Hosting.LavinMQ.ServiceDefaults/CommunityToolkit.Aspire.Hosting.LavinMQ.ServiceDefaults.csproj b/examples/lavinmq/CommunityToolkit.Aspire.Hosting.LavinMQ.ServiceDefaults/CommunityToolkit.Aspire.Hosting.LavinMQ.ServiceDefaults.csproj new file mode 100644 index 00000000..caa6344d --- /dev/null +++ b/examples/lavinmq/CommunityToolkit.Aspire.Hosting.LavinMQ.ServiceDefaults/CommunityToolkit.Aspire.Hosting.LavinMQ.ServiceDefaults.csproj @@ -0,0 +1,21 @@ + + + + enable + enable + true + + + + + + + + + + + + + + + diff --git a/examples/lavinmq/CommunityToolkit.Aspire.Hosting.LavinMQ.ServiceDefaults/Extensions.cs b/examples/lavinmq/CommunityToolkit.Aspire.Hosting.LavinMQ.ServiceDefaults/Extensions.cs new file mode 100644 index 00000000..fe858bea --- /dev/null +++ b/examples/lavinmq/CommunityToolkit.Aspire.Hosting.LavinMQ.ServiceDefaults/Extensions.cs @@ -0,0 +1,123 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.ServiceDiscovery; +using OpenTelemetry; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; + +namespace Microsoft.Extensions.Hosting; + +// Adds common .NET Aspire services: service discovery, resilience, health checks, and OpenTelemetry. +// This project should be referenced by each service project in your solution. +// To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults +public static class Extensions +{ + private const string MasstransitTag = "MassTransit"; + + public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBuilder builder) + { + builder.ConfigureOpenTelemetry(); + + builder.AddDefaultHealthChecks(); + + builder.Services.AddServiceDiscovery(); + + builder.Services.ConfigureHttpClientDefaults(http => + { + // Turn on resilience by default + http.AddStandardResilienceHandler(); + + // Turn on service discovery by default + http.AddServiceDiscovery(); + }); + + // Uncomment the following to restrict the allowed schemes for service discovery. + // builder.Services.Configure(options => + // { + // options.AllowedSchemes = ["https"]; + // }); + + return builder; + } + + public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicationBuilder builder) + { + builder.Logging.AddOpenTelemetry(logging => + { + logging.IncludeFormattedMessage = true; + logging.IncludeScopes = true; + }); + + builder.Services.AddOpenTelemetry() + .WithMetrics(metrics => + { + metrics.AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation() + .AddMeter(MasstransitTag); + }) + .WithTracing(tracing => + { + tracing.AddAspNetCoreInstrumentation() + // Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package) + //.AddGrpcClientInstrumentation() + .AddHttpClientInstrumentation() + .AddSource(MasstransitTag); + }); + + builder.AddOpenTelemetryExporters(); + + return builder; + } + + private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostApplicationBuilder builder) + { + var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); + + if (useOtlpExporter) + { + builder.Services.AddOpenTelemetry().UseOtlpExporter(); + } + + // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package) + //if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"])) + //{ + // builder.Services.AddOpenTelemetry() + // .UseAzureMonitor(); + //} + + return builder; + } + + public static IHostApplicationBuilder AddDefaultHealthChecks(this IHostApplicationBuilder builder) + { + builder.Services.AddHealthChecks() + // Add a default liveness check to ensure app is responsive + .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); + + return builder; + } + + public static WebApplication MapDefaultEndpoints(this WebApplication app) + { + // Adding health checks endpoints to applications in non-development environments has security implications. + // See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments. + if (app.Environment.IsDevelopment()) + { + // All health checks must pass for app to be considered ready to accept traffic after starting + app.MapHealthChecks("/health"); + + // Only health checks tagged with the "live" tag must pass for app to be considered alive + app.MapHealthChecks("/alive", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("live") || + r.Tags.Contains("masstransit") + }); + } + + return app; + } +} diff --git a/src/CommunityToolkit.Aspire.Hosting.LavinMQ/CommunityToolkit.Aspire.Hosting.LavinMQ.csproj b/src/CommunityToolkit.Aspire.Hosting.LavinMQ/CommunityToolkit.Aspire.Hosting.LavinMQ.csproj new file mode 100644 index 00000000..86ed193e --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.LavinMQ/CommunityToolkit.Aspire.Hosting.LavinMQ.csproj @@ -0,0 +1,18 @@ + + + + hosting LavinMQ + A .NET Aspire hosting package for hosting LavinMQ. + + + + + + + + + + + + + diff --git a/src/CommunityToolkit.Aspire.Hosting.LavinMQ/LavinMQContainerImageSettings.cs b/src/CommunityToolkit.Aspire.Hosting.LavinMQ/LavinMQContainerImageSettings.cs new file mode 100644 index 00000000..6ae606db --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.LavinMQ/LavinMQContainerImageSettings.cs @@ -0,0 +1,13 @@ +namespace CommunityToolkit.Aspire.Hosting.LavinMQ; + +internal static class LavinMQContainerImageSettings +{ + /// docker.io + internal const string Registry = "docker.io"; + + /// cloudamqp/lavinmq + internal const string Image = "cloudamqp/lavinmq"; + + /// 2.1.0 + internal const string Tag = "2.1.0"; +} diff --git a/src/CommunityToolkit.Aspire.Hosting.LavinMQ/LavinMQContainerResource.cs b/src/CommunityToolkit.Aspire.Hosting.LavinMQ/LavinMQContainerResource.cs new file mode 100644 index 00000000..dda14f63 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.LavinMQ/LavinMQContainerResource.cs @@ -0,0 +1,32 @@ +namespace Aspire.Hosting.ApplicationModel; + +/// +/// Represents a container resource for LavinMQ with configurable authentication parameters +/// and connection endpoint details, implementing the IResourceWithConnectionString interface. +/// +/// The name of the LavinMQ resource instance. +public class LavinMQContainerResource(string name) + : ContainerResource(name), IResourceWithConnectionString +{ + private const string DefaultUserName = "guest"; + private const string DefaultPassword = "guest"; + internal const string PrimaryEndpointName = "amqp"; + internal const string ManagementEndpointName = "management"; + internal const string PrimaryEndpointSchema = "amqp"; + internal const string ManagementEndpointSchema = "http"; + internal const int DefaultAmqpPort = 5672; + internal const int DefaultManagementPort = 15672; + + private EndpointReference? _primaryEndpoint; + private EndpointReference PrimaryEndpoint => _primaryEndpoint ??= new EndpointReference(this, PrimaryEndpointName); + + private static ReferenceExpression UserNameReference => ReferenceExpression.Create($"{DefaultUserName}"); + + private static ReferenceExpression PasswordReference => ReferenceExpression.Create($"{DefaultPassword}"); + + /// + /// ConnectionString for the LavinMQ server in the form of amqp://guest:guest@host:port/. + /// + public ReferenceExpression ConnectionStringExpression => + ReferenceExpression.Create($"amqp://{UserNameReference}:{PasswordReference}@{PrimaryEndpoint.Property(EndpointProperty.Host)}:{PrimaryEndpoint.Property(EndpointProperty.Port)}/"); +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Hosting.LavinMQ/LavinMQHostingExtension.cs b/src/CommunityToolkit.Aspire.Hosting.LavinMQ/LavinMQHostingExtension.cs new file mode 100644 index 00000000..bc24b11c --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.LavinMQ/LavinMQHostingExtension.cs @@ -0,0 +1,114 @@ +using Aspire.Hosting.ApplicationModel; +using CommunityToolkit.Aspire.Hosting.LavinMQ; +using Microsoft.Extensions.DependencyInjection; +using RabbitMQ.Client; + +namespace Aspire.Hosting; + +/// +/// Provides extension methods for configuring and adding a LavinMQ container as a resource +/// within a distributed application using Aspire.Hosting. This enables connection, health +/// checks, and specific configurations for LavinMQ instances. +/// +public static class LavinMQHostingExtension +{ + /// + /// Adds a LavinMQ container resource to the distributed application builder. + /// Configures the resource with specified parameters and sets up health checks for the resource. + /// + /// The distributed application builder to which the LavinMQ resource will be added. + /// The name of the LavinMQ resource. + /// The port number for the AMQP protocol. Default is 5672. + /// The port number for the management interface. Default is 15672. + /// A resource builder for the LavinMQ container resource. + /// Thrown when the resource addition fails or other errors occur during the process. + public static IResourceBuilder AddLavinMQ(this IDistributedApplicationBuilder builder, + [ResourceName] string name, + int amqpPort = LavinMQContainerResource.DefaultAmqpPort, + int managementPort = LavinMQContainerResource.DefaultManagementPort) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(name); + + LavinMQContainerResource instance = new LavinMQContainerResource(name); + + string? connectionString = null; + + builder.Eventing.Subscribe(instance, async (_, ct) => + { + connectionString = await instance.ConnectionStringExpression.GetValueAsync(ct).ConfigureAwait(false); + + if (connectionString is null) + { + throw new DistributedApplicationException($"ConnectionStringAvailableEvent was published for the '{instance.Name}' resource but the connection string was null."); + } + }); + + string healthCheckKey = $"{name}_check"; + // cache the connection so it is reused on subsequent calls to the health check + IConnection? connection = null; + builder.Services.AddHealthChecks().AddRabbitMQ(async _ => + { + // NOTE: Ensure that execution of this setup callback is deferred until after + // the container is built & started. + return connection ??= await CreateConnection(connectionString!).ConfigureAwait(false); + + static Task CreateConnection(string connectionString) + { + ConnectionFactory factory = new ConnectionFactory + { + Uri = new(connectionString), + }; + return factory.CreateConnectionAsync(); + } + }, healthCheckKey); + + return builder.AddResource(instance) + .WithImage(LavinMQContainerImageSettings.Image, LavinMQContainerImageSettings.Tag) + .WithImageRegistry(LavinMQContainerImageSettings.Registry) + .WithEndpoint( + port: amqpPort, + targetPort: LavinMQContainerResource.DefaultAmqpPort, + name: LavinMQContainerResource.PrimaryEndpointName, + scheme: LavinMQContainerResource.PrimaryEndpointSchema) + .WithEndpoint( + port: managementPort, + targetPort: LavinMQContainerResource.DefaultManagementPort, + name: LavinMQContainerResource.ManagementEndpointName, + scheme: LavinMQContainerResource.ManagementEndpointSchema) + .WithHealthCheck(healthCheckKey); + } + + /// + /// Configures a data volume for the LavinMQ container resource by specifying its name and read-only status. + /// + /// The resource builder for the LavinMQ container resource. + /// The name of the data volume to be attached to the LavinMQ container resource. + /// Indicates whether the data volume should be mounted as read-only. Default is false. + /// The updated resource builder for the LavinMQ container resource. + public static IResourceBuilder WithDataVolume(this IResourceBuilder builder, string name, + bool isReadOnly = false) + { + ArgumentNullException.ThrowIfNull(builder); + + return builder.WithVolume(name, "/var/lib/lavinmq", isReadOnly); + } + + /// + /// Configures a bind mount for the LavinMQ container resource to allow data persistence. + /// The method mounts a specified source path on the host to the container's data directory. + /// + /// The resource builder for the LavinMQ container to which the data bind mount will be added. + /// The source path on the host machine to bind mount to the container's data directory. + /// Indicates if the bind mount should be configured as read-only. Default is false. + /// An updated resource builder for the LavinMQ container resource with the configured data bind mount. + /// Thrown when the builder or source parameters are null. + public static IResourceBuilder WithDataBindMount(this IResourceBuilder builder, string source, + bool isReadOnly = false) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(source); + + return builder.WithBindMount(source, "/var/lib/lavinmq", isReadOnly); + } +} \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Hosting.LavinMQ/PublicAPI.Shipped.txt b/src/CommunityToolkit.Aspire.Hosting.LavinMQ/PublicAPI.Shipped.txt new file mode 100644 index 00000000..7dc5c581 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.LavinMQ/PublicAPI.Shipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/src/CommunityToolkit.Aspire.Hosting.LavinMQ/PublicAPI.Unshipped.txt b/src/CommunityToolkit.Aspire.Hosting.LavinMQ/PublicAPI.Unshipped.txt new file mode 100644 index 00000000..a3e9fd87 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.LavinMQ/PublicAPI.Unshipped.txt @@ -0,0 +1,8 @@ +#nullable enable +Aspire.Hosting.ApplicationModel.LavinMQContainerResource +Aspire.Hosting.ApplicationModel.LavinMQContainerResource.ConnectionStringExpression.get -> Aspire.Hosting.ApplicationModel.ReferenceExpression! +Aspire.Hosting.ApplicationModel.LavinMQContainerResource.LavinMQContainerResource(string! name) -> void +Aspire.Hosting.LavinMQHostingExtension +static Aspire.Hosting.LavinMQHostingExtension.AddLavinMQ(this Aspire.Hosting.IDistributedApplicationBuilder! builder, string! name, int amqpPort = 5672, int managementPort = 15672) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! +static Aspire.Hosting.LavinMQHostingExtension.WithDataBindMount(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string! source, bool isReadOnly = false) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! +static Aspire.Hosting.LavinMQHostingExtension.WithDataVolume(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string! name, bool isReadOnly = false) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Hosting.LavinMQ/README.md b/src/CommunityToolkit.Aspire.Hosting.LavinMQ/README.md new file mode 100644 index 00000000..b9a06864 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.LavinMQ/README.md @@ -0,0 +1,23 @@ +# CommunityToolkit.Hosting.LavinMQ + +## Overview + +This .NET Aspire Integration can be used to include [LavinMQ](https://lavinmq.com/) in a container. + +## Usage + +### Example 1: LavinMQ with default ports + +```csharp +var builder = DistributedApplication.CreateBuilder(args); + +var lavinmq = builder.AddLavinMQ("lavinmq"); +``` + +### Example 2: LavinMQ with custom ports + +```csharp +var builder = DistributedApplication.CreateBuilder(args); + +var lavinmq = builder.AddLavinMQ("lavinmq", amqpPort: 5672, managementPort: 15672); +``` diff --git a/tests/CommunityToolkit.Aspire.Hosting.LavinMQ.Tests/AppHostTests.cs b/tests/CommunityToolkit.Aspire.Hosting.LavinMQ.Tests/AppHostTests.cs new file mode 100644 index 00000000..a1f17e05 --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Hosting.LavinMQ.Tests/AppHostTests.cs @@ -0,0 +1,53 @@ +using Aspire.Components.Common.Tests; +using CommunityToolkit.Aspire.Hosting.LavinMQ.MassTransit; +using CommunityToolkit.Aspire.Testing; +using System.Net.Http.Json; + +namespace CommunityToolkit.Aspire.Hosting.LavinMQ.Tests; + +[RequiresDocker] +public class AppHostTests(AspireIntegrationTestFixture fixture) : IClassFixture> +{ + [Fact] + public async Task ResourceStartsAndRespondsOk() + { + const string resourceName = "masstransitExample"; + await fixture.ResourceNotificationService.WaitForResourceHealthyAsync(resourceName).WaitAsync(TimeSpan.FromMinutes(2)); + HttpClient httpClient = fixture.CreateHttpClient(resourceName); + + HttpResponseMessage response = await httpClient.PostAsync("/send/Hello%20World", new StringContent("")); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + [Fact] + public async Task ReplyShouldBeReceived() + { + const string resourceName = "masstransitExample"; + await fixture.ResourceNotificationService.WaitForResourceHealthyAsync(resourceName).WaitAsync(TimeSpan.FromMinutes(2)); + HttpClient httpClient = fixture.CreateHttpClient(resourceName); + + HttpResponseMessage response = await httpClient.PostAsync("/send/Hello%20World", new StringContent("")); + + string message = (await response.Content.ReadAsStringAsync()); + Assert.Equal("I've received your message: Hello World", message); + } + + [Fact] + public async Task WhenMessageIsSendItShouldBeReceivedByConsumer() + { + const string resourceName = "masstransitExample"; + await fixture.ResourceNotificationService.WaitForResourceHealthyAsync(resourceName).WaitAsync(TimeSpan.FromMinutes(2)); + HttpClient httpClient = fixture.CreateHttpClient(resourceName); + + MessageCounter? oldMessageCounter = await httpClient.GetFromJsonAsync("/received"); + Assert.NotNull(oldMessageCounter); + + await httpClient.PostAsync("/send/Hello%20World", new StringContent("")); + + MessageCounter? messageCounter = await httpClient.GetFromJsonAsync("/received"); + + Assert.NotNull(messageCounter); + Assert.True(messageCounter.ReceivedMessages > oldMessageCounter.ReceivedMessages); + } +} \ No newline at end of file diff --git a/tests/CommunityToolkit.Aspire.Hosting.LavinMQ.Tests/CommunityToolkit.Aspire.Hosting.LavinMQ.Tests.csproj b/tests/CommunityToolkit.Aspire.Hosting.LavinMQ.Tests/CommunityToolkit.Aspire.Hosting.LavinMQ.Tests.csproj new file mode 100644 index 00000000..c6bc4981 --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Hosting.LavinMQ.Tests/CommunityToolkit.Aspire.Hosting.LavinMQ.Tests.csproj @@ -0,0 +1,15 @@ + + + + false + true + + + + + + + + + + diff --git a/tests/CommunityToolkit.Aspire.Hosting.LavinMQ.Tests/ContainerResourceCreationTests.cs b/tests/CommunityToolkit.Aspire.Hosting.LavinMQ.Tests/ContainerResourceCreationTests.cs new file mode 100644 index 00000000..73aa4f34 --- /dev/null +++ b/tests/CommunityToolkit.Aspire.Hosting.LavinMQ.Tests/ContainerResourceCreationTests.cs @@ -0,0 +1,85 @@ +using Aspire.Hosting; + +namespace CommunityToolkit.Aspire.Hosting.LavinMQ.Tests; + +public class ContainerResourceCreationTests +{ + [Fact] + public void AddLavinMqApiBuilderBuilderShouldNotBeNull() + { + IDistributedApplicationBuilder builder = null!; + Assert.Throws(() => builder.AddLavinMQ("lavinmq")); + } + + [Fact] + public void AddLavinMqApiBuilderNameShouldNotBeNullOrWhiteSpace() + { + IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder(); + Assert.Throws(() => builder.AddLavinMQ(null!)); + } + + [Fact] + public void AddLavinMqApiBuilderContainerDetailsSetOnResource() + { + IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder(); + + builder.AddLavinMQ("lavinmq"); + + using DistributedApplication app = builder.Build(); + DistributedApplicationModel appModel = app.Services.GetRequiredService(); + + LavinMQContainerResource? resource = appModel.Resources.OfType().SingleOrDefault(); + + Assert.NotNull(resource); + Assert.Equal("lavinmq", resource.Name); + ValidateLavinMqContainerImageAnnotations(resource); + ValidateEndpointAnnotations(resource, LavinMQContainerResource.DefaultAmqpPort, LavinMQContainerResource.DefaultManagementPort); + } + + [Fact] + public void AddLavinMqApiBuilderContainerDetailsSetOnResourceCustomPorts() + { + const int amqpPort = 1111; + const int managementPort = 2222; + + IDistributedApplicationBuilder builder = DistributedApplication.CreateBuilder(); + + builder.AddLavinMQ("lavinmq", amqpPort, managementPort); + + using DistributedApplication app = builder.Build(); + DistributedApplicationModel appModel = app.Services.GetRequiredService(); + + LavinMQContainerResource? resource = appModel.Resources.OfType().SingleOrDefault(); + + Assert.NotNull(resource); + Assert.Equal("lavinmq", resource.Name); + ValidateLavinMqContainerImageAnnotations(resource); + ValidateEndpointAnnotations(resource, amqpPort, managementPort); + } + + private static void ValidateLavinMqContainerImageAnnotations(LavinMQContainerResource resource) + { + Assert.True(resource.TryGetLastAnnotation(out ContainerImageAnnotation? imageAnnotations)); + Assert.Equal("2.1.0", imageAnnotations.Tag); + Assert.Equal("cloudamqp/lavinmq", imageAnnotations.Image); + Assert.Equal("docker.io", imageAnnotations.Registry); + } + + private static void ValidateEndpointAnnotations(LavinMQContainerResource resource, int amqpPort, int managementPort) + { + resource.TryGetAnnotationsOfType(out IEnumerable? annotations); + Assert.NotNull(annotations); + + List endpointAnnotations = annotations.ToList(); + + Assert.Equal(2, endpointAnnotations.Count); + + Assert.Equal(LavinMQContainerResource.PrimaryEndpointSchema, endpointAnnotations[0].UriScheme); + Assert.Equal(LavinMQContainerResource.PrimaryEndpointName, endpointAnnotations[0].Name); + Assert.Equal(amqpPort, endpointAnnotations[0].Port); + + Assert.Equal(LavinMQContainerResource.ManagementEndpointSchema, endpointAnnotations[1].UriScheme); + Assert.Equal(LavinMQContainerResource.ManagementEndpointName, endpointAnnotations[1].Name); + Assert.Equal(managementPort, endpointAnnotations[1].Port); + } +}