From 9af6d6342d4bd08f3ca8dbd34f7733a0613b7045 Mon Sep 17 00:00:00 2001 From: David Fowler Date: Tue, 2 May 2023 08:00:17 -0700 Subject: [PATCH] Initial attempt at making a common service configuration --- .../Basket/Basket.API/Basket.API.csproj | 7 +- .../Basket.API/CustomExtensionMethods.cs | 129 +----- src/Services/Basket/Basket.API/Program.cs | 162 ++----- .../Basket/Basket.API/appsettings.json | 9 +- .../AuthorizeCheckOperationFilter.cs | 38 ++ .../Services.Common/CommonExtensions.cs | 398 ++++++++++++++++++ .../Services.Common/Services.Common.csproj | 64 +++ src/eShopOnContainers-ServicesAndWebApps.sln | 54 +++ 8 files changed, 595 insertions(+), 266 deletions(-) create mode 100644 src/Services/Services.Common/AuthorizeCheckOperationFilter.cs create mode 100644 src/Services/Services.Common/CommonExtensions.cs create mode 100644 src/Services/Services.Common/Services.Common.csproj diff --git a/src/Services/Basket/Basket.API/Basket.API.csproj b/src/Services/Basket/Basket.API/Basket.API.csproj index f6f36ba88..a6512c614 100644 --- a/src/Services/Basket/Basket.API/Basket.API.csproj +++ b/src/Services/Basket/Basket.API/Basket.API.csproj @@ -46,8 +46,9 @@ - - - + + + + diff --git a/src/Services/Basket/Basket.API/CustomExtensionMethods.cs b/src/Services/Basket/Basket.API/CustomExtensionMethods.cs index cdec6a100..2dccd89f6 100644 --- a/src/Services/Basket/Basket.API/CustomExtensionMethods.cs +++ b/src/Services/Basket/Basket.API/CustomExtensionMethods.cs @@ -2,23 +2,12 @@ public static class CustomExtensionMethods { - public static ConfigurationManager AddKeyVault(this ConfigurationManager configuration) - { - if (configuration.GetValue("UseVault", false)) - { - var credential = new ClientSecretCredential( - configuration["Vault:TenantId"], - configuration["Vault:ClientId"], - configuration["Vault:ClientSecret"]); - - configuration.AddAzureKeyVault(new Uri($"https://{configuration["Vault:Name"]}.vault.azure.net/"), credential); - } - - return configuration; - } - public static IServiceCollection AddRedis(this IServiceCollection services) { + // { + // "ConnectionString": "..." + // } + return services.AddSingleton(sp => { var settings = sp.GetRequiredService>().Value; @@ -27,114 +16,4 @@ public static class CustomExtensionMethods return ConnectionMultiplexer.Connect(configuration); }); } - - public static IServiceCollection AddCustomHealthCheck(this IServiceCollection services, IConfiguration configuration) - { - var hcBuilder = services.AddHealthChecks(); - - hcBuilder.AddCheck("self", () => HealthCheckResult.Healthy()); - - hcBuilder - .AddRedis( - configuration["ConnectionString"], - name: "redis-check", - tags: new string[] { "redis" }); - - if (configuration.GetValue("AzureServiceBusEnabled")) - { - hcBuilder - .AddAzureServiceBusTopic( - configuration["EventBusConnection"], - topicName: "eshop_event_bus", - name: "basket-servicebus-check", - tags: new string[] { "servicebus" }); - } - else - { - hcBuilder - .AddRabbitMQ( - $"amqp://{configuration["EventBusConnection"]}", - name: "basket-rabbitmqbus-check", - tags: new string[] { "rabbitmqbus" }); - } - - return services; - } - - public static IServiceCollection AddEventBus(this IServiceCollection services, IConfiguration configuration) - { - if (configuration.GetValue("AzureServiceBusEnabled", false)) - { - services.AddSingleton(sp => - { - var serviceBusConnectionString = configuration["EventBusConnection"]; - - return new DefaultServiceBusPersisterConnection(serviceBusConnectionString); - }); - - services.AddSingleton(sp => - { - var serviceBusPersisterConnection = sp.GetRequiredService(); - var logger = sp.GetRequiredService>(); - var eventBusSubscriptionsManager = sp.GetRequiredService(); - string subscriptionName = configuration["SubscriptionClientName"]; - - return new EventBusServiceBus(serviceBusPersisterConnection, logger, - eventBusSubscriptionsManager, sp, subscriptionName); - }); - } - else - { - services.AddSingleton(sp => - { - var logger = sp.GetRequiredService>(); - - var factory = new ConnectionFactory() - { - HostName = configuration["EventBusConnection"], - DispatchConsumersAsync = true - }; - - if (!string.IsNullOrEmpty(configuration["EventBusUserName"])) - { - factory.UserName = configuration["EventBusUserName"]; - } - - if (!string.IsNullOrEmpty(configuration["EventBusPassword"])) - { - factory.Password = configuration["EventBusPassword"]; - } - - var retryCount = 5; - if (!string.IsNullOrEmpty(configuration["EventBusRetryCount"])) - { - retryCount = int.Parse(configuration["EventBusRetryCount"]); - } - - return new DefaultRabbitMQPersistentConnection(factory, logger, retryCount); - }); - - services.AddSingleton(sp => - { - var subscriptionClientName = configuration["SubscriptionClientName"]; - var rabbitMQPersistentConnection = sp.GetRequiredService(); - var logger = sp.GetRequiredService>(); - var eventBusSubscriptionsManager = sp.GetRequiredService(); - - var retryCount = 5; - if (!string.IsNullOrEmpty(configuration["EventBusRetryCount"])) - { - retryCount = int.Parse(configuration["EventBusRetryCount"]); - } - - return new EventBusRabbitMQ(rabbitMQPersistentConnection, logger, sp, eventBusSubscriptionsManager, subscriptionClientName, retryCount); - }); - } - - services.AddSingleton(); - - services.AddTransient(); - services.AddTransient(); - return services; - } } diff --git a/src/Services/Basket/Basket.API/Program.cs b/src/Services/Basket/Basket.API/Program.cs index cb22a464f..0844d32b3 100644 --- a/src/Services/Basket/Basket.API/Program.cs +++ b/src/Services/Basket/Basket.API/Program.cs @@ -1,104 +1,52 @@ -var builder = WebApplication.CreateBuilder(args); +using Services.Common; + +var builder = WebApplication.CreateBuilder(args); builder.Configuration.AddKeyVault(); +builder.Services.AddApplicationInsights(builder.Configuration); + builder.Services.AddGrpc(options => { options.EnableDetailedErrors = true; }); -builder.Services.AddApplicationInsightsTelemetry(builder.Configuration); -builder.Services.AddApplicationInsightsKubernetesEnricher(); - builder.Services.AddControllers(options => { options.Filters.Add(typeof(HttpGlobalExceptionFilter)); options.Filters.Add(typeof(ValidateModelStateFilter)); }); -builder.Services.AddSwaggerGen(options => -{ - options.SwaggerDoc("v1", new OpenApiInfo - { - Title = "eShopOnContainers - Basket HTTP API", - Version = "v1", - Description = "The Basket Service HTTP API" - }); - - options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme - { - Type = SecuritySchemeType.OAuth2, - Flows = new OpenApiOAuthFlows() - { - Implicit = new OpenApiOAuthFlow() - { - AuthorizationUrl = new Uri($"{builder.Configuration["IdentityUrlExternal"]}/connect/authorize"), - TokenUrl = new Uri($"{builder.Configuration["IdentityUrlExternal"]}/connect/token"), - Scopes = new Dictionary() { { "basket", "Basket API" } } - } - } - }); - - options.OperationFilter(); -}); +builder.Services.AddDefaultOpenApi(builder.Configuration); -// prevent from mapping "sub" claim to nameidentifier. -JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("sub"); +builder.Services.AddDefaultAuthentication(builder.Configuration); -var identityUrl = builder.Configuration["IdentityUrl"]; +builder.Services.AddDefaultHealthChecks(builder.Configuration); -builder.Services.AddAuthentication().AddJwtBearer(options => -{ - options.Authority = identityUrl; - options.RequireHttpsMetadata = false; - options.Audience = "basket"; - options.TokenValidationParameters.ValidateAudience = false; -}); +builder.Host.UseDefaultSerilog(builder.Configuration, AppName); + +builder.WebHost.UseDefaultPorts(builder.Configuration); -builder.Services.AddAuthorization(options => +builder.WebHost.UseFailing(options => { - options.AddPolicy("ApiScope", policy => - { - policy.RequireAuthenticatedUser(); - policy.RequireClaim("scope", "basket"); - }); + options.ConfigPath = "/Failing"; + options.NotFilteredPaths.AddRange(new[] { "/hc", "/liveness" }); }); -builder.Services.AddCustomHealthCheck(builder.Configuration); +builder.Services.AddEventBus(builder.Configuration); builder.Services.Configure(builder.Configuration); builder.Services.AddRedis(); -builder.Services.AddEventBus(builder.Configuration); - -builder.Services.AddHttpContextAccessor(); +builder.Services.AddTransient(); +builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); -builder.WebHost.UseKestrel(options => -{ - var ports = GetDefinedPorts(builder.Configuration); - options.Listen(IPAddress.Any, ports.httpPort, listenOptions => - { - listenOptions.Protocols = HttpProtocols.Http1AndHttp2; - }); - - options.Listen(IPAddress.Any, ports.grpcPort, listenOptions => - { - listenOptions.Protocols = HttpProtocols.Http2; - }); -}); - -builder.Host.UseSerilog(CreateSerilogLogger(builder.Configuration)); -builder.WebHost.UseFailing(options => -{ - options.ConfigPath = "/Failing"; - options.NotFilteredPaths.AddRange(new[] { "/hc", "/liveness" }); -}); - var app = builder.Build(); + app.MapGet("hello", () => "hello"); if (!app.Environment.IsDevelopment()) @@ -112,59 +60,31 @@ if (!string.IsNullOrEmpty(pathBase)) app.UsePathBase(pathBase); } -app.UseSwagger(); - -app.UseSwaggerUI(setup => -{ - setup.SwaggerEndpoint($"{(!string.IsNullOrEmpty(pathBase) ? pathBase : string.Empty)}/swagger/v1/swagger.json", "Basket.API V1"); - setup.OAuthClientId("basketswaggerui"); - setup.OAuthAppName("Basket Swagger UI"); -}); +app.UseDefaultOpenApi(builder.Configuration); app.MapGrpcService(); app.MapControllers(); -app.MapGet("/_proto/", async ctx => -{ - ctx.Response.ContentType = "text/plain"; - using var fs = new FileStream(Path.Combine(app.Environment.ContentRootPath, "Proto", "basket.proto"), FileMode.Open, FileAccess.Read); - using var sr = new StreamReader(fs); - while (!sr.EndOfStream) - { - var line = await sr.ReadLineAsync(); - if (line != "/* >>" || line != "<< */") - { - await ctx.Response.WriteAsync(line); - } - } -}); +app.MapDefaultHealthChecks(); -app.MapHealthChecks("/hc", new HealthCheckOptions() -{ - Predicate = _ => true, - ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse -}); - -app.MapHealthChecks("/liveness", new HealthCheckOptions -{ - Predicate = r => r.Name.Contains("self") -}); +var eventBus = app.Services.GetRequiredService(); -ConfigureEventBus(app); +eventBus.Subscribe(); +eventBus.Subscribe(); try { - Log.Information("Configuring web host ({ApplicationContext})...", Program.AppName); + Log.Information("Configuring web host ({ApplicationContext})...", AppName); - Log.Information("Starting web host ({ApplicationContext})...", Program.AppName); + Log.Information("Starting web host ({ApplicationContext})...", AppName); await app.RunAsync(); return 0; } catch (Exception ex) { - Log.Fatal(ex, "Program terminated unexpectedly ({ApplicationContext})!", Program.AppName); + Log.Fatal(ex, "Program terminated unexpectedly ({ApplicationContext})!", AppName); return 1; } finally @@ -172,36 +92,6 @@ finally Log.CloseAndFlush(); } -Serilog.ILogger CreateSerilogLogger(IConfiguration configuration) -{ - var seqServerUrl = configuration["Serilog:SeqServerUrl"]; - var logstashUrl = configuration["Serilog:LogstashgUrl"]; - return new LoggerConfiguration() - .MinimumLevel.Verbose() - .Enrich.WithProperty("ApplicationContext", Program.AppName) - .Enrich.FromLogContext() - .WriteTo.Console() - .WriteTo.Seq(string.IsNullOrWhiteSpace(seqServerUrl) ? "http://seq" : seqServerUrl) - .WriteTo.Http(string.IsNullOrWhiteSpace(logstashUrl) ? "http://logstash:8080" : logstashUrl, null) - .ReadFrom.Configuration(configuration) - .CreateLogger(); -} - -(int httpPort, int grpcPort) GetDefinedPorts(IConfiguration config) -{ - var grpcPort = config.GetValue("GRPC_PORT", 5001); - var port = config.GetValue("PORT", 80); - return (port, grpcPort); -} - -void ConfigureEventBus(IApplicationBuilder app) -{ - var eventBus = app.ApplicationServices.GetRequiredService(); - - eventBus.Subscribe(); - eventBus.Subscribe(); -} - public partial class Program { private static string Namespace = typeof(Program).Assembly.GetName().Name; diff --git a/src/Services/Basket/Basket.API/appsettings.json b/src/Services/Basket/Basket.API/appsettings.json index 295294308..928ea904f 100644 --- a/src/Services/Basket/Basket.API/appsettings.json +++ b/src/Services/Basket/Basket.API/appsettings.json @@ -16,11 +16,16 @@ "Protocols": "Http2" } }, - "SubscriptionClientName": "Basket", + "EventBus": { + "SubscriptionClientName": "Basket", + "ConnectionString": "your-event-bus-connection-string", + "UserName": "your-event-bus-username", + "Password": "your-event-bus-password", + "RetryCount": 5 + }, "ApplicationInsights": { "InstrumentationKey": "" }, - "EventBusRetryCount": 5, "UseVault": false, "Vault": { "Name": "eshop", diff --git a/src/Services/Services.Common/AuthorizeCheckOperationFilter.cs b/src/Services/Services.Common/AuthorizeCheckOperationFilter.cs new file mode 100644 index 000000000..7e8d0d2ef --- /dev/null +++ b/src/Services/Services.Common/AuthorizeCheckOperationFilter.cs @@ -0,0 +1,38 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.Configuration; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace Services.Common; +internal class AuthorizeCheckOperationFilter : IOperationFilter +{ + public AuthorizeCheckOperationFilter(IConfiguration configuration) + { + + } + + public void Apply(OpenApiOperation operation, OperationFilterContext context) + { + // Check for authorize attribute + var hasAuthorize = context.MethodInfo.DeclaringType.GetCustomAttributes(true).OfType().Any() || + context.MethodInfo.GetCustomAttributes(true).OfType().Any(); + + if (!hasAuthorize) return; + + operation.Responses.TryAdd("401", new OpenApiResponse { Description = "Unauthorized" }); + operation.Responses.TryAdd("403", new OpenApiResponse { Description = "Forbidden" }); + + var oAuthScheme = new OpenApiSecurityScheme + { + Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "oauth2" } + }; + + operation.Security = new List + { + new() + { + [ oAuthScheme ] = new [] { "basketapi" } + } + }; + } +} diff --git a/src/Services/Services.Common/CommonExtensions.cs b/src/Services/Services.Common/CommonExtensions.cs new file mode 100644 index 000000000..31610ac95 --- /dev/null +++ b/src/Services/Services.Common/CommonExtensions.cs @@ -0,0 +1,398 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Net; +using Azure.Identity; +using HealthChecks.UI.Client; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.eShopOnContainers.BuildingBlocks.EventBus; +using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; +using Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ; +using Microsoft.eShopOnContainers.BuildingBlocks.EventBusServiceBus; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.OpenApi.Models; +using RabbitMQ.Client; +using Serilog; + +namespace Services.Common; + +public static class CommonExtensions +{ + public static WebApplicationBuilder AddServiceDefaults(this WebApplicationBuilder builder) + { + // Shared configuration via key vault + builder.Configuration.AddKeyVault(); + + // Shared app insights configuration + builder.Services.AddApplicationInsights(builder.Configuration); + + // Default health checks assume the event bus and self health checks + builder.Services.AddDefaultHealthChecks(builder.Configuration); + + // Configure the default logging for this application + builder.Host.UseDefaultSerilog(builder.Configuration, builder.Environment.ApplicationName); + + // Configure the default ports for this service (http and grpc ports read from configuration) + builder.WebHost.UseDefaultPorts(builder.Configuration); + + // Customizations for this application + + // Add the event bus + builder.Services.AddEventBus(builder.Configuration); + + builder.Services.AddDefaultAuthentication(builder.Configuration); + + builder.Services.AddDefaultOpenApi(builder.Configuration); + + // Add the accessor + builder.Services.AddHttpContextAccessor(); + + return builder; + } + + public static WebApplication UseServiceDefaults(this WebApplication app) + { + var pathBase = app.Configuration["PATH_BASE"]; + + if (!string.IsNullOrEmpty(pathBase)) + { + app.UsePathBase(pathBase); + } + + app.UseDefaultOpenApi(app.Configuration); + + app.MapDefaultHealthChecks(); + + return app; + } + + public static IApplicationBuilder UseDefaultOpenApi(this IApplicationBuilder app, IConfiguration configuration) + { + app.UseSwagger(); + app.UseSwaggerUI(setup => + { + var pathBase = configuration["PATH_BASE"]; + var openApiSection = configuration.GetRequiredSection("OpenApi"); + var authSection = openApiSection.GetRequiredSection("Auth"); + var endpointSection = openApiSection.GetRequiredSection("Endpoint"); + + var swaggerUrl = endpointSection["Url"] ?? $"{(!string.IsNullOrEmpty(pathBase) ? pathBase : string.Empty)}/swagger/v1/swagger.json"; + + setup.SwaggerEndpoint(swaggerUrl, endpointSection.GetRequiredValue("Name")); + setup.OAuthClientId(authSection.GetRequiredValue("ClientId")); + setup.OAuthAppName(authSection.GetRequiredValue("AppName")); + }); + + return app; + } + + public static IServiceCollection AddDefaultOpenApi(this IServiceCollection services, IConfiguration configuration) => + services.AddSwaggerGen(options => + { + var openApi = configuration.GetRequiredSection("OpenApi"); + + /// { + /// "OpenApi": { + /// "Endpoint: { + /// "Name": + /// }, + /// "Document": { + /// "Title": .. + /// "Version": .. + /// "Description": .. + /// }, + /// "Auth": { + /// "ClientId": .., + /// "AppName": .. + /// } + /// } + /// } + + var version = openApi.GetRequiredValue("Version") ?? "v1"; + + options.SwaggerDoc(version, new OpenApiInfo + { + Title = openApi.GetRequiredValue("Title"), + Version = version, + Description = openApi.GetRequiredValue("Description") + }); + + var identityUrlExternal = configuration.GetRequiredValue("IdentityUrlExternal"); + var scopes = openApi.GetRequiredSection("Scopes").AsEnumerable().ToDictionary(p => p.Key, p => p.Value); + + options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme + { + Type = SecuritySchemeType.OAuth2, + Flows = new OpenApiOAuthFlows() + { + Implicit = new OpenApiOAuthFlow() + { + AuthorizationUrl = new Uri($"{identityUrlExternal}/connect/authorize"), + TokenUrl = new Uri($"{identityUrlExternal}/connect/token"), + Scopes = openApi.GetRequiredSection("Scopes").AsEnumerable().ToDictionary(p => p.Key, p => p.Value), + } + } + }); + + options.OperationFilter(); + }); + + public static IServiceCollection AddDefaultAuthentication(this IServiceCollection services, IConfiguration configuration) + { + // prevent from mapping "sub" claim to nameidentifier. + JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("sub"); + + services.AddAuthentication().AddJwtBearer(options => + { + var identityUrl = configuration.GetRequiredValue("IdentityUrl"); + var audience = configuration.GetRequiredValue("Audience"); + + options.Authority = identityUrl; + options.RequireHttpsMetadata = false; + options.Audience = audience; + options.TokenValidationParameters.ValidateAudience = false; + }); + + services.AddAuthorization(options => + { + var scope = configuration.GetRequiredValue("Scope"); + + options.AddPolicy("ApiScope", policy => + { + policy.RequireAuthenticatedUser(); + policy.RequireClaim("scope", scope); + }); + }); + + return services; + } + + public static IWebHostBuilder UseDefaultPorts(this IWebHostBuilder builder, IConfiguration configuration) + { + builder.UseKestrel(options => + { + var (httpPort, grpcPort) = GetDefinedPorts(configuration); + + options.Listen(IPAddress.Any, httpPort, listenOptions => + { + listenOptions.Protocols = HttpProtocols.Http1AndHttp2; + }); + + options.Listen(IPAddress.Any, grpcPort, listenOptions => + { + listenOptions.Protocols = HttpProtocols.Http2; + }); + }); + + return builder; + } + + public static ConfigurationManager AddKeyVault(this ConfigurationManager configuration) + { + if (configuration.GetValue("UseVault", false)) + { + // { + // "Vault": { + // "Name": "myvault", + // "TenantId": "mytenantid", + // "ClientId": "myclientid", + // } + // } + + var vaultSection = configuration.GetRequiredSection("Vault"); + + var credential = new ClientSecretCredential( + vaultSection.GetRequiredValue("TenantId"), + vaultSection.GetRequiredValue("ClientId"), + vaultSection.GetRequiredValue("ClientSecret")); + + var name = vaultSection.GetRequiredValue("Name"); + + configuration.AddAzureKeyVault(new Uri($"https://{name}.vault.azure.net/"), credential); + } + + return configuration; + } + + public static IServiceCollection AddApplicationInsights(this IServiceCollection services, IConfiguration configuration) + { + services.AddApplicationInsightsTelemetry(configuration); + services.AddApplicationInsightsKubernetesEnricher(); + return services; + } + + public static IHealthChecksBuilder AddDefaultHealthChecks(this IServiceCollection services, IConfiguration configuration) + { + var hcBuilder = services.AddHealthChecks(); + + // Health check for the application itself + hcBuilder.AddCheck("self", () => HealthCheckResult.Healthy()); + + // { + // "EventBus": { + // "ProviderName": "ServiceBus | RabbitMQ", + // "ConnectionString": "Endpoint=sb://eshop-eventbus.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=..." + // } + // } + + var eventBusSection = configuration.GetRequiredSection("EventBus"); + var eventBusConnectionString = eventBusSection.GetRequiredValue("ConnectionString"); + + return eventBusSection.GetRequiredValue("ProviderName").ToLowerInvariant() switch + { + "servicebus" => hcBuilder.AddAzureServiceBusTopic( + eventBusConnectionString, + topicName: "eshop_event_bus", + name: "servicebus-check", + tags: new string[] { "servicebus" }), + + _ => hcBuilder.AddRabbitMQ( + $"amqp://{eventBusConnectionString}", + name: "rabbitmqbus-check", + tags: new string[] { "rabbitmqbus" }) + }; + } + + public static IServiceCollection AddEventBus(this IServiceCollection services, IConfiguration configuration) + { + // { + // "EventBus": { + // "ProviderName": "ServiceBus | RabbitMQ", + // "ConnectionString": "...", + // ... + // } + // } + + // { + // "EventBus": { + // "ProviderName": "ServiceBus", + // "ConnectionString": "Endpoint=sb://eshop-eventbus.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=..." + // "SubscriptionClientName": "eshop_event_bus" + // } + // } + + // { + // "EventBus": { + // "ProviderName": "RabbitMQ", + // "ConnectionString": "...", + // "SubscriptionClientName": "...", + // "UserName": "...", + // "Password": "...", + // "RetryCount": 1 + // } + // } + + var eventBusSection = configuration.GetRequiredSection("EventBus"); + if (string.Equals(eventBusSection["ProviderName"], "ServiceBus", StringComparison.OrdinalIgnoreCase)) + { + services.AddSingleton(sp => + { + var serviceBusConnectionString = eventBusSection.GetRequiredValue("ConnectionString"); + + return new DefaultServiceBusPersisterConnection(serviceBusConnectionString); + }); + + services.AddSingleton(sp => + { + var serviceBusPersisterConnection = sp.GetRequiredService(); + var logger = sp.GetRequiredService>(); + var eventBusSubscriptionsManager = sp.GetRequiredService(); + string subscriptionName = eventBusSection.GetRequiredValue("SubscriptionClientName"); + + return new EventBusServiceBus(serviceBusPersisterConnection, logger, + eventBusSubscriptionsManager, sp, subscriptionName); + }); + } + else + { + services.AddSingleton(sp => + { + var logger = sp.GetRequiredService>(); + + var factory = new ConnectionFactory() + { + HostName = eventBusSection.GetRequiredValue("ConnectionString"), + DispatchConsumersAsync = true + }; + + if (!string.IsNullOrEmpty(eventBusSection["UserName"])) + { + factory.UserName = eventBusSection["UserName"]; + } + + if (!string.IsNullOrEmpty(eventBusSection["Password"])) + { + factory.Password = eventBusSection["Password"]; + } + + var retryCount = eventBusSection.GetValue("RetryCount", 5); + + return new DefaultRabbitMQPersistentConnection(factory, logger, retryCount); + }); + + services.AddSingleton(sp => + { + var subscriptionClientName = eventBusSection.GetRequiredValue("SubscriptionClientName"); + var rabbitMQPersistentConnection = sp.GetRequiredService(); + var logger = sp.GetRequiredService>(); + var eventBusSubscriptionsManager = sp.GetRequiredService(); + var retryCount = eventBusSection.GetValue("RetryCount", 5); + + return new EventBusRabbitMQ(rabbitMQPersistentConnection, logger, sp, eventBusSubscriptionsManager, subscriptionClientName, retryCount); + }); + } + + services.AddSingleton(); + return services; + } + + public static void UseDefaultSerilog(this IHostBuilder builder, IConfiguration configuration, string name) + { + builder.UseSerilog(CreateSerilogLogger(configuration)); + + Serilog.ILogger CreateSerilogLogger(IConfiguration configuration) + { + var seqServerUrl = configuration["Serilog:SeqServerUrl"]; + var logstashUrl = configuration["Serilog:LogstashgUrl"]; + return new LoggerConfiguration() + .MinimumLevel.Verbose() + .Enrich.WithProperty("ApplicationContext", name) + .Enrich.FromLogContext() + .WriteTo.Console() + .WriteTo.Seq(string.IsNullOrWhiteSpace(seqServerUrl) ? "http://seq" : seqServerUrl) + .WriteTo.Http(string.IsNullOrWhiteSpace(logstashUrl) ? "http://logstash:8080" : logstashUrl, null) + .ReadFrom.Configuration(configuration) + .CreateLogger(); + } + } + + public static void MapDefaultHealthChecks(this IEndpointRouteBuilder routes) + { + routes.MapHealthChecks("/hc", new HealthCheckOptions() + { + Predicate = _ => true, + ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse + }); + + routes.MapHealthChecks("/liveness", new HealthCheckOptions + { + Predicate = r => r.Name.Contains("self") + }); + } + + static (int httpPort, int grpcPort) GetDefinedPorts(IConfiguration config) + { + var grpcPort = config.GetValue("GRPC_PORT", 5001); + var port = config.GetValue("PORT", 80); + return (port, grpcPort); + } + + private static string GetRequiredValue(this IConfiguration configuration, string name) => + configuration[name] ?? throw new InvalidOperationException($"Configuration missing value for: {(configuration is IConfigurationSection s ? s.Key + ":" + name : name)}"); +} diff --git a/src/Services/Services.Common/Services.Common.csproj b/src/Services/Services.Common/Services.Common.csproj new file mode 100644 index 000000000..0eb7de774 --- /dev/null +++ b/src/Services/Services.Common/Services.Common.csproj @@ -0,0 +1,64 @@ + + + + net7.0 + enable + enable + + + + + + + + + + + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/eShopOnContainers-ServicesAndWebApps.sln b/src/eShopOnContainers-ServicesAndWebApps.sln index 68f32a4bf..65aff4d0e 100644 --- a/src/eShopOnContainers-ServicesAndWebApps.sln +++ b/src/eShopOnContainers-ServicesAndWebApps.sln @@ -122,6 +122,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{373D8AA1 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EventBus.Tests", "BuildingBlocks\EventBus\EventBus.Tests\EventBus.Tests.csproj", "{95D735BE-2899-4495-BE3F-2600E93B4E3C}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Services.Common", "Services\Services.Common\Services.Common.csproj", "{CD430CE4-D5E0-4C96-84F5-AEC9162651B5}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Common", "Common", "{42B85D0F-2ED6-4C00-91FA-103DACC3D5E2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Ad-Hoc|Any CPU = Ad-Hoc|Any CPU @@ -1480,6 +1484,54 @@ Global {95D735BE-2899-4495-BE3F-2600E93B4E3C}.Release|x64.Build.0 = Release|Any CPU {95D735BE-2899-4495-BE3F-2600E93B4E3C}.Release|x86.ActiveCfg = Release|Any CPU {95D735BE-2899-4495-BE3F-2600E93B4E3C}.Release|x86.Build.0 = Release|Any CPU + {CD430CE4-D5E0-4C96-84F5-AEC9162651B5}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {CD430CE4-D5E0-4C96-84F5-AEC9162651B5}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {CD430CE4-D5E0-4C96-84F5-AEC9162651B5}.Ad-Hoc|ARM.ActiveCfg = Debug|Any CPU + {CD430CE4-D5E0-4C96-84F5-AEC9162651B5}.Ad-Hoc|ARM.Build.0 = Debug|Any CPU + {CD430CE4-D5E0-4C96-84F5-AEC9162651B5}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {CD430CE4-D5E0-4C96-84F5-AEC9162651B5}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {CD430CE4-D5E0-4C96-84F5-AEC9162651B5}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {CD430CE4-D5E0-4C96-84F5-AEC9162651B5}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {CD430CE4-D5E0-4C96-84F5-AEC9162651B5}.Ad-Hoc|x64.ActiveCfg = Debug|Any CPU + {CD430CE4-D5E0-4C96-84F5-AEC9162651B5}.Ad-Hoc|x64.Build.0 = Debug|Any CPU + {CD430CE4-D5E0-4C96-84F5-AEC9162651B5}.Ad-Hoc|x86.ActiveCfg = Debug|Any CPU + {CD430CE4-D5E0-4C96-84F5-AEC9162651B5}.Ad-Hoc|x86.Build.0 = Debug|Any CPU + {CD430CE4-D5E0-4C96-84F5-AEC9162651B5}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {CD430CE4-D5E0-4C96-84F5-AEC9162651B5}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {CD430CE4-D5E0-4C96-84F5-AEC9162651B5}.AppStore|ARM.ActiveCfg = Debug|Any CPU + {CD430CE4-D5E0-4C96-84F5-AEC9162651B5}.AppStore|ARM.Build.0 = Debug|Any CPU + {CD430CE4-D5E0-4C96-84F5-AEC9162651B5}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {CD430CE4-D5E0-4C96-84F5-AEC9162651B5}.AppStore|iPhone.Build.0 = Debug|Any CPU + {CD430CE4-D5E0-4C96-84F5-AEC9162651B5}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {CD430CE4-D5E0-4C96-84F5-AEC9162651B5}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {CD430CE4-D5E0-4C96-84F5-AEC9162651B5}.AppStore|x64.ActiveCfg = Debug|Any CPU + {CD430CE4-D5E0-4C96-84F5-AEC9162651B5}.AppStore|x64.Build.0 = Debug|Any CPU + {CD430CE4-D5E0-4C96-84F5-AEC9162651B5}.AppStore|x86.ActiveCfg = Debug|Any CPU + {CD430CE4-D5E0-4C96-84F5-AEC9162651B5}.AppStore|x86.Build.0 = Debug|Any CPU + {CD430CE4-D5E0-4C96-84F5-AEC9162651B5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CD430CE4-D5E0-4C96-84F5-AEC9162651B5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CD430CE4-D5E0-4C96-84F5-AEC9162651B5}.Debug|ARM.ActiveCfg = Debug|Any CPU + {CD430CE4-D5E0-4C96-84F5-AEC9162651B5}.Debug|ARM.Build.0 = Debug|Any CPU + {CD430CE4-D5E0-4C96-84F5-AEC9162651B5}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {CD430CE4-D5E0-4C96-84F5-AEC9162651B5}.Debug|iPhone.Build.0 = Debug|Any CPU + {CD430CE4-D5E0-4C96-84F5-AEC9162651B5}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {CD430CE4-D5E0-4C96-84F5-AEC9162651B5}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {CD430CE4-D5E0-4C96-84F5-AEC9162651B5}.Debug|x64.ActiveCfg = Debug|Any CPU + {CD430CE4-D5E0-4C96-84F5-AEC9162651B5}.Debug|x64.Build.0 = Debug|Any CPU + {CD430CE4-D5E0-4C96-84F5-AEC9162651B5}.Debug|x86.ActiveCfg = Debug|Any CPU + {CD430CE4-D5E0-4C96-84F5-AEC9162651B5}.Debug|x86.Build.0 = Debug|Any CPU + {CD430CE4-D5E0-4C96-84F5-AEC9162651B5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CD430CE4-D5E0-4C96-84F5-AEC9162651B5}.Release|Any CPU.Build.0 = Release|Any CPU + {CD430CE4-D5E0-4C96-84F5-AEC9162651B5}.Release|ARM.ActiveCfg = Release|Any CPU + {CD430CE4-D5E0-4C96-84F5-AEC9162651B5}.Release|ARM.Build.0 = Release|Any CPU + {CD430CE4-D5E0-4C96-84F5-AEC9162651B5}.Release|iPhone.ActiveCfg = Release|Any CPU + {CD430CE4-D5E0-4C96-84F5-AEC9162651B5}.Release|iPhone.Build.0 = Release|Any CPU + {CD430CE4-D5E0-4C96-84F5-AEC9162651B5}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {CD430CE4-D5E0-4C96-84F5-AEC9162651B5}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {CD430CE4-D5E0-4C96-84F5-AEC9162651B5}.Release|x64.ActiveCfg = Release|Any CPU + {CD430CE4-D5E0-4C96-84F5-AEC9162651B5}.Release|x64.Build.0 = Release|Any CPU + {CD430CE4-D5E0-4C96-84F5-AEC9162651B5}.Release|x86.ActiveCfg = Release|Any CPU + {CD430CE4-D5E0-4C96-84F5-AEC9162651B5}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1536,6 +1588,8 @@ Global {B62E859F-825E-4C8B-93EC-5966EACFD026} = {798BFC44-2CCD-45FA-B37A-5173B03C2B30} {373D8AA1-36BE-49EC-89F0-6CB736666285} = {807BB76E-B2BB-47A2-A57B-3D1B20FF5E7F} {95D735BE-2899-4495-BE3F-2600E93B4E3C} = {373D8AA1-36BE-49EC-89F0-6CB736666285} + {CD430CE4-D5E0-4C96-84F5-AEC9162651B5} = {42B85D0F-2ED6-4C00-91FA-103DACC3D5E2} + {42B85D0F-2ED6-4C00-91FA-103DACC3D5E2} = {91CF7717-08AB-4E65-B10E-0B426F01E2E8} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {25728519-5F0F-4973-8A64-0A81EB4EA8D9}