From 89ea7085c7d376adc7f35966865eb7b60d2381da Mon Sep 17 00:00:00 2001 From: Toby Henderson Date: Thu, 15 Feb 2024 14:02:20 +0000 Subject: [PATCH 1/3] feat(aspire): Added aspire to RMQTaskQueue sample --- Brighter.sln | 30 +++++ Directory.Packages.props | 11 +- samples/RMQTaskQueue/AppHost/AppHost.csproj | 20 +++ samples/RMQTaskQueue/AppHost/Program.cs | 10 ++ .../AppHost/Properties/launchSettings.json | 16 +++ .../AppHost/appsettings.Development.json | 8 ++ samples/RMQTaskQueue/AppHost/appsettings.json | 9 ++ .../GreetingsReceiverConsole.csproj | 2 + .../GreetingsReceiverConsole/Program.cs | 90 +++++++------ .../Properties/launchSettings.json | 12 ++ .../GreetingsSender/GreetingsSender.csproj | 2 + .../RMQTaskQueue/GreetingsSender/Program.cs | 118 ++++++++--------- .../Properties/launchSettings.json | 12 ++ .../ServiceDefaults/Extensions.cs | 125 ++++++++++++++++++ .../ServiceDefaults/ServiceDefaults.csproj | 23 ++++ .../MessagePump.cs | 2 +- 16 files changed, 387 insertions(+), 103 deletions(-) create mode 100644 samples/RMQTaskQueue/AppHost/AppHost.csproj create mode 100644 samples/RMQTaskQueue/AppHost/Program.cs create mode 100644 samples/RMQTaskQueue/AppHost/Properties/launchSettings.json create mode 100644 samples/RMQTaskQueue/AppHost/appsettings.Development.json create mode 100644 samples/RMQTaskQueue/AppHost/appsettings.json create mode 100644 samples/RMQTaskQueue/GreetingsReceiverConsole/Properties/launchSettings.json create mode 100644 samples/RMQTaskQueue/GreetingsSender/Properties/launchSettings.json create mode 100644 samples/RMQTaskQueue/ServiceDefaults/Extensions.cs create mode 100644 samples/RMQTaskQueue/ServiceDefaults/ServiceDefaults.csproj diff --git a/Brighter.sln b/Brighter.sln index 8757596dca..ab994eb3a5 100644 --- a/Brighter.sln +++ b/Brighter.sln @@ -337,6 +337,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Paramore.Brighter.ServiceAc EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Paramore.Brighter.Sqlite.Dapper", "src\Paramore.Brighter.Sqlite.Dapper\Paramore.Brighter.Sqlite.Dapper.csproj", "{3384FBF0-5DCB-452D-8288-FAD1D0023089}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AppHost", "samples\RMQTaskQueue\AppHost\AppHost.csproj", "{995A09D0-C440-4E88-A0C6-7AF93D82CA82}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ServiceDefaults", "samples\RMQTaskQueue\ServiceDefaults\ServiceDefaults.csproj", "{69AF4ECC-FABB-431F-89CF-0AF972BF197F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1931,6 +1935,30 @@ Global {3384FBF0-5DCB-452D-8288-FAD1D0023089}.Release|Mixed Platforms.Build.0 = Release|Any CPU {3384FBF0-5DCB-452D-8288-FAD1D0023089}.Release|x86.ActiveCfg = Release|Any CPU {3384FBF0-5DCB-452D-8288-FAD1D0023089}.Release|x86.Build.0 = Release|Any CPU + {995A09D0-C440-4E88-A0C6-7AF93D82CA82}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {995A09D0-C440-4E88-A0C6-7AF93D82CA82}.Debug|Any CPU.Build.0 = Debug|Any CPU + {995A09D0-C440-4E88-A0C6-7AF93D82CA82}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {995A09D0-C440-4E88-A0C6-7AF93D82CA82}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {995A09D0-C440-4E88-A0C6-7AF93D82CA82}.Debug|x86.ActiveCfg = Debug|Any CPU + {995A09D0-C440-4E88-A0C6-7AF93D82CA82}.Debug|x86.Build.0 = Debug|Any CPU + {995A09D0-C440-4E88-A0C6-7AF93D82CA82}.Release|Any CPU.ActiveCfg = Release|Any CPU + {995A09D0-C440-4E88-A0C6-7AF93D82CA82}.Release|Any CPU.Build.0 = Release|Any CPU + {995A09D0-C440-4E88-A0C6-7AF93D82CA82}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {995A09D0-C440-4E88-A0C6-7AF93D82CA82}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {995A09D0-C440-4E88-A0C6-7AF93D82CA82}.Release|x86.ActiveCfg = Release|Any CPU + {995A09D0-C440-4E88-A0C6-7AF93D82CA82}.Release|x86.Build.0 = Release|Any CPU + {69AF4ECC-FABB-431F-89CF-0AF972BF197F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {69AF4ECC-FABB-431F-89CF-0AF972BF197F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {69AF4ECC-FABB-431F-89CF-0AF972BF197F}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {69AF4ECC-FABB-431F-89CF-0AF972BF197F}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {69AF4ECC-FABB-431F-89CF-0AF972BF197F}.Debug|x86.ActiveCfg = Debug|Any CPU + {69AF4ECC-FABB-431F-89CF-0AF972BF197F}.Debug|x86.Build.0 = Debug|Any CPU + {69AF4ECC-FABB-431F-89CF-0AF972BF197F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {69AF4ECC-FABB-431F-89CF-0AF972BF197F}.Release|Any CPU.Build.0 = Release|Any CPU + {69AF4ECC-FABB-431F-89CF-0AF972BF197F}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {69AF4ECC-FABB-431F-89CF-0AF972BF197F}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {69AF4ECC-FABB-431F-89CF-0AF972BF197F}.Release|x86.ActiveCfg = Release|Any CPU + {69AF4ECC-FABB-431F-89CF-0AF972BF197F}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -2044,6 +2072,8 @@ Global {D7361C21-AB4D-4E82-8094-FB86C2ED1800} = {65F8C2DE-0CB6-4102-8187-A247F1D5D3D7} {18742337-075A-40D6-B67F-91F5894A50C3} = {65F8C2DE-0CB6-4102-8187-A247F1D5D3D7} {AA2AA086-9B8A-4910-A793-E92B1E352351} = {329736D2-BF92-4D06-A7BF-19F4B6B64EDD} + {995A09D0-C440-4E88-A0C6-7AF93D82CA82} = {E9748DC0-6E72-4634-AF49-428F806E03B0} + {69AF4ECC-FABB-431F-89CF-0AF972BF197F} = {E9748DC0-6E72-4634-AF49-428F806E03B0} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {8B7C7E31-2E32-4E0D-9426-BC9AF22E9F4C} diff --git a/Directory.Packages.props b/Directory.Packages.props index 90ecb0bce8..9ad1347319 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,6 +4,9 @@ false + + + @@ -38,8 +41,10 @@ + + @@ -55,9 +60,13 @@ + - + + + + diff --git a/samples/RMQTaskQueue/AppHost/AppHost.csproj b/samples/RMQTaskQueue/AppHost/AppHost.csproj new file mode 100644 index 0000000000..f35acd51cc --- /dev/null +++ b/samples/RMQTaskQueue/AppHost/AppHost.csproj @@ -0,0 +1,20 @@ + + + + Exe + net8.0 + enable + enable + true + + + + + + + + + + + + diff --git a/samples/RMQTaskQueue/AppHost/Program.cs b/samples/RMQTaskQueue/AppHost/Program.cs new file mode 100644 index 0000000000..f9e1bc0c11 --- /dev/null +++ b/samples/RMQTaskQueue/AppHost/Program.cs @@ -0,0 +1,10 @@ +var builder = DistributedApplication.CreateBuilder(args); + +var rabbit = builder.AddRabbitMQ("messaging"); + +var receiver = builder.AddProject("receiver") + .WithReference(rabbit); +var sender = builder.AddProject("sender") + .WithReference(rabbit); + +builder.Build().Run(); diff --git a/samples/RMQTaskQueue/AppHost/Properties/launchSettings.json b/samples/RMQTaskQueue/AppHost/Properties/launchSettings.json new file mode 100644 index 0000000000..38f25fcdf1 --- /dev/null +++ b/samples/RMQTaskQueue/AppHost/Properties/launchSettings.json @@ -0,0 +1,16 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:15281", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:16176" + } + } + } +} diff --git a/samples/RMQTaskQueue/AppHost/appsettings.Development.json b/samples/RMQTaskQueue/AppHost/appsettings.Development.json new file mode 100644 index 0000000000..0c208ae918 --- /dev/null +++ b/samples/RMQTaskQueue/AppHost/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/samples/RMQTaskQueue/AppHost/appsettings.json b/samples/RMQTaskQueue/AppHost/appsettings.json new file mode 100644 index 0000000000..31c092aa45 --- /dev/null +++ b/samples/RMQTaskQueue/AppHost/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" + } + } +} diff --git a/samples/RMQTaskQueue/GreetingsReceiverConsole/GreetingsReceiverConsole.csproj b/samples/RMQTaskQueue/GreetingsReceiverConsole/GreetingsReceiverConsole.csproj index 0bca6766d1..8cc64c1389 100644 --- a/samples/RMQTaskQueue/GreetingsReceiverConsole/GreetingsReceiverConsole.csproj +++ b/samples/RMQTaskQueue/GreetingsReceiverConsole/GreetingsReceiverConsole.csproj @@ -5,8 +5,10 @@ + + diff --git a/samples/RMQTaskQueue/GreetingsReceiverConsole/Program.cs b/samples/RMQTaskQueue/GreetingsReceiverConsole/Program.cs index 135c7faf38..b1f35e58fd 100644 --- a/samples/RMQTaskQueue/GreetingsReceiverConsole/Program.cs +++ b/samples/RMQTaskQueue/GreetingsReceiverConsole/Program.cs @@ -22,6 +22,7 @@ THE SOFTWARE. */ #endregion +using Aspire.RabbitMQ.Client; using System; using System.Threading.Tasks; using Greetings.Ports.Commands; @@ -32,6 +33,7 @@ THE SOFTWARE. */ using Paramore.Brighter.MessagingGateway.RMQ; using Paramore.Brighter.ServiceActivator.Extensions.DependencyInjection; using Paramore.Brighter.ServiceActivator.Extensions.Hosting; +using RabbitMQ.Client; using Serilog; namespace GreetingsReceiverConsole @@ -46,52 +48,56 @@ public static async Task Main(string[] args) .WriteTo.Console() .CreateLogger(); - var host = new HostBuilder() - .ConfigureServices((hostContext, services) => + var subscriptions = new Subscription[] + { + new RmqSubscription( + new SubscriptionName("paramore.example.greeting"), + new ChannelName("greeting.event"), + new RoutingKey("greeting.event"), + timeoutInMilliseconds: 200, + isDurable: true, + highAvailability: true, + makeChannels: OnMissingChannel + .Create), //change to OnMissingChannel.Validate if you have infrastructure declared elsewhere + new RmqSubscription( + new SubscriptionName( + "paramore.example.farewell"), //change to OnMissingChannel.Validate if you have infrastructure declared elsewhere + new ChannelName("farewell.event"), + new RoutingKey("farewell.event"), + timeoutInMilliseconds: 200, + isDurable: true, + highAvailability: true, + makeChannels: OnMissingChannel.Create) + }; + var builder = new HostApplicationBuilder(); + + + builder.AddServiceDefaults(); + builder.AddRabbitMQ("messaging"); + builder.Logging.AddSerilog(); + + var rmqConnection = new RmqMessagingGatewayConnection + { + AmpqUri = new AmqpUriSpecification(builder.Services.BuildServiceProvider() + .GetService().Uri), + Exchange = new Exchange("paramore.brighter.exchange") + }; + + var rmqMessageConsumerFactory = new RmqMessageConsumerFactory(rmqConnection); + + builder.Services.AddServiceActivator(options => { - var subscriptions = new Subscription[] - { - new RmqSubscription( - new SubscriptionName("paramore.example.greeting"), - new ChannelName("greeting.event"), - new RoutingKey("greeting.event"), - timeoutInMilliseconds: 200, - isDurable: true, - highAvailability: true, - makeChannels: OnMissingChannel.Create), //change to OnMissingChannel.Validate if you have infrastructure declared elsewhere - new RmqSubscription( - new SubscriptionName("paramore.example.farewell"), //change to OnMissingChannel.Validate if you have infrastructure declared elsewhere - new ChannelName("farewell.event"), - new RoutingKey("farewell.event"), - timeoutInMilliseconds: 200, - isDurable: true, - highAvailability: true, - makeChannels: OnMissingChannel.Create) - }; - - var rmqConnection = new RmqMessagingGatewayConnection - { - AmpqUri = new AmqpUriSpecification(new Uri("amqp://guest:guest@localhost:5672")), - Exchange = new Exchange("paramore.brighter.exchange") - }; - - var rmqMessageConsumerFactory = new RmqMessageConsumerFactory(rmqConnection); - - services.AddServiceActivator(options => - { - options.Subscriptions = subscriptions; - options.ChannelFactory = new ChannelFactory(rmqMessageConsumerFactory); - }) - .AutoFromAssemblies(); - - - services.AddHostedService(); + options.Subscriptions = subscriptions; + options.ChannelFactory = new ChannelFactory(rmqMessageConsumerFactory); + + }) - .UseConsoleLifetime() - .UseSerilog() - .Build(); + .AutoFromAssemblies(); + + builder.Services.AddHostedService(); + using var host = builder.Build(); await host.RunAsync(); } } diff --git a/samples/RMQTaskQueue/GreetingsReceiverConsole/Properties/launchSettings.json b/samples/RMQTaskQueue/GreetingsReceiverConsole/Properties/launchSettings.json new file mode 100644 index 0000000000..eedf326775 --- /dev/null +++ b/samples/RMQTaskQueue/GreetingsReceiverConsole/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "GreetingsReceiverConsole": { + "commandName": "Project", + "dotnetRunMessages": true, + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + } + } + } +} diff --git a/samples/RMQTaskQueue/GreetingsSender/GreetingsSender.csproj b/samples/RMQTaskQueue/GreetingsSender/GreetingsSender.csproj index faef444721..bc4e023146 100644 --- a/samples/RMQTaskQueue/GreetingsSender/GreetingsSender.csproj +++ b/samples/RMQTaskQueue/GreetingsSender/GreetingsSender.csproj @@ -4,6 +4,7 @@ net8.0 + @@ -14,6 +15,7 @@ + diff --git a/samples/RMQTaskQueue/GreetingsSender/Program.cs b/samples/RMQTaskQueue/GreetingsSender/Program.cs index 59fb8d000e..bfdbcadddd 100644 --- a/samples/RMQTaskQueue/GreetingsSender/Program.cs +++ b/samples/RMQTaskQueue/GreetingsSender/Program.cs @@ -1,4 +1,5 @@ #region Licence + /* The MIT License (MIT) Copyright © 2017 Ian Cooper @@ -23,72 +24,71 @@ THE SOFTWARE. */ #endregion using System; -using System.Transactions; using Greetings.Ports.Commands; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Hosting; using Paramore.Brighter; using Paramore.Brighter.Extensions.DependencyInjection; using Paramore.Brighter.MessagingGateway.RMQ; -using Serilog; -using Serilog.Extensions.Logging; +using RabbitMQ.Client; + + +//private static ActivitySource source = new ActivitySource("GreetingsSender", "1.0.0"); + -namespace GreetingsSender +HostApplicationBuilder builder = Host.CreateApplicationBuilder(args); + +builder.AddServiceDefaults(); +builder.AddRabbitMQ("messaging"); + +RmqMessagingGatewayConnection rmqConnection = new() { - class Program + AmpqUri = new AmqpUriSpecification(builder.Services.BuildServiceProvider().GetService().Uri), + Exchange = new Exchange("paramore.brighter.exchange") +}; + +IAmAProducerRegistry producerRegistry = new RmqProducerRegistryFactory( + rmqConnection, + new RmqPublication[] { - static void Main(string[] args) + new() + { + MaxOutStandingMessages = 5, + MaxOutStandingCheckIntervalMilliSeconds = 500, + WaitForConfirmsTimeOutInMilliseconds = 1000, + MakeChannels = OnMissingChannel.Create, + Topic = new RoutingKey("greeting.event") + }, + new() { - Log.Logger = new LoggerConfiguration() - .MinimumLevel.Debug() - .Enrich.FromLogContext() - .WriteTo.Console() - .CreateLogger(); - - var serviceCollection = new ServiceCollection(); - serviceCollection.AddSingleton(new SerilogLoggerFactory()); - - var rmqConnection = new RmqMessagingGatewayConnection - { - AmpqUri = new AmqpUriSpecification(new Uri("amqp://guest:guest@localhost:5672")), - Exchange = new Exchange("paramore.brighter.exchange"), - }; - - var producerRegistry = new RmqProducerRegistryFactory( - rmqConnection, - new RmqPublication[] - { - new() - { - MaxOutStandingMessages = 5, - MaxOutStandingCheckIntervalMilliSeconds = 500, - WaitForConfirmsTimeOutInMilliseconds = 1000, - MakeChannels =OnMissingChannel.Create, - Topic = new RoutingKey("greeting.event") - }, - new() - { - MaxOutStandingMessages = 5, - MaxOutStandingCheckIntervalMilliSeconds = 500, - WaitForConfirmsTimeOutInMilliseconds = 1000, - MakeChannels =OnMissingChannel.Create, - Topic = new RoutingKey("farewell.event") - } - }).Create(); - - serviceCollection.AddBrighter() - .UseExternalBus((configure) => - { - configure.ProducerRegistry = producerRegistry; - }) - .AutoFromAssemblies(); - - var serviceProvider = serviceCollection.BuildServiceProvider(); - - var commandProcessor = serviceProvider.GetService(); - - commandProcessor.Post(new GreetingEvent("Ian says: Hi there!")); - commandProcessor.Post(new FarewellEvent("Ian says: See you later!")); + MaxOutStandingMessages = 5, + MaxOutStandingCheckIntervalMilliSeconds = 500, + WaitForConfirmsTimeOutInMilliseconds = 1000, + MakeChannels = OnMissingChannel.Create, + Topic = new RoutingKey("farewell.event") } - } -} + }).Create(); + +builder.Services.AddBrighter() + .UseExternalBus(configure => + { + configure.ProducerRegistry = producerRegistry; + }) + .AutoFromAssemblies(); + +IHost host = builder.Build(); + +IAmACommandProcessor commandProcessor = host.Services.GetService(); + +Console.ReadKey(); +// using (var activity = source.StartActivity("Post")) +//{ +commandProcessor.Post(new GreetingEvent("Ian says: Hi there!")); +//} + +//using (var activity = source.StartActivity("Post")) +//{ +commandProcessor.Post(new FarewellEvent("Ian says: See you later!")); +//} + +host.Run(); diff --git a/samples/RMQTaskQueue/GreetingsSender/Properties/launchSettings.json b/samples/RMQTaskQueue/GreetingsSender/Properties/launchSettings.json new file mode 100644 index 0000000000..c5ab01ab1b --- /dev/null +++ b/samples/RMQTaskQueue/GreetingsSender/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "GreetingsSender": { + "commandName": "Project", + "dotnetRunMessages": true, + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + } + } + } +} diff --git a/samples/RMQTaskQueue/ServiceDefaults/Extensions.cs b/samples/RMQTaskQueue/ServiceDefaults/Extensions.cs new file mode 100644 index 0000000000..bb7ebd68b7 --- /dev/null +++ b/samples/RMQTaskQueue/ServiceDefaults/Extensions.cs @@ -0,0 +1,125 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Logging; +using OpenTelemetry.Logs; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; + +namespace Microsoft.Extensions.Hosting; + +public static class Extensions +{ + 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.UseServiceDiscovery(); + }); + + 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.AddRuntimeInstrumentation() + .AddBuiltInMeters(); + }) + .WithTracing(tracing => + { + if (builder.Environment.IsDevelopment()) + { + // We want to view all traces in development + tracing.SetSampler(new AlwaysOnSampler()); + } + + tracing.AddAspNetCoreInstrumentation() + .AddGrpcClientInstrumentation() + .AddHttpClientInstrumentation() + .AddSource( + "Paramore.Brighter", + "Paramore.Brighter.ServiceActivator", + "Brighter", + "Aspire.RabbitMQ.Client"); + }); + + 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.Configure(logging => logging.AddOtlpExporter()); + builder.Services.ConfigureOpenTelemetryMeterProvider(metrics => metrics.AddOtlpExporter()); + builder.Services.ConfigureOpenTelemetryTracerProvider(tracing => tracing.AddOtlpExporter()); + } + + // Uncomment the following lines to enable the Prometheus exporter (requires the OpenTelemetry.Exporter.Prometheus.AspNetCore package) + // builder.Services.AddOpenTelemetry() + // .WithMetrics(metrics => metrics.AddPrometheusExporter()); + + // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package) + // 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) + { + // Uncomment the following line to enable the Prometheus endpoint (requires the OpenTelemetry.Exporter.Prometheus.AspNetCore package) + // app.MapPrometheusScrapingEndpoint(); + + // 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") }); + + return app; + } + + private static MeterProviderBuilder AddBuiltInMeters(this MeterProviderBuilder meterProviderBuilder) => + meterProviderBuilder.AddMeter( + "Microsoft.AspNetCore.Hosting", + "Microsoft.AspNetCore.Server.Kestrel", + "System.Net.Http", + "Paramore.Brighter", + "Paramore.Brighter.ServiceActivator", + "Brighter", + "Aspire.RabbitMQ.Client"); +} diff --git a/samples/RMQTaskQueue/ServiceDefaults/ServiceDefaults.csproj b/samples/RMQTaskQueue/ServiceDefaults/ServiceDefaults.csproj new file mode 100644 index 0000000000..2e29dc1eda --- /dev/null +++ b/samples/RMQTaskQueue/ServiceDefaults/ServiceDefaults.csproj @@ -0,0 +1,23 @@ + + + + Library + net8.0 + enable + enable + true + + + + + + + + + + + + + + + diff --git a/src/Paramore.Brighter.ServiceActivator/MessagePump.cs b/src/Paramore.Brighter.ServiceActivator/MessagePump.cs index 15f5e4ad9d..db7c0a979b 100644 --- a/src/Paramore.Brighter.ServiceActivator/MessagePump.cs +++ b/src/Paramore.Brighter.ServiceActivator/MessagePump.cs @@ -53,7 +53,7 @@ public abstract class MessagePump : IAmAMessagePump where TRequest : c internal static readonly ILogger s_logger = ApplicationLogging.CreateLogger>(); private static readonly ActivitySource _activitySource = new ActivitySource("Paramore.Brighter.ServiceActivator", - Assembly.GetAssembly(typeof(CommandProcessor)).GetName().Version.ToString()); + Assembly.GetAssembly(typeof(CommandProcessor))?.GetName().Version?.ToString()); protected readonly IAmACommandProcessorProvider CommandProcessorProvider; private int _unacceptableMessageCount = 0; From bb4c8328e8bc8b6c44df2abf81ff531315982d31 Mon Sep 17 00:00:00 2001 From: Toby Henderson Date: Thu, 15 Feb 2024 14:20:54 +0000 Subject: [PATCH 2/3] instructions --- Brighter.sln | 3 +++ samples/AspireSetup.md | 44 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 samples/AspireSetup.md diff --git a/Brighter.sln b/Brighter.sln index ab994eb3a5..9621f789d7 100644 --- a/Brighter.sln +++ b/Brighter.sln @@ -4,6 +4,9 @@ Microsoft Visual Studio Solution File, Format Version 12.00 VisualStudioVersion = 17.0.32112.339 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{235DE1F1-E71B-4817-8E27-3B34FF006E4C}" + ProjectSection(SolutionItems) = preProject + samples\AspireSetup.md = samples\AspireSetup.md + EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{329736D2-BF92-4D06-A7BF-19F4B6B64EDD}" ProjectSection(SolutionItems) = preProject diff --git a/samples/AspireSetup.md b/samples/AspireSetup.md new file mode 100644 index 0000000000..1b73187ab7 --- /dev/null +++ b/samples/AspireSetup.md @@ -0,0 +1,44 @@ +Aspire documentation +https://learn.microsoft.com/en-us/dotnet/aspire/ + +Visual Studio: +Visual Studio 2022 Preview version 17.9 or higher (Optional) + +Rider: +plugin is https://plugins.jetbrains.com/plugin/23289--net-aspire + + +To install the aspire workload +```shell +dotnet workload update +dotnet workload install aspire +``` +To check your workload +```shell +dotnet workload list +``` +To update your workloads +```shell +dotnet workload update +``` + +Aspire template +```shell +dotnet new list aspire +``` + +Try the sample-starter in a new directory +```shell +mkdir TestAspire +cd TestAspire +dotnet new aspire-starter +dotnet run --project TestAspire.AppHost +``` + +To run sample from the commandline + +```shell +dotnet run --project samples/RMQTaskQueue/AppHost +``` + + From 4e3bfa32fca55801b876148cbf6eeaba34687609 Mon Sep 17 00:00:00 2001 From: Toby Henderson Date: Mon, 19 Feb 2024 15:57:56 +0000 Subject: [PATCH 3/3] Added aspire and updated to minimal api --- Brighter.sln | 34 ++- Directory.Packages.props | 16 +- .../Controllers/GreetingsController.cs | 48 --- .../Controllers/PeopleController.cs | 60 ---- .../GreetingsWeb/GreetingsWeb.csproj | 2 + samples/WebAPI_EFCore/GreetingsWeb/Program.cs | 276 +++++++++++++++--- samples/WebAPI_EFCore/GreetingsWeb/Startup.cs | 233 --------------- .../WebAPI_EFCore.AppHost/Program.cs | 10 + .../Properties/launchSettings.json | 16 + .../WebAPI_EFCore.AppHost.csproj | 20 ++ .../appsettings.Development.json | 8 + .../WebAPI_EFCore.AppHost/appsettings.json | 9 + .../Extensions.cs | 119 ++++++++ .../WebAPI_EFCore.ServiceDefaults.csproj | 24 ++ 14 files changed, 489 insertions(+), 386 deletions(-) delete mode 100644 samples/WebAPI_EFCore/GreetingsWeb/Controllers/GreetingsController.cs delete mode 100644 samples/WebAPI_EFCore/GreetingsWeb/Controllers/PeopleController.cs delete mode 100644 samples/WebAPI_EFCore/GreetingsWeb/Startup.cs create mode 100644 samples/WebAPI_EFCore/WebAPI_EFCore.AppHost/Program.cs create mode 100644 samples/WebAPI_EFCore/WebAPI_EFCore.AppHost/Properties/launchSettings.json create mode 100644 samples/WebAPI_EFCore/WebAPI_EFCore.AppHost/WebAPI_EFCore.AppHost.csproj create mode 100644 samples/WebAPI_EFCore/WebAPI_EFCore.AppHost/appsettings.Development.json create mode 100644 samples/WebAPI_EFCore/WebAPI_EFCore.AppHost/appsettings.json create mode 100644 samples/WebAPI_EFCore/WebAPI_EFCore.ServiceDefaults/Extensions.cs create mode 100644 samples/WebAPI_EFCore/WebAPI_EFCore.ServiceDefaults/WebAPI_EFCore.ServiceDefaults.csproj diff --git a/Brighter.sln b/Brighter.sln index 9621f789d7..786253ea6a 100644 --- a/Brighter.sln +++ b/Brighter.sln @@ -1,8 +1,8 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 -VisualStudioVersion = 17.0.32112.339 -MinimumVisualStudioVersion = 10.0.40219.1 +VisualStudioVersion = 17.8.0.0 +MinimumVisualStudioVersion = 17.8.0.0 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{235DE1F1-E71B-4817-8E27-3B34FF006E4C}" ProjectSection(SolutionItems) = preProject samples\AspireSetup.md = samples\AspireSetup.md @@ -344,6 +344,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AppHost", "samples\RMQTaskQ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ServiceDefaults", "samples\RMQTaskQueue\ServiceDefaults\ServiceDefaults.csproj", "{69AF4ECC-FABB-431F-89CF-0AF972BF197F}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebAPI_EFCore.AppHost", "samples\WebAPI_EFCore\WebAPI_EFCore.AppHost\WebAPI_EFCore.AppHost.csproj", "{F457820A-6B15-4697-B2B3-E3E9FCAABFDE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebAPI_EFCore.ServiceDefaults", "samples\WebAPI_EFCore\WebAPI_EFCore.ServiceDefaults\WebAPI_EFCore.ServiceDefaults.csproj", "{0D35AAF9-54AE-43CB-8E29-D9605ECA5EDF}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1962,6 +1966,30 @@ Global {69AF4ECC-FABB-431F-89CF-0AF972BF197F}.Release|Mixed Platforms.Build.0 = Release|Any CPU {69AF4ECC-FABB-431F-89CF-0AF972BF197F}.Release|x86.ActiveCfg = Release|Any CPU {69AF4ECC-FABB-431F-89CF-0AF972BF197F}.Release|x86.Build.0 = Release|Any CPU + {F457820A-6B15-4697-B2B3-E3E9FCAABFDE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F457820A-6B15-4697-B2B3-E3E9FCAABFDE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F457820A-6B15-4697-B2B3-E3E9FCAABFDE}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {F457820A-6B15-4697-B2B3-E3E9FCAABFDE}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {F457820A-6B15-4697-B2B3-E3E9FCAABFDE}.Debug|x86.ActiveCfg = Debug|Any CPU + {F457820A-6B15-4697-B2B3-E3E9FCAABFDE}.Debug|x86.Build.0 = Debug|Any CPU + {F457820A-6B15-4697-B2B3-E3E9FCAABFDE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F457820A-6B15-4697-B2B3-E3E9FCAABFDE}.Release|Any CPU.Build.0 = Release|Any CPU + {F457820A-6B15-4697-B2B3-E3E9FCAABFDE}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {F457820A-6B15-4697-B2B3-E3E9FCAABFDE}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {F457820A-6B15-4697-B2B3-E3E9FCAABFDE}.Release|x86.ActiveCfg = Release|Any CPU + {F457820A-6B15-4697-B2B3-E3E9FCAABFDE}.Release|x86.Build.0 = Release|Any CPU + {0D35AAF9-54AE-43CB-8E29-D9605ECA5EDF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0D35AAF9-54AE-43CB-8E29-D9605ECA5EDF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0D35AAF9-54AE-43CB-8E29-D9605ECA5EDF}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {0D35AAF9-54AE-43CB-8E29-D9605ECA5EDF}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {0D35AAF9-54AE-43CB-8E29-D9605ECA5EDF}.Debug|x86.ActiveCfg = Debug|Any CPU + {0D35AAF9-54AE-43CB-8E29-D9605ECA5EDF}.Debug|x86.Build.0 = Debug|Any CPU + {0D35AAF9-54AE-43CB-8E29-D9605ECA5EDF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0D35AAF9-54AE-43CB-8E29-D9605ECA5EDF}.Release|Any CPU.Build.0 = Release|Any CPU + {0D35AAF9-54AE-43CB-8E29-D9605ECA5EDF}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {0D35AAF9-54AE-43CB-8E29-D9605ECA5EDF}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {0D35AAF9-54AE-43CB-8E29-D9605ECA5EDF}.Release|x86.ActiveCfg = Release|Any CPU + {0D35AAF9-54AE-43CB-8E29-D9605ECA5EDF}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -2077,6 +2105,8 @@ Global {AA2AA086-9B8A-4910-A793-E92B1E352351} = {329736D2-BF92-4D06-A7BF-19F4B6B64EDD} {995A09D0-C440-4E88-A0C6-7AF93D82CA82} = {E9748DC0-6E72-4634-AF49-428F806E03B0} {69AF4ECC-FABB-431F-89CF-0AF972BF197F} = {E9748DC0-6E72-4634-AF49-428F806E03B0} + {F457820A-6B15-4697-B2B3-E3E9FCAABFDE} = {C6B17EFD-4F05-4D45-AF3E-C4F3F790B994} + {0D35AAF9-54AE-43CB-8E29-D9605ECA5EDF} = {C6B17EFD-4F05-4D45-AF3E-C4F3F790B994} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {8B7C7E31-2E32-4E0D-9426-BC9AF22E9F4C} diff --git a/Directory.Packages.props b/Directory.Packages.props index 9ad1347319..cd120aa7bf 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,7 +4,6 @@ false - @@ -30,6 +29,7 @@ + @@ -41,10 +41,10 @@ - + - + @@ -60,13 +60,13 @@ - + - + - - - + + + diff --git a/samples/WebAPI_EFCore/GreetingsWeb/Controllers/GreetingsController.cs b/samples/WebAPI_EFCore/GreetingsWeb/Controllers/GreetingsController.cs deleted file mode 100644 index 10bbb3ad1e..0000000000 --- a/samples/WebAPI_EFCore/GreetingsWeb/Controllers/GreetingsController.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.Threading.Tasks; -using GreetingsPorts.Requests; -using GreetingsPorts.Responses; -using GreetingsWeb.Models; -using Microsoft.AspNetCore.Mvc; -using Paramore.Brighter; -using Paramore.Darker; - -namespace GreetingsWeb.Controllers -{ - [ApiController] - [Route("[controller]")] - public class GreetingsController : Controller - { - private readonly IAmACommandProcessor _commandProcessor; - private readonly IQueryProcessor _queryProcessor; - - public GreetingsController(IAmACommandProcessor commandProcessor, IQueryProcessor queryProcessor) - { - _commandProcessor = commandProcessor; - _queryProcessor = queryProcessor; - } - - [Route("{name}")] - [HttpGet] - public async Task Get(string name) - { - var personsGreetings = await _queryProcessor.ExecuteAsync(new FindGreetingsForPerson(name)); - - if (personsGreetings == null) return new NotFoundResult(); - - return Ok(personsGreetings); - } - - [Route("{name}/new")] - [HttpPost] - public async Task> Post(string name, NewGreeting newGreeting) - { - await _commandProcessor.SendAsync(new AddGreeting(name, newGreeting.Greeting)); - - var personsGreetings = await _queryProcessor.ExecuteAsync(new FindGreetingsForPerson(name)); - - if (personsGreetings == null) return new NotFoundResult(); - - return Ok(personsGreetings); - } - } -} diff --git a/samples/WebAPI_EFCore/GreetingsWeb/Controllers/PeopleController.cs b/samples/WebAPI_EFCore/GreetingsWeb/Controllers/PeopleController.cs deleted file mode 100644 index 0ce06da254..0000000000 --- a/samples/WebAPI_EFCore/GreetingsWeb/Controllers/PeopleController.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; -using System.Threading.Tasks; -using GreetingsPorts.Requests; -using GreetingsPorts.Responses; -using GreetingsWeb.Models; -using Microsoft.AspNetCore.Mvc; -using Paramore.Brighter; -using Paramore.Darker; - -namespace GreetingsWeb.Controllers -{ - [ApiController] - [Route("[controller]")] - public class PeopleController : Controller - { - private readonly IAmACommandProcessor _commandProcessor; - private readonly IQueryProcessor _queryProcessor; - - public PeopleController(IAmACommandProcessor commandProcessor, IQueryProcessor queryProcessor) - { - _commandProcessor = commandProcessor; - _queryProcessor = queryProcessor; - } - - - [Route("{name}")] - [HttpGet] - public async Task> Get(string name) - { - var foundPerson = await _queryProcessor.ExecuteAsync(new FindPersonByName(name)); - - if (foundPerson == null) return new NotFoundResult(); - - return Ok(foundPerson); - } - - [Route("{name}")] - [HttpDelete] - public async Task Delete(string name) - { - await _commandProcessor.SendAsync(new DeletePerson(name)); - - return Ok(); - } - - [Route("new")] - [HttpPost] - public async Task> Post(NewPerson newPerson) - { - await _commandProcessor.SendAsync(new AddPerson(newPerson.Name)); - - var addedPerson = await _queryProcessor.ExecuteAsync(new FindPersonByName(newPerson.Name)); - - if (addedPerson == null) return new NotFoundResult(); - - return Ok(addedPerson); - } - - } -} diff --git a/samples/WebAPI_EFCore/GreetingsWeb/GreetingsWeb.csproj b/samples/WebAPI_EFCore/GreetingsWeb/GreetingsWeb.csproj index bcd78a44ec..585fb21c4d 100644 --- a/samples/WebAPI_EFCore/GreetingsWeb/GreetingsWeb.csproj +++ b/samples/WebAPI_EFCore/GreetingsWeb/GreetingsWeb.csproj @@ -5,6 +5,7 @@ + @@ -25,6 +26,7 @@ true + diff --git a/samples/WebAPI_EFCore/GreetingsWeb/Program.cs b/samples/WebAPI_EFCore/GreetingsWeb/Program.cs index 5c3320abab..9b86e17d06 100644 --- a/samples/WebAPI_EFCore/GreetingsWeb/Program.cs +++ b/samples/WebAPI_EFCore/GreetingsWeb/Program.cs @@ -1,45 +1,251 @@ -using System.IO; -using GreetingsWeb; -using GreetingsWeb.Database; -using Microsoft.AspNetCore.Hosting; +using System; +using GreetingsPorts.EntityGateway; +using GreetingsPorts.Handlers; +using GreetingsPorts.Policies; +using GreetingsPorts.Requests; +using GreetingsWeb.Models; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; +using Microsoft.OpenApi.Models; +using MySqlConnector; +using Paramore.Brighter; +using Paramore.Brighter.Extensions.DependencyInjection; +using Paramore.Brighter.Extensions.Hosting; +using Paramore.Brighter.MessagingGateway.RMQ; +using Paramore.Brighter.MySql; +using Paramore.Brighter.MySql.EntityFrameworkCore; +using Paramore.Brighter.Outbox.MySql; +using Paramore.Brighter.Outbox.Sqlite; +using Paramore.Brighter.Sqlite; +using Paramore.Brighter.Sqlite.EntityFrameworkCore; +using Paramore.Darker; +using Paramore.Darker.AspNetCore; +using Paramore.Darker.Policies; +using Paramore.Darker.QueryLogging; +using Polly; -var host = CreateHostBuilder(args).Build(); +const string _outBoxTableName = "Outbox"; -host.CheckDbIsUp(); -host.MigrateDatabase(); -host.CreateOutbox(); +var builder = WebApplication.CreateBuilder(args); +builder.Services.AddProblemDetails(); -host.Run(); +// Add service defaults & Aspire components. +builder.AddServiceDefaults(); + +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(options => options.SwaggerDoc("v1", new OpenApiInfo { Title = "GreetingsAPI", Version = "v1" })); + +if (builder.Environment.IsDevelopment()) +{ + //NOTE: Sqlite needs to use a shared cache to allow Db writes to the Outbox as well as entities + builder.Services.AddDbContext( + options => + { + options.UseSqlite(DbConnectionString(), + optionsBuilder => + { + optionsBuilder.MigrationsAssembly("Greetings_SqliteMigrations"); + }) + .EnableDetailedErrors() + .EnableSensitiveDataLogging(); + }); +} +else +{ + builder.Services.AddDbContextPool(options => + { + options + .UseMySql(DbConnectionString(), ServerVersion.AutoDetect(DbConnectionString()), optionsBuilder => + { + optionsBuilder.MigrationsAssembly("Greetings_MySqlMigrations"); + }) + .EnableDetailedErrors() + .EnableSensitiveDataLogging(); + }); +} + +string DbConnectionString() +{ + if (builder.Environment.IsDevelopment()) + { + return "Filename=Greetings.db;Cache=Shared"; + } + + return builder.Configuration.GetConnectionString("Greetings"); +} + +(IAmAnOutbox outbox, Type transactionProvider, Type connectionProvider) = MakeOutbox(); +var outboxConfiguration = new RelationalDatabaseConfiguration(DbConnectionString()); + +builder.Services.AddSingleton(outboxConfiguration); + +IAmAProducerRegistry producerRegistry = ConfigureProducerRegistry(); + +builder.Services.AddBrighter(options => + { + //we want to use scoped, so make sure everything understands that which needs to + options.HandlerLifetime = ServiceLifetime.Scoped; + options.CommandProcessorLifetime = ServiceLifetime.Scoped; + options.MapperLifetime = ServiceLifetime.Singleton; + options.PolicyRegistry = new GreetingsPolicy(); + }) + .UseExternalBus((configure) => + { + configure.ProducerRegistry = producerRegistry; + configure.Outbox = outbox; + configure.TransactionProvider = transactionProvider; + configure.ConnectionProvider = connectionProvider; + } + ) + .UseOutboxSweeper(options => + { + options.TimerInterval = 5; + options.MinimumMessageAge = 5000; + }) + .UseOutboxSweeper() + .AutoFromAssemblies(); + + +builder.Services.AddDarker(options => + { + options.HandlerLifetime = ServiceLifetime.Scoped; + options.QueryProcessorLifetime = ServiceLifetime.Scoped; + }) + .AddHandlersFromAssemblies(typeof(FindPersonByNameHandlerAsync).Assembly) + .AddJsonQueryLogging() + .AddPolicies(new GreetingsPolicy()); + + +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} +else +{ + app.UseExceptionHandler("/Error", createScopeForErrors: true); +} + +app.UseHttpsRedirection(); + +//host.CheckDbIsUp(); +string connectionString = DbConnectionString(); + +var policy = Policy.Handle().WaitAndRetryForever( + retryAttempt => TimeSpan.FromSeconds(2), + (exception, timespan) => + { + Console.WriteLine($"Healthcheck: Waiting for the database {connectionString} to come online - {exception.Message}"); + }); + +policy.Execute(() => +{ + //don't check this for SQlite in development + if (!app.Environment.IsDevelopment()) + { + using var conn = new MySqlConnection(connectionString); + conn.Open(); + } +}); + + +//host.CreateOutbox(); + + +//host.MigrateDatabase(); +using (var scope = app.Services.CreateScope()) +{ + var dbContext = scope.ServiceProvider.GetRequiredService(); + dbContext.Database.Migrate(); +} + +// Greetings +app.MapGet("greetings", async (string name, IQueryProcessor queryProcessor) => +{ + var personsGreetings = await queryProcessor.ExecuteAsync(new FindGreetingsForPerson(name)); + return personsGreetings == null ? Results.NotFound() : Results.Ok(personsGreetings); +}).WithName("GetGreetings").WithOpenApi(); + +app.MapPost("greetings/new", async (string name, NewGreeting newGreeting, IAmACommandProcessor commandProcessor, IQueryProcessor queryProcessor) => +{ + await commandProcessor.SendAsync(new AddGreeting(name, newGreeting.Greeting)); + + var personsGreetings = await queryProcessor.ExecuteAsync(new FindGreetingsForPerson(name)); + + return personsGreetings == null ? Results.NotFound() : Results.Ok(personsGreetings); +}); + +// People +app.MapGet("people", async (string name, IQueryProcessor queryProcessor) => +{ + var foundPerson = await queryProcessor.ExecuteAsync(new FindPersonByName(name)); + + return foundPerson == null ? Results.NotFound() : Results.Ok(foundPerson); +}); + +app.MapDelete("people", async (string name, IAmACommandProcessor commandProcessor) => +{ + await commandProcessor.SendAsync(new DeletePerson(name)); + + return Results.Ok(); +}); + +app.MapPost("people/new", async (NewPerson newPerson, IAmACommandProcessor commandProcessor, IQueryProcessor queryProcessor) => +{ + await commandProcessor.SendAsync(new AddPerson(newPerson.Name)); + + var addedPerson = await queryProcessor.ExecuteAsync(new FindPersonByName(newPerson.Name)); + + return addedPerson == null ? Results.NotFound() : Results.Ok(addedPerson); +}); + +app.Run(); return; -static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureAppConfiguration((context, configBuilder) => + +(IAmAnOutbox outbox, Type transactionProvider, Type connectionProvider) MakeOutbox() +{ + if (builder.Environment.IsDevelopment()) + { + var outbox = new SqliteOutbox(new RelationalDatabaseConfiguration(DbConnectionString(), _outBoxTableName)); + var transactionProvider = typeof(SqliteEntityFrameworkConnectionProvider); + var connectionProvider = typeof(SqliteConnectionProvider); + return (outbox, transactionProvider, connectionProvider); + } + else + { + var outbox = new MySqlOutbox(new RelationalDatabaseConfiguration(DbConnectionString(), _outBoxTableName)); + var transactionProvider = typeof(MySqlEntityFrameworkConnectionProvider); + var connectionProvider = typeof(MySqlConnectionProvider); + return (outbox, transactionProvider, connectionProvider); + } +} + +static IAmAProducerRegistry ConfigureProducerRegistry() +{ + var producerRegistry = new RmqProducerRegistryFactory( + new RmqMessagingGatewayConnection { - var env = context.HostingEnvironment; - configBuilder.AddJsonFile("appsettings.json", optional: false); - configBuilder.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true); - configBuilder.AddEnvironmentVariables(prefix: "BRIGHTER_"); - }) - .ConfigureWebHostDefaults(webBuilder => + AmpqUri = new AmqpUriSpecification(new Uri("amqp://guest:guest@localhost:5672")), + Exchange = new Exchange("paramore.brighter.exchange"), + }, + new RmqPublication[] { - webBuilder.UseKestrel(); - webBuilder.UseContentRoot(Directory.GetCurrentDirectory()); - webBuilder.CaptureStartupErrors(true); - webBuilder.UseSetting("detailedErrors", "true"); - webBuilder.ConfigureLogging((hostingContext, logging) => + new RmqPublication { - logging.AddConsole(); - logging.AddDebug(); - }); - webBuilder.UseDefaultServiceProvider((context, options) => - { - var isDevelopment = context.HostingEnvironment.IsDevelopment(); - options.ValidateScopes = isDevelopment; - options.ValidateOnBuild = isDevelopment; - }); - webBuilder.UseStartup(); - }); + Topic = new RoutingKey("GreetingMade"), + MaxOutStandingMessages = 5, + MaxOutStandingCheckIntervalMilliSeconds = 500, + WaitForConfirmsTimeOutInMilliseconds = 1000, + MakeChannels = OnMissingChannel.Create + } + } + ).Create(); + return producerRegistry; +} diff --git a/samples/WebAPI_EFCore/GreetingsWeb/Startup.cs b/samples/WebAPI_EFCore/GreetingsWeb/Startup.cs deleted file mode 100644 index 60376552e2..0000000000 --- a/samples/WebAPI_EFCore/GreetingsWeb/Startup.cs +++ /dev/null @@ -1,233 +0,0 @@ -using System; -using GreetingsPorts.EntityGateway; -using GreetingsPorts.Handlers; -using GreetingsPorts.Policies; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.OpenApi.Models; -using MySqlConnector; -using Paramore.Brighter; -using Paramore.Brighter.Extensions.DependencyInjection; -using Paramore.Brighter.Extensions.Hosting; -using Paramore.Brighter.MessagingGateway.RMQ; -using Paramore.Brighter.MySql; -using Paramore.Brighter.MySql.EntityFrameworkCore; -using Paramore.Brighter.Outbox.MySql; -using Paramore.Brighter.Outbox.Sqlite; -using Paramore.Brighter.Sqlite; -using Paramore.Brighter.Sqlite.EntityFrameworkCore; -using Paramore.Darker.AspNetCore; -using Paramore.Darker.Policies; -using Paramore.Darker.QueryLogging; -using Polly; - -namespace GreetingsWeb -{ - public class Startup - { - private const string _outBoxTableName = "Outbox"; - private IWebHostEnvironment _env; - - public Startup(IConfiguration configuration, IWebHostEnvironment env) - { - Configuration = configuration; - _env = env; - } - - public IConfiguration Configuration { get; } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - if (env.IsDevelopment()) - { - app.UseSwagger(); - app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "GreetingsAPI v1")); - } - - app.UseHttpsRedirection(); - app.UseRouting(); - - app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); - } - - - // This method gets called by the runtime. Use this method to add services to the container. - public void ConfigureServices(IServiceCollection services) - { - services.AddMvcCore().AddApiExplorer(); - services.AddControllers(options => - { - options.RespectBrowserAcceptHeader = true; - }) - .AddXmlSerializerFormatters(); - services.AddProblemDetails(); - services.AddSwaggerGen(c => - { - c.SwaggerDoc("v1", new OpenApiInfo { Title = "GreetingsAPI", Version = "v1" }); - }); - - ConfigureEFCore(services); - ConfigureBrighter(services); - ConfigureDarker(services); - } - - private void CheckDbIsUp() - { - string connectionString = DbConnectionString(); - - var policy = Policy.Handle().WaitAndRetryForever( - retryAttempt => TimeSpan.FromSeconds(2), - (exception, timespan) => - { - Console.WriteLine( - $"Healthcheck: Waiting for the database {connectionString} to come online - {exception.Message}"); - }); - - policy.Execute(() => - { - //don't check this for SQlite in development - if (!_env.IsDevelopment()) - { - using (var conn = new MySqlConnection(connectionString)) - { - conn.Open(); - } - } - }); - } - - private void ConfigureBrighter(IServiceCollection services) - { - (IAmAnOutbox outbox, Type transactionProvider, Type connectionProvider) = MakeOutbox(); - var outboxConfiguration = new RelationalDatabaseConfiguration(DbConnectionString()); - services.AddSingleton(outboxConfiguration); - - IAmAProducerRegistry producerRegistry = ConfigureProducerRegistry(); - - services.AddBrighter(options => - { - //we want to use scoped, so make sure everything understands that which needs to - options.HandlerLifetime = ServiceLifetime.Scoped; - options.CommandProcessorLifetime = ServiceLifetime.Scoped; - options.MapperLifetime = ServiceLifetime.Singleton; - options.PolicyRegistry = new GreetingsPolicy(); - }) - .UseExternalBus((configure) => - { - configure.ProducerRegistry = producerRegistry; - configure.Outbox = outbox; - configure.TransactionProvider = transactionProvider; - configure.ConnectionProvider = connectionProvider; - } - ) - .UseOutboxSweeper(options => - { - options.TimerInterval = 5; - options.MinimumMessageAge = 5000; - }) - .UseOutboxSweeper() - .AutoFromAssemblies(); - } - - private void ConfigureDarker(IServiceCollection services) - { - services.AddDarker(options => - { - options.HandlerLifetime = ServiceLifetime.Scoped; - options.QueryProcessorLifetime = ServiceLifetime.Scoped; - }) - .AddHandlersFromAssemblies(typeof(FindPersonByNameHandlerAsync).Assembly) - .AddJsonQueryLogging() - .AddPolicies(new GreetingsPolicy()); - } - - private void ConfigureEFCore(IServiceCollection services) - { - string connectionString = DbConnectionString(); - - if (_env.IsDevelopment()) - { - services.AddDbContext( - builder => - { - builder.UseSqlite(connectionString, - optionsBuilder => - { - optionsBuilder.MigrationsAssembly("Greetings_SqliteMigrations"); - }) - .EnableDetailedErrors() - .EnableSensitiveDataLogging(); - }); - } - else - { - services.AddDbContextPool(builder => - { - builder - .UseMySql(connectionString, ServerVersion.AutoDetect(connectionString), optionsBuilder => - { - optionsBuilder.MigrationsAssembly("Greetings_MySqlMigrations"); - }) - .EnableDetailedErrors() - .EnableSensitiveDataLogging(); - }); - } - } - - private static IAmAProducerRegistry ConfigureProducerRegistry() - { - var producerRegistry = new RmqProducerRegistryFactory( - new RmqMessagingGatewayConnection - { - AmpqUri = new AmqpUriSpecification(new Uri("amqp://guest:guest@localhost:5672")), - Exchange = new Exchange("paramore.brighter.exchange"), - }, - new RmqPublication[] - { - new RmqPublication - { - Topic = new RoutingKey("GreetingMade"), - MaxOutStandingMessages = 5, - MaxOutStandingCheckIntervalMilliSeconds = 500, - WaitForConfirmsTimeOutInMilliseconds = 1000, - MakeChannels = OnMissingChannel.Create - } - } - ).Create(); - return producerRegistry; - } - - private string DbConnectionString() - { - //NOTE: Sqlite needs to use a shared cache to allow Db writes to the Outbox as well as entities - return _env.IsDevelopment() - ? "Filename=Greetings.db;Cache=Shared" - : Configuration.GetConnectionString("Greetings"); - } - - private (IAmAnOutbox outbox, Type transactionProvider, Type connectionProvider) MakeOutbox() - { - if (_env.IsDevelopment()) - { - var outbox = new SqliteOutbox( - new RelationalDatabaseConfiguration(DbConnectionString(), _outBoxTableName)); - var transactionProvider = typeof(SqliteEntityFrameworkConnectionProvider); - var connectionProvider = typeof(SqliteConnectionProvider); - return (outbox, transactionProvider, connectionProvider); - } - else - { - var outbox = new MySqlOutbox( - new RelationalDatabaseConfiguration(DbConnectionString(), _outBoxTableName)); - var transactionProvider = typeof(MySqlEntityFrameworkConnectionProvider); - var connectionProvider = typeof(MySqlConnectionProvider); - return (outbox, transactionProvider, connectionProvider); - } - } - } -} diff --git a/samples/WebAPI_EFCore/WebAPI_EFCore.AppHost/Program.cs b/samples/WebAPI_EFCore/WebAPI_EFCore.AppHost/Program.cs new file mode 100644 index 0000000000..9ed54abcb8 --- /dev/null +++ b/samples/WebAPI_EFCore/WebAPI_EFCore.AppHost/Program.cs @@ -0,0 +1,10 @@ +var builder = DistributedApplication.CreateBuilder(args); + +var rabbit = builder.AddRabbitMQ("messaging"); + +var greetingsWeb = builder.AddProject("greetings_web") + .WithReference(rabbit); +var salutation_analytics = builder.AddProject("salutation_analytics") + .WithReference(rabbit); + +builder.Build().Run(); diff --git a/samples/WebAPI_EFCore/WebAPI_EFCore.AppHost/Properties/launchSettings.json b/samples/WebAPI_EFCore/WebAPI_EFCore.AppHost/Properties/launchSettings.json new file mode 100644 index 0000000000..2713ccd572 --- /dev/null +++ b/samples/WebAPI_EFCore/WebAPI_EFCore.AppHost/Properties/launchSettings.json @@ -0,0 +1,16 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:15080", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:16016" + } + } + } +} diff --git a/samples/WebAPI_EFCore/WebAPI_EFCore.AppHost/WebAPI_EFCore.AppHost.csproj b/samples/WebAPI_EFCore/WebAPI_EFCore.AppHost/WebAPI_EFCore.AppHost.csproj new file mode 100644 index 0000000000..440b4cd3df --- /dev/null +++ b/samples/WebAPI_EFCore/WebAPI_EFCore.AppHost/WebAPI_EFCore.AppHost.csproj @@ -0,0 +1,20 @@ + + + + Exe + net8.0 + enable + enable + true + + + + + + + + + + + + diff --git a/samples/WebAPI_EFCore/WebAPI_EFCore.AppHost/appsettings.Development.json b/samples/WebAPI_EFCore/WebAPI_EFCore.AppHost/appsettings.Development.json new file mode 100644 index 0000000000..0c208ae918 --- /dev/null +++ b/samples/WebAPI_EFCore/WebAPI_EFCore.AppHost/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/samples/WebAPI_EFCore/WebAPI_EFCore.AppHost/appsettings.json b/samples/WebAPI_EFCore/WebAPI_EFCore.AppHost/appsettings.json new file mode 100644 index 0000000000..31c092aa45 --- /dev/null +++ b/samples/WebAPI_EFCore/WebAPI_EFCore.AppHost/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" + } + } +} diff --git a/samples/WebAPI_EFCore/WebAPI_EFCore.ServiceDefaults/Extensions.cs b/samples/WebAPI_EFCore/WebAPI_EFCore.ServiceDefaults/Extensions.cs new file mode 100644 index 0000000000..4921efee9a --- /dev/null +++ b/samples/WebAPI_EFCore/WebAPI_EFCore.ServiceDefaults/Extensions.cs @@ -0,0 +1,119 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Logging; +using OpenTelemetry.Logs; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; + +namespace Microsoft.Extensions.Hosting; + +public static class Extensions +{ + 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.UseServiceDiscovery(); + }); + + 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.AddRuntimeInstrumentation() + .AddBuiltInMeters(); + }) + .WithTracing(tracing => + { + if (builder.Environment.IsDevelopment()) + { + // We want to view all traces in development + tracing.SetSampler(new AlwaysOnSampler()); + } + + tracing.AddAspNetCoreInstrumentation() + .AddGrpcClientInstrumentation() + .AddHttpClientInstrumentation(); + }); + + 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.Configure(logging => logging.AddOtlpExporter()); + builder.Services.ConfigureOpenTelemetryMeterProvider(metrics => metrics.AddOtlpExporter()); + builder.Services.ConfigureOpenTelemetryTracerProvider(tracing => tracing.AddOtlpExporter()); + } + + // Uncomment the following lines to enable the Prometheus exporter (requires the OpenTelemetry.Exporter.Prometheus.AspNetCore package) + // builder.Services.AddOpenTelemetry() + // .WithMetrics(metrics => metrics.AddPrometheusExporter()); + + // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package) + // 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) + { + // Uncomment the following line to enable the Prometheus endpoint (requires the OpenTelemetry.Exporter.Prometheus.AspNetCore package) + // app.MapPrometheusScrapingEndpoint(); + + // 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") + }); + + return app; + } + + private static MeterProviderBuilder AddBuiltInMeters(this MeterProviderBuilder meterProviderBuilder) => + meterProviderBuilder.AddMeter( + "Microsoft.AspNetCore.Hosting", + "Microsoft.AspNetCore.Server.Kestrel", + "System.Net.Http"); +} diff --git a/samples/WebAPI_EFCore/WebAPI_EFCore.ServiceDefaults/WebAPI_EFCore.ServiceDefaults.csproj b/samples/WebAPI_EFCore/WebAPI_EFCore.ServiceDefaults/WebAPI_EFCore.ServiceDefaults.csproj new file mode 100644 index 0000000000..30d5c1fb7c --- /dev/null +++ b/samples/WebAPI_EFCore/WebAPI_EFCore.ServiceDefaults/WebAPI_EFCore.ServiceDefaults.csproj @@ -0,0 +1,24 @@ + + + + Library + net8.0 + enable + enable + true + + + + + + + + + + + + + + + +