diff --git a/src/Services/Catalog/Catalog.API/Catalog.API.csproj b/src/Services/Catalog/Catalog.API/Catalog.API.csproj index 7d778716b..63ae7ce25 100644 --- a/src/Services/Catalog/Catalog.API/Catalog.API.csproj +++ b/src/Services/Catalog/Catalog.API/Catalog.API.csproj @@ -79,6 +79,7 @@ + diff --git a/src/Services/Catalog/Catalog.API/CustomExtensionMethods.cs b/src/Services/Catalog/Catalog.API/CustomExtensionMethods.cs new file mode 100644 index 000000000..7b3633a12 --- /dev/null +++ b/src/Services/Catalog/Catalog.API/CustomExtensionMethods.cs @@ -0,0 +1,89 @@ +public static class CustomExtensionMethods +{ + public static IServiceCollection AddCustomHealthCheck(this IServiceCollection services, IConfiguration configuration) + { + var hcBuilder = services.AddHealthChecks(); + + hcBuilder + .AddSqlServer( + configuration.GetConnectionString("Application"), + name: "CatalogDB-check", + tags: new string[] { "catalogdb" }); + + var accountName = configuration["AzureStorageAccountName"]; + var accountKey = configuration["AzureStorageAccountKey"]; + + if (!string.IsNullOrEmpty(accountName) && !string.IsNullOrEmpty(accountKey)) + { + hcBuilder + .AddAzureBlobStorage( + $"DefaultEndpointsProtocol=https;AccountName={accountName};AccountKey={accountKey};EndpointSuffix=core.windows.net", + name: "catalog-storage-check", + tags: new string[] { "catalogstorage" }); + } + + return services; + } + + public static IServiceCollection AddCustomDbContext(this IServiceCollection services, IConfiguration configuration) + { + services.AddEntityFrameworkSqlServer() + .AddDbContext(options => + { + options.UseSqlServer(configuration["ConnectionString"], + sqlServerOptionsAction: sqlOptions => + { + sqlOptions.MigrationsAssembly(typeof(Program).GetTypeInfo().Assembly.GetName().Name); + //Configuring Connection Resiliency: https://docs.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency + sqlOptions.EnableRetryOnFailure(maxRetryCount: 15, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null); + }); + }); + + services.AddDbContext(options => + { + options.UseSqlServer(configuration["ConnectionString"], + sqlServerOptionsAction: sqlOptions => + { + sqlOptions.MigrationsAssembly(typeof(Program).GetTypeInfo().Assembly.GetName().Name); + //Configuring Connection Resiliency: https://docs.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency + sqlOptions.EnableRetryOnFailure(maxRetryCount: 15, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null); + }); + }); + + return services; + } + + public static IServiceCollection AddCustomOptions(this IServiceCollection services, IConfiguration configuration) + { + services.Configure(configuration); + services.Configure(options => + { + options.InvalidModelStateResponseFactory = context => + { + var problemDetails = new ValidationProblemDetails(context.ModelState) + { + Instance = context.HttpContext.Request.Path, + Status = StatusCodes.Status400BadRequest, + Detail = "Please refer to the errors property for additional details." + }; + + return new BadRequestObjectResult(problemDetails) + { + ContentTypes = { "application/problem+json", "application/problem+xml" } + }; + }; + }); + + return services; + } + + public static IServiceCollection AddIntegrationServices(this IServiceCollection services) + { + services.AddTransient>( + sp => (DbConnection c) => new IntegrationEventLogService(c)); + + services.AddTransient(); + + return services; + } +} diff --git a/src/Services/Catalog/Catalog.API/Program.cs b/src/Services/Catalog/Catalog.API/Program.cs index 9c919dbc1..f8fd8b4ab 100644 --- a/src/Services/Catalog/Catalog.API/Program.cs +++ b/src/Services/Catalog/Catalog.API/Program.cs @@ -1,117 +1,56 @@ -var builder = WebApplication.CreateBuilder(new WebApplicationOptions -{ - Args = args, - ApplicationName = typeof(Program).Assembly.FullName, - ContentRootPath = Directory.GetCurrentDirectory(), - WebRootPath = "Pics", -}); -if (builder.Configuration.GetValue("UseVault", false)) -{ - TokenCredential credential = new ClientSecretCredential( - builder.Configuration["Vault:TenantId"], - builder.Configuration["Vault:ClientId"], - builder.Configuration["Vault:ClientSecret"]); - //builder.AddAzureKeyVault(new Uri($"https://{builder.Configuration["Vault:Name"]}.vault.azure.net/"), credential); -} -builder.Configuration.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true); -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; - }); +using Services.Common; -}); -builder.Services.AddAppInsight(builder.Configuration); -builder.Services.AddGrpc().Services - .AddCustomMVC(builder.Configuration) - .AddCustomDbContext(builder.Configuration) - .AddCustomOptions(builder.Configuration) - .AddCustomHealthCheck(builder.Configuration) - .AddIntegrationServices(builder.Configuration) - .AddEventBus(builder.Configuration) - .AddSwagger(builder.Configuration); +var builder = WebApplication.CreateBuilder(args); -var app = builder.Build(); +builder.AddServiceDefaults(); -if (app.Environment.IsDevelopment()) +builder.Services.AddControllers(options => { - app.UseDeveloperExceptionPage(); -} -else -{ - app.UseExceptionHandler("/Home/Error"); -} + options.Filters.Add(typeof(HttpGlobalExceptionFilter)); +}) +.AddJsonOptions(options => options.JsonSerializerOptions.WriteIndented = true); -var pathBase = app.Configuration["PATH_BASE"]; -if (!string.IsNullOrEmpty(pathBase)) -{ - app.UsePathBase(pathBase); -} +builder.Services.AddGrpc(); + +builder.Services.AddCustomDbContext(builder.Configuration); +builder.Services.AddCustomOptions(builder.Configuration); +builder.Services.AddCustomHealthCheck(builder.Configuration); +builder.Services.AddIntegrationServices(); -app.UseSwagger() - .UseSwaggerUI(c => - { - c.SwaggerEndpoint($"{(!string.IsNullOrEmpty(pathBase) ? pathBase : string.Empty)}/swagger/v1/swagger.json", "Catalog.API V1"); - }); +builder.Services.AddTransient(); +builder.Services.AddTransient(); + +var app = builder.Build(); + +app.UseServiceDefaults(); -app.UseRouting(); -app.UseCors("CorsPolicy"); app.MapDefaultControllerRoute(); app.MapControllers(); + app.UseFileServer(new FileServerOptions { FileProvider = new PhysicalFileProvider(Path.Combine(app.Environment.ContentRootPath, "Pics")), RequestPath = "/pics" }); -app.UseStaticFiles(new StaticFileOptions -{ - FileProvider = new PhysicalFileProvider(Path.Combine(app.Environment.ContentRootPath, "Pics")), - RequestPath = "/pics" -}); -app.MapGet("/_proto/", async ctx => -{ - ctx.Response.ContentType = "text/plain"; - using var fs = new FileStream(Path.Combine(app.Environment.ContentRootPath, "Proto", "catalog.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.MapGrpcService(); -app.MapHealthChecks("/hc", new HealthCheckOptions() -{ - Predicate = _ => true, - ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse -}); -app.MapHealthChecks("/liveness", new HealthCheckOptions -{ - Predicate = r => r.Name.Contains("self") -}); -ConfigureEventBus(app); +var eventBus = app.Services.GetRequiredService(); + +eventBus.Subscribe(); +eventBus.Subscribe(); try { - Log.Information("Configuring web host ({ApplicationContext})...", Program.AppName); + app.Logger.LogInformation("Configuring web host ({ApplicationContext})...", AppName); + using var scope = app.Services.CreateScope(); var context = scope.ServiceProvider.GetRequiredService(); - var env = app.Services.GetService(); var settings = app.Services.GetService>(); var logger = app.Services.GetService>(); await context.Database.MigrateAsync(); - await new CatalogContextSeed().SeedAsync(context, env, settings, logger); + await new CatalogContextSeed().SeedAsync(context, app.Environment, settings, logger); var integEventContext = scope.ServiceProvider.GetRequiredService(); await integEventContext.Database.MigrateAsync(); app.Logger.LogInformation("Starting web host ({ApplicationName})...", AppName); @@ -121,268 +60,12 @@ try } catch (Exception ex) { - Log.Fatal(ex, "Program terminated unexpectedly ({ApplicationContext})!", Program.AppName); + app.Logger.LogCritical(ex, "Program terminated unexpectedly ({ApplicationContext})!", AppName); return 1; } -finally -{ - Log.CloseAndFlush(); -} -void ConfigureEventBus(IApplicationBuilder app) -{ - var eventBus = app.ApplicationServices.GetRequiredService(); - eventBus.Subscribe(); - eventBus.Subscribe(); -} - -(int httpPort, int grpcPort) GetDefinedPorts(IConfiguration config) -{ - var grpcPort = config.GetValue("GRPC_PORT", 81); - var port = config.GetValue("PORT", 80); - return (port, grpcPort); -} public partial class Program { public static string Namespace = typeof(Program).Assembly.GetName().Name; public static string AppName = Namespace.Substring(Namespace.LastIndexOf('.', Namespace.LastIndexOf('.') - 1) + 1); } - -public static class CustomExtensionMethods -{ - public static IServiceCollection AddAppInsight(this IServiceCollection services, IConfiguration configuration) - { - services.AddApplicationInsightsTelemetry(configuration); - services.AddApplicationInsightsKubernetesEnricher(); - - return services; - } - - public static IServiceCollection AddCustomMVC(this IServiceCollection services, IConfiguration configuration) - { - services.AddControllers(options => - { - options.Filters.Add(typeof(HttpGlobalExceptionFilter)); - }) - .AddJsonOptions(options => options.JsonSerializerOptions.WriteIndented = true); - - services.AddCors(options => - { - options.AddPolicy("CorsPolicy", - builder => builder - .SetIsOriginAllowed((host) => true) - .AllowAnyMethod() - .AllowAnyHeader() - .AllowCredentials()); - }); - - return services; - } - - public static IServiceCollection AddCustomHealthCheck(this IServiceCollection services, IConfiguration configuration) - { - var accountName = configuration.GetValue("AzureStorageAccountName"); - var accountKey = configuration.GetValue("AzureStorageAccountKey"); - - var hcBuilder = services.AddHealthChecks(); - - hcBuilder - .AddCheck("self", () => HealthCheckResult.Healthy()) - .AddSqlServer( - configuration["ConnectionString"], - name: "CatalogDB-check", - tags: new string[] { "catalogdb" }); - - if (!string.IsNullOrEmpty(accountName) && !string.IsNullOrEmpty(accountKey)) - { - hcBuilder - .AddAzureBlobStorage( - $"DefaultEndpointsProtocol=https;AccountName={accountName};AccountKey={accountKey};EndpointSuffix=core.windows.net", - name: "catalog-storage-check", - tags: new string[] { "catalogstorage" }); - } - - if (configuration.GetValue("AzureServiceBusEnabled")) - { - hcBuilder - .AddAzureServiceBusTopic( - configuration["EventBusConnection"], - topicName: "eshop_event_bus", - name: "catalog-servicebus-check", - tags: new string[] { "servicebus" }); - } - else - { - hcBuilder - .AddRabbitMQ( - $"amqp://{configuration["EventBusConnection"]}", - name: "catalog-rabbitmqbus-check", - tags: new string[] { "rabbitmqbus" }); - } - - return services; - } - - public static IServiceCollection AddCustomDbContext(this IServiceCollection services, IConfiguration configuration) - { - services.AddEntityFrameworkSqlServer() - .AddDbContext(options => - { - options.UseSqlServer(configuration["ConnectionString"], - sqlServerOptionsAction: sqlOptions => - { - sqlOptions.MigrationsAssembly(typeof(Program).GetTypeInfo().Assembly.GetName().Name); - //Configuring Connection Resiliency: https://docs.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency - sqlOptions.EnableRetryOnFailure(maxRetryCount: 15, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null); - }); - }); - - services.AddDbContext(options => - { - options.UseSqlServer(configuration["ConnectionString"], - sqlServerOptionsAction: sqlOptions => - { - sqlOptions.MigrationsAssembly(typeof(Program).GetTypeInfo().Assembly.GetName().Name); - //Configuring Connection Resiliency: https://docs.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency - sqlOptions.EnableRetryOnFailure(maxRetryCount: 15, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null); - }); - }); - - return services; - } - - public static IServiceCollection AddCustomOptions(this IServiceCollection services, IConfiguration configuration) - { - services.Configure(configuration); - services.Configure(options => - { - options.InvalidModelStateResponseFactory = context => - { - var problemDetails = new ValidationProblemDetails(context.ModelState) - { - Instance = context.HttpContext.Request.Path, - Status = StatusCodes.Status400BadRequest, - Detail = "Please refer to the errors property for additional details." - }; - - return new BadRequestObjectResult(problemDetails) - { - ContentTypes = { "application/problem+json", "application/problem+xml" } - }; - }; - }); - - return services; - } - - public static IServiceCollection AddSwagger(this IServiceCollection services, IConfiguration configuration) - { - services.AddSwaggerGen(options => - { - options.SwaggerDoc("v1", new OpenApiInfo - { - Title = "eShopOnContainers - Catalog HTTP API", - Version = "v1", - Description = "The Catalog Microservice HTTP API. This is a Data-Driven/CRUD microservice sample" - }); - }); - - return services; - - } - - public static IServiceCollection AddIntegrationServices(this IServiceCollection services, IConfiguration configuration) - { - services.AddTransient>( - sp => (DbConnection c) => new IntegrationEventLogService(c)); - - services.AddTransient(); - - if (configuration.GetValue("AzureServiceBusEnabled")) - { - services.AddSingleton(sp => - { - var settings = sp.GetRequiredService>().Value; - var serviceBusConnection = settings.EventBusConnection; - - return new DefaultServiceBusPersisterConnection(serviceBusConnection); - }); - } - else - { - services.AddSingleton(sp => - { - var settings = sp.GetRequiredService>().Value; - 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); - }); - } - - return services; - } - - public static IServiceCollection AddEventBus(this IServiceCollection services, IConfiguration configuration) - { - if (configuration.GetValue("AzureServiceBusEnabled")) - { - services.AddSingleton(sp => - { - var serviceBusPersisterConnection = sp.GetRequiredService(); - var logger = sp.GetRequiredService>(); - var eventBusSubcriptionsManager = sp.GetRequiredService(); - string subscriptionName = configuration["SubscriptionClientName"]; - - return new EventBusServiceBus(serviceBusPersisterConnection, logger, - eventBusSubcriptionsManager, sp, subscriptionName); - }); - - } - else - { - services.AddSingleton(sp => - { - var subscriptionClientName = configuration["SubscriptionClientName"]; - var rabbitMQPersistentConnection = sp.GetRequiredService(); - var logger = sp.GetRequiredService>(); - var eventBusSubcriptionsManager = sp.GetRequiredService(); - - var retryCount = 5; - if (!string.IsNullOrEmpty(configuration["EventBusRetryCount"])) - { - retryCount = int.Parse(configuration["EventBusRetryCount"]); - } - - return new EventBusRabbitMQ(rabbitMQPersistentConnection, logger, sp, eventBusSubcriptionsManager, subscriptionClientName, retryCount); - }); - } - - services.AddSingleton(); - services.AddTransient(); - services.AddTransient(); - - return services; - } -} diff --git a/src/Services/Catalog/Catalog.API/appsettings.json b/src/Services/Catalog/Catalog.API/appsettings.json index f8342fe8d..eac02f2fc 100644 --- a/src/Services/Catalog/Catalog.API/appsettings.json +++ b/src/Services/Catalog/Catalog.API/appsettings.json @@ -18,13 +18,30 @@ "ApplicationInsights": { "InstrumentationKey": "" }, - "EventBusRetryCount": 5, "UseVault": false, "Vault": { "Name": "eshop", "ClientId": "your-client-id", "ClientSecret": "your-client-secret" + }, + "OpenApi": { + "Endpoint": { + "Name": "" + }, + "Document": { + "Name": "Catalog.API V1", + "Description": "The Catalog Microservice HTTP API. This is a Data-Driven/CRUD microservice sample", + "Title": "eShopOnContainers - Catalog HTTP API", + "Version": "v1" + } + }, + "ConnectionStrings": { + "Application": "" + }, + "EventBus": { + "SubscriptionClientName": "Basket", + "ConnectionString": "localhost", + "RetryCount": 5 } - }