diff --git a/src/Services/Ordering/Ordering.API/.editorconfig b/src/Services/Ordering/Ordering.API/.editorconfig new file mode 100644 index 000000000..a2816621e --- /dev/null +++ b/src/Services/Ordering/Ordering.API/.editorconfig @@ -0,0 +1,132 @@ +############################### +# Core EditorConfig Options # +############################### +root = true +# All files +[*] +indent_style = space + +# XML project files +[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] +indent_size = 2 + +# XML config files +[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}] +indent_size = 2 + +# Code files +[*.{cs,csx,vb,vbx}] +indent_size = 4 +insert_final_newline = true +charset = utf-8-bom +############################### +# .NET Coding Conventions # +############################### +[*.{cs,vb}] +# Organize usings +dotnet_sort_system_directives_first = true +# this. preferences +dotnet_style_qualification_for_field = false:silent +dotnet_style_qualification_for_property = false:silent +dotnet_style_qualification_for_method = false:silent +dotnet_style_qualification_for_event = false:silent +# Language keywords vs BCL types preferences +dotnet_style_predefined_type_for_locals_parameters_members = true:silent +dotnet_style_predefined_type_for_member_access = true:silent +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent +# Modifier preferences +dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent +dotnet_style_readonly_field = true:suggestion +# Expression-level preferences +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_auto_properties = true:silent +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +############################### +# Naming Conventions # +############################### +# Style Definitions +dotnet_naming_style.pascal_case_style.capitalization = pascal_case +# Use PascalCase for constant fields +dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields +dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style +dotnet_naming_symbols.constant_fields.applicable_kinds = field +dotnet_naming_symbols.constant_fields.applicable_accessibilities = * +dotnet_naming_symbols.constant_fields.required_modifiers = const +############################### +# C# Coding Conventions # +############################### +[*.cs] +# var preferences +csharp_style_var_for_built_in_types = true:silent +csharp_style_var_when_type_is_apparent = true:silent +csharp_style_var_elsewhere = true:silent +# Expression-bodied members +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_properties = true:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_accessors = true:silent +# Pattern matching preferences +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +# Null-checking preferences +csharp_style_throw_expression = true:suggestion +csharp_style_conditional_delegate_call = true:suggestion +# Modifier preferences +csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion +# Expression-level preferences +csharp_prefer_braces = true:silent +csharp_style_deconstructed_variable_declaration = true:suggestion +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_prefer_local_over_anonymous_function = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +############################### +# C# Formatting Rules # +############################### +# New line preferences +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_between_query_expression_clauses = true +# Indentation preferences +csharp_indent_case_contents = true +csharp_indent_switch_labels = true +csharp_indent_labels = flush_left +# Space preferences +csharp_space_after_cast = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_around_binary_operators = before_and_after +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +# Wrapping preferences +csharp_preserve_single_line_statements = true +csharp_preserve_single_line_blocks = true +############################### +# VB Coding Conventions # +############################### +[*.vb] +# Modifier preferences +visual_basic_preferred_modifier_order = Partial,Default,Private,Protected,Public,Friend,NotOverridable,Overridable,MustOverride,Overloads,Overrides,MustInherit,NotInheritable,Static,Shared,Shadows,ReadOnly,WriteOnly,Dim,Const,WithEvents,Widening,Narrowing,Custom,Async:suggestion \ No newline at end of file diff --git a/src/Services/Ordering/Ordering.API/Ordering.API.csproj b/src/Services/Ordering/Ordering.API/Ordering.API.csproj index 573ce2665..166685321 100644 --- a/src/Services/Ordering/Ordering.API/Ordering.API.csproj +++ b/src/Services/Ordering/Ordering.API/Ordering.API.csproj @@ -76,4 +76,8 @@ + + + + diff --git a/src/Services/Ordering/Ordering.API/Program.cs b/src/Services/Ordering/Ordering.API/Program.cs index 66499d73e..ba62a4dcc 100644 --- a/src/Services/Ordering/Ordering.API/Program.cs +++ b/src/Services/Ordering/Ordering.API/Program.cs @@ -1,27 +1,134 @@ -var configuration = GetConfiguration(); +using Autofac.Core; +using Microsoft.Azure.Amqp.Framing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; -Log.Logger = CreateSerilogLogger(configuration); - -try +var appName = "Ordering.API"; +var builder = WebApplication.CreateBuilder(new WebApplicationOptions +{ + Args = args, + ApplicationName = typeof(Program).Assembly.FullName, + ContentRootPath = Directory.GetCurrentDirectory() +}); +if (builder.Configuration.GetValue("UseVault", false)) +{ + TokenCredential credential = new ClientSecretCredential( + builder.Configuration["Vault:TenantId"], + builder.Configuration["Vault:ClientId"], + builder.Configuration["Vault:ClientSecret"]); + builder.Configuration.AddAzureKeyVault(new Uri($"https://{builder.Configuration["Vault:Name"]}.vault.azure.net/"), credential); +} +builder.Configuration.SetBasePath(Directory.GetCurrentDirectory()); +builder.Configuration.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true); +builder.Configuration.AddEnvironmentVariables(); +builder.WebHost.ConfigureKestrel(options => { - Log.Information("Configuring web host ({ApplicationContext})...", Program.AppName); - var host = BuildWebHost(configuration, args); + var ports = GetDefinedPorts(builder.Configuration); + options.Listen(IPAddress.Any, ports.httpPort, listenOptions => + { + listenOptions.Protocols = HttpProtocols.Http1AndHttp2; + }); - Log.Information("Applying migrations ({ApplicationContext})...", Program.AppName); - host.MigrateDbContext((context, services) => + options.Listen(IPAddress.Any, ports.grpcPort, listenOptions => { - var env = services.GetService(); - var settings = services.GetService>(); - var logger = services.GetService>(); + listenOptions.Protocols = HttpProtocols.Http2; + }); - new OrderingContextSeed() - .SeedAsync(context, env, settings, logger) - .Wait(); +}); +builder.WebHost.CaptureStartupErrors(false); +builder.Host.UseSerilog(CreateSerilogLogger(builder.Configuration)); +builder.Services + .AddGrpc(options => + { + options.EnableDetailedErrors = true; }) - .MigrateDbContext((_, __) => { }); + .Services + .AddApplicationInsights(builder.Configuration) + .AddCustomMvc() + .AddHealthChecks(builder.Configuration) + .AddCustomDbContext(builder.Configuration) + .AddCustomSwagger(builder.Configuration) + .AddCustomAuthentication(builder.Configuration) + .AddCustomAuthorization(builder.Configuration) + .AddCustomIntegrations(builder.Configuration) + .AddCustomConfiguration(builder.Configuration) + .AddEventBus(builder.Configuration); + +builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory()); +// Register your own things directly with Autofac here. Don't +// call builder.Populate(), that happens in AutofacServiceProviderFactory +// for you. +builder.Host.ConfigureContainer(conbuilder => conbuilder.RegisterModule(new MediatorModule())); +builder.Host.ConfigureContainer(conbuilder => conbuilder.RegisterModule(new ApplicationModule(builder.Configuration["ConnectionString"]))); +var app = builder.Build(); +if (app.Environment.IsDevelopment()) +{ + app.UseDeveloperExceptionPage(); +} +else +{ + app.UseExceptionHandler("/Home/Error"); +} +var pathBase = app.Configuration["PATH_BASE"]; +if (!string.IsNullOrEmpty(pathBase)) +{ + app.UsePathBase(pathBase); +} +app.UseSwagger() + .UseSwaggerUI(c => + { + c.SwaggerEndpoint($"{(!string.IsNullOrEmpty(pathBase) ? pathBase : string.Empty)}/swagger/v1/swagger.json", "Ordering.API V1"); + c.OAuthClientId("orderingswaggerui"); + c.OAuthAppName("Ordering Swagger UI"); + }); + +app.UseRouting(); +app.UseCors("CorsPolicy"); +app.UseAuthentication(); +app.UseAuthorization(); +app.MapGrpcService(); +app.MapDefaultControllerRoute(); +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.MapHealthChecks("/hc", new HealthCheckOptions() +{ + Predicate = _ => true, + ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse +}); +app.MapHealthChecks("/liveness", new HealthCheckOptions +{ + Predicate = r => r.Name.Contains("self") +}); +ConfigureEventBus(app); +try +{ + Log.Information("Applying migrations ({ApplicationContext})...", Program.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 OrderingContextSeed().SeedAsync(context, env, settings, logger); + var integEventContext = scope.ServiceProvider.GetRequiredService(); + await integEventContext.Database.MigrateAsync(); Log.Information("Starting web host ({ApplicationContext})...", Program.AppName); - host.Run(); + await app.RunAsync(); return 0; } @@ -34,30 +141,17 @@ finally { Log.CloseAndFlush(); } +void ConfigureEventBus(IApplicationBuilder app) +{ + var eventBus = app.ApplicationServices.GetRequiredService(); -IWebHost BuildWebHost(IConfiguration configuration, string[] args) => - WebHost.CreateDefaultBuilder(args) - .CaptureStartupErrors(false) - .ConfigureKestrel(options => - { - var ports = GetDefinedPorts(configuration); - options.Listen(IPAddress.Any, ports.httpPort, listenOptions => - { - listenOptions.Protocols = HttpProtocols.Http1AndHttp2; - }); - - options.Listen(IPAddress.Any, ports.grpcPort, listenOptions => - { - listenOptions.Protocols = HttpProtocols.Http2; - }); - - }) - .ConfigureAppConfiguration(x => x.AddConfiguration(configuration)) - .UseStartup() - .UseContentRoot(Directory.GetCurrentDirectory()) - .UseSerilog() - .Build(); - + eventBus.Subscribe>(); + eventBus.Subscribe>(); + eventBus.Subscribe>(); + eventBus.Subscribe>(); + eventBus.Subscribe>(); + eventBus.Subscribe>(); +} Serilog.ILogger CreateSerilogLogger(IConfiguration configuration) { var seqServerUrl = configuration["Serilog:SeqServerUrl"]; @@ -68,42 +162,295 @@ Serilog.ILogger CreateSerilogLogger(IConfiguration configuration) .Enrich.FromLogContext() .WriteTo.Console() .WriteTo.Seq(string.IsNullOrWhiteSpace(seqServerUrl) ? "http://seq" : seqServerUrl) - .WriteTo.Http(string.IsNullOrWhiteSpace(logstashUrl) ? "http://logstash:8080" : logstashUrl,null) + .WriteTo.Http(string.IsNullOrWhiteSpace(logstashUrl) ? "http://logstash:8080" : logstashUrl, null) .ReadFrom.Configuration(configuration) .CreateLogger(); } - -IConfiguration GetConfiguration() -{ - var builder = new ConfigurationBuilder() - .SetBasePath(Directory.GetCurrentDirectory()) - .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) - .AddEnvironmentVariables(); - - var config = builder.Build(); - - if (config.GetValue("UseVault", false)) - { - TokenCredential credential = new ClientSecretCredential( - config["Vault:TenantId"], - config["Vault:ClientId"], - config["Vault:ClientSecret"]); - builder.AddAzureKeyVault(new Uri($"https://{config["Vault:Name"]}.vault.azure.net/"), credential); - } - - return builder.Build(); -} - (int httpPort, int grpcPort) GetDefinedPorts(IConfiguration config) { var grpcPort = config.GetValue("GRPC_PORT", 5001); var port = config.GetValue("PORT", 80); return (port, grpcPort); } - public partial class Program { - public static string Namespace = typeof(Startup).Namespace; + public static string Namespace = typeof(Program).Assembly.GetName().Name; public static string AppName = Namespace.Substring(Namespace.LastIndexOf('.', Namespace.LastIndexOf('.') - 1) + 1); -} \ No newline at end of file +} +static class CustomExtensionsMethods +{ + public static IServiceCollection AddApplicationInsights(this IServiceCollection services, IConfiguration configuration) + { + services.AddApplicationInsightsTelemetry(configuration); + services.AddApplicationInsightsKubernetesEnricher(); + + return services; + } + + public static IServiceCollection AddCustomMvc(this IServiceCollection services) + { + // Add framework services. + services.AddControllers(options => + { + options.Filters.Add(typeof(HttpGlobalExceptionFilter)); + }) + // Added for functional tests + .AddApplicationPart(typeof(OrdersController).Assembly) + .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 AddHealthChecks(this IServiceCollection services, IConfiguration configuration) + { + var hcBuilder = services.AddHealthChecks(); + + hcBuilder.AddCheck("self", () => HealthCheckResult.Healthy()); + + hcBuilder + .AddSqlServer( + configuration["ConnectionString"], + name: "OrderingDB-check", + tags: new string[] { "orderingdb" }); + + if (configuration.GetValue("AzureServiceBusEnabled")) + { + hcBuilder + .AddAzureServiceBusTopic( + configuration["EventBusConnection"], + topicName: "eshop_event_bus", + name: "ordering-servicebus-check", + tags: new string[] { "servicebus" }); + } + else + { + hcBuilder + .AddRabbitMQ( + $"amqp://{configuration["EventBusConnection"]}", + name: "ordering-rabbitmqbus-check", + tags: new string[] { "rabbitmqbus" }); + } + + return services; + } + + public static IServiceCollection AddCustomDbContext(this IServiceCollection services, IConfiguration configuration) + { + services.AddDbContext(options => + { + options.UseSqlServer(configuration["ConnectionString"], + sqlServerOptionsAction: sqlOptions => + { + sqlOptions.MigrationsAssembly(typeof(Program).GetTypeInfo().Assembly.GetName().Name); + sqlOptions.EnableRetryOnFailure(maxRetryCount: 15, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null); + }); + }, + ServiceLifetime.Scoped //Showing explicitly that the DbContext is shared across the HTTP request scope (graph of objects started in the HTTP request) + ); + + 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 AddCustomSwagger(this IServiceCollection services, IConfiguration configuration) + { + services.AddSwaggerGen(options => + { + options.SwaggerDoc("v1", new OpenApiInfo + { + Title = "eShopOnContainers - Ordering HTTP API", + Version = "v1", + Description = "The Ordering Service HTTP API" + }); + options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme + { + Type = SecuritySchemeType.OAuth2, + Flows = new OpenApiOAuthFlows() + { + Implicit = new OpenApiOAuthFlow() + { + AuthorizationUrl = new Uri($"{configuration.GetValue("IdentityUrlExternal")}/connect/authorize"), + TokenUrl = new Uri($"{configuration.GetValue("IdentityUrlExternal")}/connect/token"), + Scopes = new Dictionary() + { + { "orders", "Ordering API" } + } + } + } + }); + options.OperationFilter(); + }); + return services; + } + + public static IServiceCollection AddCustomIntegrations(this IServiceCollection services, IConfiguration configuration) + { + services.AddSingleton(); + services.AddTransient(); + services.AddTransient>( + sp => (DbConnection c) => new IntegrationEventLogService(c)); + + services.AddTransient(); + + if (configuration.GetValue("AzureServiceBusEnabled")) + { + services.AddSingleton(sp => + { + var serviceBusConnectionString = configuration["EventBusConnection"]; + + var subscriptionClientName = configuration["SubscriptionClientName"]; + + return new DefaultServiceBusPersisterConnection(serviceBusConnectionString); + }); + } + 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); + }); + } + + return services; + } + + public static IServiceCollection AddCustomConfiguration(this IServiceCollection services, IConfiguration configuration) + { + services.AddOptions(); + 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 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(); + + return services; + } + + public static IServiceCollection AddCustomAuthentication(this IServiceCollection services, IConfiguration configuration) + { + // prevent from mapping "sub" claim to nameidentifier. + JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("sub"); + + var identityUrl = configuration.GetValue("IdentityUrl"); + + services.AddAuthentication("Bearer").AddJwtBearer(options => + { + options.Authority = identityUrl; + options.RequireHttpsMetadata = false; + options.Audience = "orders"; + options.TokenValidationParameters.ValidateAudience = false; + }); + + return services; + } + public static IServiceCollection AddCustomAuthorization(this IServiceCollection services, IConfiguration configuration) + { + services.AddAuthorization(options => + { + options.AddPolicy("ApiScope", policy => + { + policy.RequireAuthenticatedUser(); + policy.RequireClaim("scope", "orders"); + }); + }); + return services; + } +} diff --git a/src/Services/Ordering/Ordering.API/Startup.cs b/src/Services/Ordering/Ordering.API/Startup.cs deleted file mode 100644 index 8c8ecb117..000000000 --- a/src/Services/Ordering/Ordering.API/Startup.cs +++ /dev/null @@ -1,396 +0,0 @@ -namespace Microsoft.eShopOnContainers.Services.Ordering.API; - -public class Startup -{ - public Startup(IConfiguration configuration) - { - Configuration = configuration; - } - - public IConfiguration Configuration { get; } - - public virtual IServiceProvider ConfigureServices(IServiceCollection services) - { - services - .AddGrpc(options => - { - options.EnableDetailedErrors = true; - }) - .Services - .AddApplicationInsights(Configuration) - .AddCustomMvc() - .AddHealthChecks(Configuration) - .AddCustomDbContext(Configuration) - .AddCustomSwagger(Configuration) - .AddCustomAuthentication(Configuration) - .AddCustomAuthorization(Configuration) - .AddCustomIntegrations(Configuration) - .AddCustomConfiguration(Configuration) - .AddEventBus(Configuration); - //configure autofac - - var container = new ContainerBuilder(); - container.Populate(services); - - container.RegisterModule(new MediatorModule()); - container.RegisterModule(new ApplicationModule(Configuration["ConnectionString"])); - - return new AutofacServiceProvider(container.Build()); - } - - - public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory) - { - //loggerFactory.AddAzureWebAppDiagnostics(); - //loggerFactory.AddApplicationInsights(app.ApplicationServices, LogLevel.Trace); - - var pathBase = Configuration["PATH_BASE"]; - if (!string.IsNullOrEmpty(pathBase)) - { - loggerFactory.CreateLogger().LogDebug("Using PATH BASE '{pathBase}'", pathBase); - app.UsePathBase(pathBase); - } - - app.UseSwagger() - .UseSwaggerUI(c => - { - c.SwaggerEndpoint($"{ (!string.IsNullOrEmpty(pathBase) ? pathBase : string.Empty) }/swagger/v1/swagger.json", "Ordering.API V1"); - c.OAuthClientId("orderingswaggerui"); - c.OAuthAppName("Ordering Swagger UI"); - }); - - app.UseRouting(); - app.UseCors("CorsPolicy"); - ConfigureAuth(app); - - app.UseEndpoints(endpoints => - { - endpoints.MapGrpcService(); - endpoints.MapDefaultControllerRoute(); - endpoints.MapControllers(); - endpoints.MapGet("/_proto/", async ctx => - { - ctx.Response.ContentType = "text/plain"; - using var fs = new FileStream(Path.Combine(env.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); - } - } - }); - endpoints.MapHealthChecks("/hc", new HealthCheckOptions() - { - Predicate = _ => true, - ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse - }); - endpoints.MapHealthChecks("/liveness", new HealthCheckOptions - { - Predicate = r => r.Name.Contains("self") - }); - }); - - ConfigureEventBus(app); - } - - - private void ConfigureEventBus(IApplicationBuilder app) - { - var eventBus = app.ApplicationServices.GetRequiredService(); - - eventBus.Subscribe>(); - eventBus.Subscribe>(); - eventBus.Subscribe>(); - eventBus.Subscribe>(); - eventBus.Subscribe>(); - eventBus.Subscribe>(); - } - - protected virtual void ConfigureAuth(IApplicationBuilder app) - { - app.UseAuthentication(); - app.UseAuthorization(); - } -} - -static class CustomExtensionsMethods -{ - public static IServiceCollection AddApplicationInsights(this IServiceCollection services, IConfiguration configuration) - { - services.AddApplicationInsightsTelemetry(configuration); - services.AddApplicationInsightsKubernetesEnricher(); - - return services; - } - - public static IServiceCollection AddCustomMvc(this IServiceCollection services) - { - // Add framework services. - services.AddControllers(options => - { - options.Filters.Add(typeof(HttpGlobalExceptionFilter)); - }) - // Added for functional tests - .AddApplicationPart(typeof(OrdersController).Assembly) - .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 AddHealthChecks(this IServiceCollection services, IConfiguration configuration) - { - var hcBuilder = services.AddHealthChecks(); - - hcBuilder.AddCheck("self", () => HealthCheckResult.Healthy()); - - hcBuilder - .AddSqlServer( - configuration["ConnectionString"], - name: "OrderingDB-check", - tags: new string[] { "orderingdb" }); - - if (configuration.GetValue("AzureServiceBusEnabled")) - { - hcBuilder - .AddAzureServiceBusTopic( - configuration["EventBusConnection"], - topicName: "eshop_event_bus", - name: "ordering-servicebus-check", - tags: new string[] { "servicebus" }); - } - else - { - hcBuilder - .AddRabbitMQ( - $"amqp://{configuration["EventBusConnection"]}", - name: "ordering-rabbitmqbus-check", - tags: new string[] { "rabbitmqbus" }); - } - - return services; - } - - public static IServiceCollection AddCustomDbContext(this IServiceCollection services, IConfiguration configuration) - { - services.AddDbContext(options => - { - options.UseSqlServer(configuration["ConnectionString"], - sqlServerOptionsAction: sqlOptions => - { - sqlOptions.MigrationsAssembly(typeof(Startup).GetTypeInfo().Assembly.GetName().Name); - sqlOptions.EnableRetryOnFailure(maxRetryCount: 15, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null); - }); - }, - ServiceLifetime.Scoped //Showing explicitly that the DbContext is shared across the HTTP request scope (graph of objects started in the HTTP request) - ); - - services.AddDbContext(options => - { - options.UseSqlServer(configuration["ConnectionString"], - sqlServerOptionsAction: sqlOptions => - { - sqlOptions.MigrationsAssembly(typeof(Startup).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 AddCustomSwagger(this IServiceCollection services, IConfiguration configuration) - { - services.AddSwaggerGen(options => - { - options.SwaggerDoc("v1", new OpenApiInfo - { - Title = "eShopOnContainers - Ordering HTTP API", - Version = "v1", - Description = "The Ordering Service HTTP API" - }); - options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme - { - Type = SecuritySchemeType.OAuth2, - Flows = new OpenApiOAuthFlows() - { - Implicit = new OpenApiOAuthFlow() - { - AuthorizationUrl = new Uri($"{configuration.GetValue("IdentityUrlExternal")}/connect/authorize"), - TokenUrl = new Uri($"{configuration.GetValue("IdentityUrlExternal")}/connect/token"), - Scopes = new Dictionary() - { - { "orders", "Ordering API" } - } - } - } - }); - - options.OperationFilter(); - }); - - return services; - } - - public static IServiceCollection AddCustomIntegrations(this IServiceCollection services, IConfiguration configuration) - { - services.AddSingleton(); - services.AddTransient(); - services.AddTransient>( - sp => (DbConnection c) => new IntegrationEventLogService(c)); - - services.AddTransient(); - - if (configuration.GetValue("AzureServiceBusEnabled")) - { - services.AddSingleton(sp => - { - var serviceBusConnectionString = configuration["EventBusConnection"]; - - var subscriptionClientName = configuration["SubscriptionClientName"]; - - return new DefaultServiceBusPersisterConnection(serviceBusConnectionString); - }); - } - 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); - }); - } - - return services; - } - - public static IServiceCollection AddCustomConfiguration(this IServiceCollection services, IConfiguration configuration) - { - services.AddOptions(); - 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 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(); - - return services; - } - - public static IServiceCollection AddCustomAuthentication(this IServiceCollection services, IConfiguration configuration) - { - // prevent from mapping "sub" claim to nameidentifier. - JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("sub"); - - var identityUrl = configuration.GetValue("IdentityUrl"); - - services.AddAuthentication("Bearer").AddJwtBearer(options => - { - options.Authority = identityUrl; - options.RequireHttpsMetadata = false; - options.Audience = "orders"; - options.TokenValidationParameters.ValidateAudience = false; - }); - - return services; - } - public static IServiceCollection AddCustomAuthorization(this IServiceCollection services, IConfiguration configuration) - { - services.AddAuthorization(options => - { - options.AddPolicy("ApiScope", policy => - { - policy.RequireAuthenticatedUser(); - policy.RequireClaim("scope", "orders"); - }); - }); - return services; - } -} \ No newline at end of file diff --git a/src/Services/Ordering/Ordering.FunctionalTests/OrderingScenarioBase.cs b/src/Services/Ordering/Ordering.FunctionalTests/OrderingScenarioBase.cs index d03d54380..a0b31568b 100644 --- a/src/Services/Ordering/Ordering.FunctionalTests/OrderingScenarioBase.cs +++ b/src/Services/Ordering/Ordering.FunctionalTests/OrderingScenarioBase.cs @@ -13,7 +13,7 @@ public class OrderingScenarioBase { cb.AddJsonFile("appsettings.json", optional: false) .AddEnvironmentVariables(); - }).UseStartup(); + }); var testServer = new TestServer(hostBuilder); diff --git a/src/Services/Ordering/Ordering.FunctionalTests/OrderingTestStartup.cs b/src/Services/Ordering/Ordering.FunctionalTests/OrderingTestStartup.cs deleted file mode 100644 index 7367042de..000000000 --- a/src/Services/Ordering/Ordering.FunctionalTests/OrderingTestStartup.cs +++ /dev/null @@ -1,27 +0,0 @@ -namespace Ordering.FunctionalTests; - -public class OrderingTestsStartup : Startup -{ - public OrderingTestsStartup(IConfiguration env) : base(env) - { - } - - public override IServiceProvider ConfigureServices(IServiceCollection services) - { - // Added to avoid the Authorize data annotation in test environment. - // Property "SuppressCheckForUnhandledSecurityMetadata" in appsettings.json - services.Configure(Configuration); - return base.ConfigureServices(services); - } - protected override void ConfigureAuth(IApplicationBuilder app) - { - if (Configuration["isTest"] == bool.TrueString.ToLowerInvariant()) - { - app.UseMiddleware(); - } - else - { - base.ConfigureAuth(app); - } - } -} diff --git a/src/Tests/Services/Application.FunctionalTests/Services/Basket/BasketScenariosBase.cs b/src/Tests/Services/Application.FunctionalTests/Services/Basket/BasketScenariosBase.cs index 71487d7b4..0cb668b8a 100644 --- a/src/Tests/Services/Application.FunctionalTests/Services/Basket/BasketScenariosBase.cs +++ b/src/Tests/Services/Application.FunctionalTests/Services/Basket/BasketScenariosBase.cs @@ -16,7 +16,7 @@ public class BasketScenariosBase { cb.AddJsonFile("Services/Basket/appsettings.json", optional: false) .AddEnvironmentVariables(); - }).UseStartup(); + }); return new TestServer(hostBuilder); } diff --git a/src/Tests/Services/Application.FunctionalTests/Services/Basket/BasketTestsStartup.cs b/src/Tests/Services/Application.FunctionalTests/Services/Basket/BasketTestsStartup.cs deleted file mode 100644 index 23523a9f4..000000000 --- a/src/Tests/Services/Application.FunctionalTests/Services/Basket/BasketTestsStartup.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace FunctionalTests.Services.Basket; -using Microsoft.eShopOnContainers.Services.Basket.API; -class BasketTestsStartup : Startup -{ - public BasketTestsStartup(IConfiguration env) : base(env) - { - } - - protected override void ConfigureAuth(IApplicationBuilder app) - { - if (Configuration["isTest"] == bool.TrueString.ToLowerInvariant()) - { - app.UseMiddleware(); - app.UseAuthorization(); - } - else - { - base.ConfigureAuth(app); - } - } -} diff --git a/src/Tests/Services/Application.FunctionalTests/Services/Catalog/CatalogScenariosBase.cs b/src/Tests/Services/Application.FunctionalTests/Services/Catalog/CatalogScenariosBase.cs index ec762a947..0fcf32e3c 100644 --- a/src/Tests/Services/Application.FunctionalTests/Services/Catalog/CatalogScenariosBase.cs +++ b/src/Tests/Services/Application.FunctionalTests/Services/Catalog/CatalogScenariosBase.cs @@ -14,7 +14,7 @@ public class CatalogScenariosBase { cb.AddJsonFile("Services/Catalog/appsettings.json", optional: false) .AddEnvironmentVariables(); - }).UseStartup(); + }); var testServer = new TestServer(hostBuilder); diff --git a/src/Tests/Services/Application.FunctionalTests/Services/Ordering/OrderingScenariosBase.cs b/src/Tests/Services/Application.FunctionalTests/Services/Ordering/OrderingScenariosBase.cs index fce6bca18..3d419eb83 100644 --- a/src/Tests/Services/Application.FunctionalTests/Services/Ordering/OrderingScenariosBase.cs +++ b/src/Tests/Services/Application.FunctionalTests/Services/Ordering/OrderingScenariosBase.cs @@ -13,7 +13,7 @@ public class OrderingScenariosBase { cb.AddJsonFile("Services/Ordering/appsettings.json", optional: false) .AddEnvironmentVariables(); - }).UseStartup(); + }); var testServer = new TestServer(hostBuilder); diff --git a/src/Tests/Services/Application.FunctionalTests/Services/Ordering/OrderingTestsStartup.cs b/src/Tests/Services/Application.FunctionalTests/Services/Ordering/OrderingTestsStartup.cs deleted file mode 100644 index f6f3d8e51..000000000 --- a/src/Tests/Services/Application.FunctionalTests/Services/Ordering/OrderingTestsStartup.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace FunctionalTests.Services.Ordering; -using Microsoft.eShopOnContainers.Services.Ordering.API; - -public class OrderingTestsStartup : Startup -{ - public OrderingTestsStartup(IConfiguration env) : base(env) - { - } - - protected override void ConfigureAuth(IApplicationBuilder app) - { - if (Configuration["isTest"] == bool.TrueString.ToLowerInvariant()) - { - app.UseMiddleware(); - app.UseAuthorization(); - } - else - { - base.ConfigureAuth(app); - } - } -}