diff --git a/src/Services/Catalog/Catalog.API/Infrastructure/Filters/HttpGlobalExceptionFilter.cs b/src/Services/Catalog/Catalog.API/Infrastructure/Filters/HttpGlobalExceptionFilter.cs index 41d3f08e1..4c4c746a6 100644 --- a/src/Services/Catalog/Catalog.API/Infrastructure/Filters/HttpGlobalExceptionFilter.cs +++ b/src/Services/Catalog/Catalog.API/Infrastructure/Filters/HttpGlobalExceptionFilter.cs @@ -1,6 +1,7 @@ using Catalog.API.Infrastructure.ActionResults; using Catalog.API.Infrastructure.Exceptions; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.Extensions.Logging; @@ -27,12 +28,16 @@ namespace Catalog.API.Infrastructure.Filters if (context.Exception.GetType() == typeof(CatalogDomainException)) { - var json = new JsonErrorResponse + var problemDetails = new ValidationProblemDetails() { - Messages = new[] { context.Exception.Message } + Instance = context.HttpContext.Request.Path, + Status = StatusCodes.Status400BadRequest, + Detail = "Please refer to the errors property for additional details." }; - context.Result = new BadRequestObjectResult(json); + problemDetails.Errors.Add("DomainValidations", new string[] { context.Exception.Message.ToString() }); + + context.Result = new BadRequestObjectResult(problemDetails); context.HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest; } else diff --git a/src/Services/Ordering/Ordering.API/Controllers/OrdersController.cs b/src/Services/Ordering/Ordering.API/Controllers/OrdersController.cs index ac179c97f..c93e18400 100644 --- a/src/Services/Ordering/Ordering.API/Controllers/OrdersController.cs +++ b/src/Services/Ordering/Ordering.API/Controllers/OrdersController.cs @@ -5,7 +5,6 @@ using Microsoft.eShopOnContainers.Services.Ordering.API.Application.Commands; using Microsoft.eShopOnContainers.Services.Ordering.API.Application.Queries; using Microsoft.eShopOnContainers.Services.Ordering.API.Infrastructure.Services; using Ordering.API.Application.Commands; -using Ordering.API.Application.Models; using System; using System.Collections.Generic; using System.Net; @@ -15,6 +14,7 @@ namespace Microsoft.eShopOnContainers.Services.Ordering.API.Controllers { [Route("api/v1/[controller]")] [Authorize] + [ApiController] public class OrdersController : Controller { private readonly IMediator _mediator; diff --git a/src/Services/Ordering/Ordering.API/Infrastructure/Filters/HttpGlobalExceptionFilter.cs b/src/Services/Ordering/Ordering.API/Infrastructure/Filters/HttpGlobalExceptionFilter.cs index 69faf8e42..a4ce415ea 100644 --- a/src/Services/Ordering/Ordering.API/Infrastructure/Filters/HttpGlobalExceptionFilter.cs +++ b/src/Services/Ordering/Ordering.API/Infrastructure/Filters/HttpGlobalExceptionFilter.cs @@ -3,6 +3,7 @@ using AspNetCore.Mvc; using global::Ordering.Domain.Exceptions; using Microsoft.AspNetCore.Hosting; + using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.eShopOnContainers.Services.Ordering.API.Infrastructure.ActionResults; using Microsoft.Extensions.Logging; @@ -25,16 +26,18 @@ context.Exception, context.Exception.Message); - if (context.Exception.GetType() == typeof(OrderingDomainException)) + if (context.Exception.GetType() == typeof(OrderingDomainException)) { - var json = new JsonErrorResponse + var problemDetails = new ValidationProblemDetails() { - Messages = new[] { context.Exception.Message } + Instance = context.HttpContext.Request.Path, + Status = StatusCodes.Status400BadRequest, + Detail = "Please refer to the errors property for additional details." }; - // Result asigned to a result object but in destiny the response is empty. This is a known bug of .net core 1.1 - //It will be fixed in .net core 1.1.2. See https://github.com/aspnet/Mvc/issues/5594 for more information - context.Result = new BadRequestObjectResult(json); + problemDetails.Errors.Add("DomainValidations", new string[] { context.Exception.Message.ToString() }); + + context.Result = new BadRequestObjectResult(problemDetails); context.HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest; } else @@ -52,7 +55,7 @@ // Result asigned to a result object but in destiny the response is empty. This is a known bug of .net core 1.1 // It will be fixed in .net core 1.1.2. See https://github.com/aspnet/Mvc/issues/5594 for more information context.Result = new InternalServerErrorObjectResult(json); - context.HttpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError; + context.HttpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError; } context.ExceptionHandled = true; } diff --git a/src/Services/Ordering/Ordering.API/Startup.cs b/src/Services/Ordering/Ordering.API/Startup.cs index 2948a3997..42567641a 100644 --- a/src/Services/Ordering/Ordering.API/Startup.cs +++ b/src/Services/Ordering/Ordering.API/Startup.cs @@ -15,6 +15,7 @@ using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; + using Microsoft.AspNetCore.Mvc; using Microsoft.Azure.ServiceBus; using Microsoft.EntityFrameworkCore; using Microsoft.eShopOnContainers.BuildingBlocks.EventBus; @@ -47,8 +48,110 @@ public IServiceProvider ConfigureServices(IServiceCollection services) { - RegisterAppInsights(services); + services.AddApplicationInsights(Configuration) + .AddCustomMvc() + .AddHealthChecks(Configuration) + .AddCustomDbContext(Configuration) + .AddCustomSwagger(Configuration) + .AddCustomIntegrations(Configuration) + .AddCustomConfiguration(Configuration) + .AddEventBus(Configuration) + .AddCustomAuthentication(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, ILoggerFactory loggerFactory) + { + loggerFactory.AddAzureWebAppDiagnostics(); + loggerFactory.AddApplicationInsights(app.ApplicationServices, LogLevel.Trace); + + var pathBase = Configuration["PATH_BASE"]; + if (!string.IsNullOrEmpty(pathBase)) + { + loggerFactory.CreateLogger("init").LogDebug($"Using PATH BASE '{pathBase}'"); + app.UsePathBase(pathBase); + } + +#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously + app.Map("/liveness", lapp => lapp.Run(async ctx => ctx.Response.StatusCode = 200)); +#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously + + app.UseCors("CorsPolicy"); + + ConfigureAuth(app); + + app.UseMvcWithDefaultRoute(); + + 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"); + }); + + 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) + { + if (Configuration.GetValue("UseLoadTest")) + { + app.UseMiddleware(); + } + + app.UseAuthentication(); + } + } + + static class CustomExtensionsMethods + { + public static IServiceCollection AddApplicationInsights(this IServiceCollection services, IConfiguration configuration) + { + services.AddApplicationInsightsTelemetry(configuration); + var orchestratorType = configuration.GetValue("OrchestratorType"); + + if (orchestratorType?.ToUpper() == "K8S") + { + // Enable K8s telemetry initializer + services.EnableKubernetes(); + } + if (orchestratorType?.ToUpper() == "SF") + { + // Enable SF telemetry initializer + services.AddSingleton((serviceProvider) => + new FabricTelemetryInitializer()); + } + + return services; + } + + public static IServiceCollection AddCustomMvc(this IServiceCollection services) + { // Add framework services. services.AddMvc(options => { @@ -56,34 +159,51 @@ }).AddControllersAsServices(); //Injecting Controllers themselves thru DI //For further info see: http://docs.autofac.org/en/latest/integration/aspnetcore.html#controllers-as-services - services.AddTransient(); + services.AddCors(options => + { + options.AddPolicy("CorsPolicy", + builder => builder.AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader() + .AllowCredentials()); + }); + + return services; + } + public static IServiceCollection AddHealthChecks(this IServiceCollection services, IConfiguration configuration) + { services.AddHealthChecks(checks => { var minutes = 1; - if (int.TryParse(Configuration["HealthCheck:Timeout"], out var minutesParsed)) + if (int.TryParse(configuration["HealthCheck:Timeout"], out var minutesParsed)) { minutes = minutesParsed; } - checks.AddSqlCheck("OrderingDb", Configuration["ConnectionString"], TimeSpan.FromMinutes(minutes)); + checks.AddSqlCheck("OrderingDb", configuration["ConnectionString"], TimeSpan.FromMinutes(minutes)); }); + return services; + } + + public static IServiceCollection AddCustomDbContext(this IServiceCollection services, IConfiguration configuration) + { services.AddEntityFrameworkSqlServer() - .AddDbContext(options => - { - options.UseSqlServer(Configuration["ConnectionString"], - sqlServerOptionsAction: sqlOptions => - { - sqlOptions.MigrationsAssembly(typeof(Startup).GetTypeInfo().Assembly.GetName().Name); - sqlOptions.EnableRetryOnFailure(maxRetryCount: 10, 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) - ); + .AddDbContext(options => + { + options.UseSqlServer(configuration["ConnectionString"], + sqlServerOptionsAction: sqlOptions => + { + sqlOptions.MigrationsAssembly(typeof(Startup).GetTypeInfo().Assembly.GetName().Name); + sqlOptions.EnableRetryOnFailure(maxRetryCount: 10, 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"], + options.UseSqlServer(configuration["ConnectionString"], sqlServerOptionsAction: sqlOptions => { sqlOptions.MigrationsAssembly(typeof(Startup).GetTypeInfo().Assembly.GetName().Name); @@ -91,10 +211,12 @@ sqlOptions.EnableRetryOnFailure(maxRetryCount: 10, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null); }); }); - - services.Configure(Configuration); + return services; + } + public static IServiceCollection AddCustomSwagger(this IServiceCollection services, IConfiguration configuration) + { services.AddSwaggerGen(options => { options.DescribeAllEnumsAsStrings(); @@ -110,8 +232,8 @@ { Type = "oauth2", Flow = "implicit", - AuthorizationUrl = $"{Configuration.GetValue("IdentityUrlExternal")}/connect/authorize", - TokenUrl = $"{Configuration.GetValue("IdentityUrlExternal")}/connect/token", + AuthorizationUrl = $"{configuration.GetValue("IdentityUrlExternal")}/connect/authorize", + TokenUrl = $"{configuration.GetValue("IdentityUrlExternal")}/connect/token", Scopes = new Dictionary() { { "orders", "Ordering API" } @@ -121,16 +243,11 @@ options.OperationFilter(); }); - services.AddCors(options => - { - options.AddPolicy("CorsPolicy", - builder => builder.AllowAnyOrigin() - .AllowAnyMethod() - .AllowAnyHeader() - .AllowCredentials()); - }); + return services; + } - // Add application services. + public static IServiceCollection AddCustomIntegrations(this IServiceCollection services, IConfiguration configuration) + { services.AddSingleton(); services.AddTransient(); services.AddTransient>( @@ -138,13 +255,13 @@ services.AddTransient(); - if (Configuration.GetValue("AzureServiceBusEnabled")) + if (configuration.GetValue("AzureServiceBusEnabled")) { services.AddSingleton(sp => { var logger = sp.GetRequiredService>(); - var serviceBusConnectionString = Configuration["EventBusConnection"]; + var serviceBusConnectionString = configuration["EventBusConnection"]; var serviceBusConnection = new ServiceBusConnectionStringBuilder(serviceBusConnectionString); return new DefaultServiceBusPersisterConnection(serviceBusConnection, logger); @@ -159,152 +276,69 @@ var factory = new ConnectionFactory() { - HostName = Configuration["EventBusConnection"] + HostName = configuration["EventBusConnection"] }; - if (!string.IsNullOrEmpty(Configuration["EventBusUserName"])) + if (!string.IsNullOrEmpty(configuration["EventBusUserName"])) { - factory.UserName = Configuration["EventBusUserName"]; + factory.UserName = configuration["EventBusUserName"]; } - if (!string.IsNullOrEmpty(Configuration["EventBusPassword"])) + if (!string.IsNullOrEmpty(configuration["EventBusPassword"])) { - factory.Password = Configuration["EventBusPassword"]; + factory.Password = configuration["EventBusPassword"]; } var retryCount = 5; - if (!string.IsNullOrEmpty(Configuration["EventBusRetryCount"])) + if (!string.IsNullOrEmpty(configuration["EventBusRetryCount"])) { - retryCount = int.Parse(Configuration["EventBusRetryCount"]); + retryCount = int.Parse(configuration["EventBusRetryCount"]); } return new DefaultRabbitMQPersistentConnection(factory, logger, retryCount); }); } - RegisterEventBus(services); - ConfigureAuthService(services); - services.AddOptions(); - - //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, ILoggerFactory loggerFactory) - { - loggerFactory.AddConsole(Configuration.GetSection("Logging")); - loggerFactory.AddDebug(); - loggerFactory.AddAzureWebAppDiagnostics(); - loggerFactory.AddApplicationInsights(app.ApplicationServices, LogLevel.Trace); - - var pathBase = Configuration["PATH_BASE"]; - if (!string.IsNullOrEmpty(pathBase)) - { - loggerFactory.CreateLogger("init").LogDebug($"Using PATH BASE '{pathBase}'"); - app.UsePathBase(pathBase); - } - -#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously - app.Map("/liveness", lapp => lapp.Run(async ctx => ctx.Response.StatusCode = 200)); -#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously - - app.UseCors("CorsPolicy"); - - ConfigureAuth(app); - - app.UseMvcWithDefaultRoute(); - - 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"); - }); - - ConfigureEventBus(app); - } - - private void RegisterAppInsights(IServiceCollection services) - { - services.AddApplicationInsightsTelemetry(Configuration); - var orchestratorType = Configuration.GetValue("OrchestratorType"); - - if (orchestratorType?.ToUpper() == "K8S") - { - // Enable K8s telemetry initializer - services.EnableKubernetes(); - } - if (orchestratorType?.ToUpper() == "SF") - { - // Enable SF telemetry initializer - services.AddSingleton((serviceProvider) => - new FabricTelemetryInitializer()); - } + return services; } - private void ConfigureEventBus(IApplicationBuilder app) + public static IServiceCollection AddCustomConfiguration(this IServiceCollection services, IConfiguration configuration) { - var eventBus = app.ApplicationServices.GetRequiredService(); - - eventBus.Subscribe>(); - eventBus.Subscribe>(); - eventBus.Subscribe>(); - eventBus.Subscribe>(); - eventBus.Subscribe>(); - eventBus.Subscribe>(); - } - - private void ConfigureAuthService(IServiceCollection services) - { - // prevent from mapping "sub" claim to nameidentifier. - JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("sub"); - - var identityUrl = Configuration.GetValue("IdentityUrl"); - - services.AddAuthentication(options => + services.AddOptions(); + services.Configure(configuration); + services.Configure(options => { - options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; - options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + 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." + }; - }).AddJwtBearer(options => - { - options.Authority = identityUrl; - options.RequireHttpsMetadata = false; - options.Audience = "orders"; + return new BadRequestObjectResult(problemDetails) + { + ContentTypes = { "application/problem+json", "application/problem+xml" } + }; + }; }); - } - - protected virtual void ConfigureAuth(IApplicationBuilder app) - { - if (Configuration.GetValue("UseLoadTest")) - { - app.UseMiddleware(); - } - app.UseAuthentication(); + return services; } - private void RegisterEventBus(IServiceCollection services) + public static IServiceCollection AddEventBus(this IServiceCollection services, IConfiguration configuration) { - var subscriptionClientName = Configuration["SubscriptionClientName"]; + var subscriptionClientName = configuration["SubscriptionClientName"]; - if (Configuration.GetValue("AzureServiceBusEnabled")) + if (configuration.GetValue("AzureServiceBusEnabled")) { services.AddSingleton(sp => { var serviceBusPersisterConnection = sp.GetRequiredService(); var iLifetimeScope = sp.GetRequiredService(); var logger = sp.GetRequiredService>(); - var eventBusSubcriptionsManager = sp.GetRequiredService(); + var eventBusSubcriptionsManager = sp.GetRequiredService(); return new EventBusServiceBus(serviceBusPersisterConnection, logger, eventBusSubcriptionsManager, subscriptionClientName, iLifetimeScope); @@ -320,9 +354,9 @@ var eventBusSubcriptionsManager = sp.GetRequiredService(); var retryCount = 5; - if (!string.IsNullOrEmpty(Configuration["EventBusRetryCount"])) + if (!string.IsNullOrEmpty(configuration["EventBusRetryCount"])) { - retryCount = int.Parse(Configuration["EventBusRetryCount"]); + retryCount = int.Parse(configuration["EventBusRetryCount"]); } return new EventBusRabbitMQ(rabbitMQPersistentConnection, logger, iLifetimeScope, eventBusSubcriptionsManager, subscriptionClientName, retryCount); @@ -330,6 +364,30 @@ } 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(options => + { + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + + }).AddJwtBearer(options => + { + options.Authority = identityUrl; + options.RequireHttpsMetadata = false; + options.Audience = "orders"; + }); + + return services; } } }