Modernizationpull/2058/head
@ -0,0 +1,62 @@ | |||
internal static class Extensions | |||
{ | |||
public static IServiceCollection AddReverseProxy(this IServiceCollection services, IConfiguration configuration) | |||
{ | |||
services.AddReverseProxy().LoadFromConfig(configuration.GetRequiredSection("ReverseProxy")); | |||
return services; | |||
} | |||
public static IServiceCollection AddHealthChecks(this IServiceCollection services, IConfiguration configuration) | |||
{ | |||
services.AddHealthChecks() | |||
.AddUrlGroup(_ => new Uri(configuration.GetRequiredValue("CatalogUrlHC")), name: "catalogapi-check", tags: new string[] { "catalogapi" }) | |||
.AddUrlGroup(_ => new Uri(configuration.GetRequiredValue("OrderingUrlHC")), name: "orderingapi-check", tags: new string[] { "orderingapi" }) | |||
.AddUrlGroup(_ => new Uri(configuration.GetRequiredValue("BasketUrlHC")), name: "basketapi-check", tags: new string[] { "basketapi" }) | |||
.AddUrlGroup(_ => new Uri(configuration.GetRequiredValue("IdentityUrlHC")), name: "identityapi-check", tags: new string[] { "identityapi" }); | |||
return services; | |||
} | |||
public static IServiceCollection AddApplicationServices(this IServiceCollection services) | |||
{ | |||
// Register delegating handlers | |||
services.AddTransient<HttpClientAuthorizationDelegatingHandler>(); | |||
// Register http services | |||
services.AddHttpClient<IOrderApiClient, OrderApiClient>() | |||
.AddHttpMessageHandler<HttpClientAuthorizationDelegatingHandler>(); | |||
return services; | |||
} | |||
public static IServiceCollection AddGrpcServices(this IServiceCollection services) | |||
{ | |||
services.AddTransient<GrpcExceptionInterceptor>(); | |||
services.AddScoped<IBasketService, BasketService>(); | |||
services.AddGrpcClient<Basket.BasketClient>((services, options) => | |||
{ | |||
var basketApi = services.GetRequiredService<IOptions<UrlsConfig>>().Value.GrpcBasket; | |||
options.Address = new Uri(basketApi); | |||
}).AddInterceptor<GrpcExceptionInterceptor>(); | |||
services.AddScoped<ICatalogService, CatalogService>(); | |||
services.AddGrpcClient<Catalog.CatalogClient>((services, options) => | |||
{ | |||
var catalogApi = services.GetRequiredService<IOptions<UrlsConfig>>().Value.GrpcCatalog; | |||
options.Address = new Uri(catalogApi); | |||
}).AddInterceptor<GrpcExceptionInterceptor>(); | |||
services.AddScoped<IOrderingService, OrderingService>(); | |||
services.AddGrpcClient<GrpcOrdering.OrderingGrpc.OrderingGrpcClient>((services, options) => | |||
{ | |||
var orderingApi = services.GetRequiredService<IOptions<UrlsConfig>>().Value.GrpcOrdering; | |||
options.Address = new Uri(orderingApi); | |||
}).AddInterceptor<GrpcExceptionInterceptor>(); | |||
return services; | |||
} | |||
} |
@ -1,33 +0,0 @@ | |||
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Filters | |||
{ | |||
namespace Basket.API.Infrastructure.Filters | |||
{ | |||
public class AuthorizeCheckOperationFilter : IOperationFilter | |||
{ | |||
public void Apply(OpenApiOperation operation, OperationFilterContext context) | |||
{ | |||
// Check for authorize attribute | |||
var hasAuthorize = context.MethodInfo.DeclaringType.GetCustomAttributes(true).OfType<AuthorizeAttribute>().Any() || | |||
context.MethodInfo.GetCustomAttributes(true).OfType<AuthorizeAttribute>().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<OpenApiSecurityRequirement> | |||
{ | |||
new() | |||
{ | |||
[ oAuthScheme ] = new [] { "Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator" } | |||
} | |||
}; | |||
} | |||
} | |||
} | |||
} |
@ -1,44 +0,0 @@ | |||
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator.Infrastructure; | |||
public class HttpClientAuthorizationDelegatingHandler : DelegatingHandler | |||
{ | |||
private readonly IHttpContextAccessor _httpContextAccessor; | |||
private readonly ILogger<HttpClientAuthorizationDelegatingHandler> _logger; | |||
public HttpClientAuthorizationDelegatingHandler(IHttpContextAccessor httpContextAccessor, ILogger<HttpClientAuthorizationDelegatingHandler> logger) | |||
{ | |||
_httpContextAccessor = httpContextAccessor; | |||
_logger = logger; | |||
} | |||
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) | |||
{ | |||
request.Version = new System.Version(2, 0); | |||
request.Method = HttpMethod.Get; | |||
var authorizationHeader = _httpContextAccessor.HttpContext | |||
.Request.Headers["Authorization"]; | |||
if (!string.IsNullOrEmpty(authorizationHeader)) | |||
{ | |||
request.Headers.Add("Authorization", new List<string>() { authorizationHeader }); | |||
} | |||
var token = await GetToken(); | |||
if (token != null) | |||
{ | |||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); | |||
} | |||
return await base.SendAsync(request, cancellationToken); | |||
} | |||
async Task<string> GetToken() | |||
{ | |||
const string ACCESS_TOKEN = "access_token"; | |||
return await _httpContextAccessor.HttpContext | |||
.GetTokenAsync(ACCESS_TOKEN); | |||
} | |||
} |
@ -1,23 +1,24 @@ | |||
await BuildWebHost(args).RunAsync(); | |||
IWebHost BuildWebHost(string[] args) => | |||
WebHost | |||
.CreateDefaultBuilder(args) | |||
.ConfigureAppConfiguration(cb => | |||
{ | |||
var sources = cb.Sources; | |||
sources.Insert(3, new Microsoft.Extensions.Configuration.Json.JsonConfigurationSource() | |||
{ | |||
Optional = true, | |||
Path = "appsettings.localhost.json", | |||
ReloadOnChange = false | |||
}); | |||
}) | |||
.UseStartup<Startup>() | |||
.UseSerilog((builderContext, config) => | |||
{ | |||
config | |||
.MinimumLevel.Information() | |||
.Enrich.FromLogContext() | |||
.WriteTo.Console(); | |||
}) | |||
.Build(); | |||
var builder = WebApplication.CreateBuilder(args); | |||
builder.AddServiceDefaults(); | |||
builder.Services.AddReverseProxy(builder.Configuration); | |||
builder.Services.AddControllers(); | |||
builder.Services.AddHealthChecks(builder.Configuration); | |||
builder.Services.AddApplicationServices(); | |||
builder.Services.AddGrpcServices(); | |||
builder.Services.Configure<UrlsConfig>(builder.Configuration.GetSection("urls")); | |||
var app = builder.Build(); | |||
app.UseServiceDefaults(); | |||
app.UseHttpsRedirection(); | |||
app.MapControllers(); | |||
app.MapReverseProxy(); | |||
await app.RunAsync(); |
@ -1,210 +0,0 @@ | |||
namespace Microsoft.eShopOnContainers.Mobile.Shopping.HttpAggregator; | |||
public class Startup | |||
{ | |||
public Startup(IConfiguration configuration) | |||
{ | |||
Configuration = configuration; | |||
} | |||
public IConfiguration Configuration { get; } | |||
// This method gets called by the runtime. Use this method to add services to the container. | |||
public void ConfigureServices(IServiceCollection services) | |||
{ | |||
services.AddHealthChecks() | |||
.AddCheck("self", () => HealthCheckResult.Healthy()) | |||
.AddUrlGroup(new Uri(Configuration["CatalogUrlHC"]), name: "catalogapi-check", tags: new string[] { "catalogapi" }) | |||
.AddUrlGroup(new Uri(Configuration["OrderingUrlHC"]), name: "orderingapi-check", tags: new string[] { "orderingapi" }) | |||
.AddUrlGroup(new Uri(Configuration["BasketUrlHC"]), name: "basketapi-check", tags: new string[] { "basketapi" }) | |||
.AddUrlGroup(new Uri(Configuration["IdentityUrlHC"]), name: "identityapi-check", tags: new string[] { "identityapi" }) | |||
.AddUrlGroup(new Uri(Configuration["PaymentUrlHC"]), name: "paymentapi-check", tags: new string[] { "paymentapi" }); | |||
services.AddCustomMvc(Configuration) | |||
.AddCustomAuthentication(Configuration) | |||
.AddHttpServices() | |||
.AddGrpcServices(); | |||
} | |||
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. | |||
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory) | |||
{ | |||
var pathBase = Configuration["PATH_BASE"]; | |||
if (!string.IsNullOrEmpty(pathBase)) | |||
{ | |||
loggerFactory.CreateLogger<Startup>().LogDebug("Using PATH BASE '{pathBase}'", pathBase); | |||
app.UsePathBase(pathBase); | |||
} | |||
if (env.IsDevelopment()) | |||
{ | |||
app.UseDeveloperExceptionPage(); | |||
} | |||
app.UseSwagger().UseSwaggerUI(c => | |||
{ | |||
c.SwaggerEndpoint($"{(!string.IsNullOrEmpty(pathBase) ? pathBase : string.Empty)}/swagger/v1/swagger.json", "Purchase BFF V1"); | |||
c.OAuthClientId("mobileshoppingaggswaggerui"); | |||
c.OAuthClientSecret(string.Empty); | |||
c.OAuthRealm(string.Empty); | |||
c.OAuthAppName("Purchase BFF Swagger UI"); | |||
}); | |||
app.UseRouting(); | |||
app.UseCors("CorsPolicy"); | |||
app.UseAuthentication(); | |||
app.UseAuthorization(); | |||
app.UseEndpoints(endpoints => | |||
{ | |||
endpoints.MapDefaultControllerRoute(); | |||
endpoints.MapControllers(); | |||
endpoints.MapHealthChecks("/hc", new HealthCheckOptions() | |||
{ | |||
Predicate = _ => true, | |||
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse | |||
}); | |||
endpoints.MapHealthChecks("/liveness", new HealthCheckOptions | |||
{ | |||
Predicate = r => r.Name.Contains("self") | |||
}); | |||
}); | |||
} | |||
} | |||
public static class ServiceCollectionExtensions | |||
{ | |||
public static IServiceCollection AddCustomMvc(this IServiceCollection services, IConfiguration configuration) | |||
{ | |||
services.AddOptions(); | |||
services.Configure<UrlsConfig>(configuration.GetSection("urls")); | |||
services.AddControllers() | |||
.AddJsonOptions(options => options.JsonSerializerOptions.WriteIndented = true); | |||
services.AddSwaggerGen(options => | |||
{ | |||
//options.DescribeAllEnumsAsStrings(); | |||
options.SwaggerDoc("v1", new OpenApiInfo | |||
{ | |||
Title = "Shopping Aggregator for Mobile Clients", | |||
Version = "v1", | |||
Description = "Shopping Aggregator for Mobile Clients" | |||
}); | |||
options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme | |||
{ | |||
Type = SecuritySchemeType.OAuth2, | |||
Flows = new OpenApiOAuthFlows() | |||
{ | |||
Implicit = new OpenApiOAuthFlow() | |||
{ | |||
AuthorizationUrl = new Uri($"{configuration.GetValue<string>("IdentityUrlExternal")}/connect/authorize"), | |||
TokenUrl = new Uri($"{configuration.GetValue<string>("IdentityUrlExternal")}/connect/token"), | |||
Scopes = new Dictionary<string, string>() | |||
{ | |||
{ "mobileshoppingagg", "Shopping Aggregator for Mobile Clients" } | |||
} | |||
} | |||
} | |||
}); | |||
options.OperationFilter<AuthorizeCheckOperationFilter>(); | |||
}); | |||
services.AddCors(options => | |||
{ | |||
options.AddPolicy("CorsPolicy", | |||
builder => builder | |||
.AllowAnyMethod() | |||
.AllowAnyHeader() | |||
.SetIsOriginAllowed((host) => true) | |||
.AllowCredentials()); | |||
}); | |||
return services; | |||
} | |||
public static IServiceCollection AddCustomAuthentication(this IServiceCollection services, IConfiguration configuration) | |||
{ | |||
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("sub"); | |||
var identityUrl = configuration.GetValue<string>("urls:identity"); | |||
services.AddAuthentication(options => | |||
{ | |||
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; | |||
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; | |||
}) | |||
.AddJwtBearer(options => | |||
{ | |||
options.Authority = identityUrl; | |||
options.RequireHttpsMetadata = false; | |||
options.Audience = "mobileshoppingagg"; | |||
options.TokenValidationParameters = new 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", "mobileshoppingagg"); | |||
}); | |||
}); | |||
return services; | |||
} | |||
public static IServiceCollection AddHttpServices(this IServiceCollection services) | |||
{ | |||
//register delegating handlers | |||
services.AddTransient<HttpClientAuthorizationDelegatingHandler>(); | |||
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>(); | |||
//register http services | |||
services.AddHttpClient<IOrderApiClient, OrderApiClient>(); | |||
return services; | |||
} | |||
public static IServiceCollection AddGrpcServices(this IServiceCollection services) | |||
{ | |||
services.AddTransient<GrpcExceptionInterceptor>(); | |||
services.AddScoped<IBasketService, BasketService>(); | |||
services.AddGrpcClient<Basket.BasketClient>((services, options) => | |||
{ | |||
var basketApi = services.GetRequiredService<IOptions<UrlsConfig>>().Value.GrpcBasket; | |||
options.Address = new Uri(basketApi); | |||
}).AddInterceptor<GrpcExceptionInterceptor>(); | |||
services.AddScoped<ICatalogService, CatalogService>(); | |||
services.AddGrpcClient<Catalog.CatalogClient>((services, options) => | |||
{ | |||
var catalogApi = services.GetRequiredService<IOptions<UrlsConfig>>().Value.GrpcCatalog; | |||
options.Address = new Uri(catalogApi); | |||
}).AddInterceptor<GrpcExceptionInterceptor>(); | |||
services.AddScoped<IOrderingService, OrderingService>(); | |||
services.AddGrpcClient<OrderingGrpc.OrderingGrpcClient>((services, options) => | |||
{ | |||
var orderingApi = services.GetRequiredService<IOptions<UrlsConfig>>().Value.GrpcOrdering; | |||
options.Address = new Uri(orderingApi); | |||
}).AddInterceptor<GrpcExceptionInterceptor>(); | |||
return services; | |||
} | |||
} |
@ -1,15 +1,138 @@ | |||
{ | |||
"Logging": { | |||
"IncludeScopes": false, | |||
"Debug": { | |||
"LogLevel": { | |||
"Default": "Warning" | |||
"LogLevel": { | |||
"Default": "Information", | |||
"Microsoft.AspNetCore": "Warning", | |||
"System.Net.Http": "Warning" | |||
} | |||
}, | |||
"OpenApi": { | |||
"Endpoint": { | |||
"Name": "Purchase BFF V1" | |||
}, | |||
"Document": { | |||
"Description": "Shopping Aggregator for Mobile Clients", | |||
"Title": "Shopping Aggregator for Mobile Clients", | |||
"Version": "v1" | |||
}, | |||
"Auth": { | |||
"ClientId": "mobileshoppingaggswaggerui", | |||
"AppName": "Mobile shopping BFF Swagger UI" | |||
} | |||
}, | |||
"Identity": { | |||
"Url": "http://localhost:5223", | |||
"Audience": "mobileshoppingagg", | |||
"Scopes": { | |||
"webshoppingagg": "Shopping Aggregator for Mobile Clients" | |||
} | |||
}, | |||
"ReverseProxy": { | |||
"Routes": { | |||
"c-short": { | |||
"ClusterId": "catalog", | |||
"Match": { | |||
"Path": "c/{**catch-all}" | |||
}, | |||
"Transforms": [ | |||
{ "PathRemovePrefix": "/c" } | |||
] | |||
}, | |||
"c-long": { | |||
"ClusterId": "catalog", | |||
"Match": { | |||
"Path": "catalog-api/{**catch-all}" | |||
}, | |||
"Transforms": [ | |||
{ "PathRemovePrefix": "/catalog-api" } | |||
] | |||
}, | |||
"b-short": { | |||
"ClusterId": "basket", | |||
"Match": { | |||
"Path": "b/{**catch-all}" | |||
}, | |||
"Transforms": [ | |||
{ "PathRemovePrefix": "/b" } | |||
] | |||
}, | |||
"b-long": { | |||
"ClusterId": "basket", | |||
"Match": { | |||
"Path": "basket-api/{**catch-all}" | |||
}, | |||
"Transforms": [ | |||
{ "PathRemovePrefix": "/basket-api" } | |||
] | |||
}, | |||
"o-short": { | |||
"ClusterId": "orders", | |||
"Match": { | |||
"Path": "o/{**catch-all}" | |||
}, | |||
"Transforms": [ | |||
{ "PathRemovePrefix": "/o" } | |||
] | |||
}, | |||
"o-long": { | |||
"ClusterId": "orders", | |||
"Match": { | |||
"Path": "ordering-api/{**catch-all}" | |||
}, | |||
"Transforms": [ | |||
{ "PathRemovePrefix": "/ordering-api" } | |||
] | |||
}, | |||
"h-long": { | |||
"ClusterId": "signalr", | |||
"Match": { | |||
"Path": "hub/notificationhub/{**catch-all}" | |||
} | |||
} | |||
}, | |||
"Console": { | |||
"LogLevel": { | |||
"Default": "Warning" | |||
"Clusters": { | |||
"basket": { | |||
"Destinations": { | |||
"destination0": { | |||
"Address": "http://localhost:5221" | |||
} | |||
} | |||
}, | |||
"catalog": { | |||
"Destinations": { | |||
"destination0": { | |||
"Address": "http://localhost:5222" | |||
} | |||
} | |||
}, | |||
"orders": { | |||
"Destinations": { | |||
"destination0": { | |||
"Address": "http://localhost:5224" | |||
} | |||
} | |||
}, | |||
"signalr": { | |||
"Destinations": { | |||
"destination0": { | |||
"Address": "http://localhost:5225" | |||
} | |||
} | |||
} | |||
} | |||
} | |||
}, | |||
"Urls": { | |||
"Basket": "http://localhost:5221", | |||
"Catalog": "http://localhost:5222", | |||
"Orders": "http://localhost:5224", | |||
"Identity": "http://localhost:5223", | |||
"Signalr": "http://localhost:5225", | |||
"GrpcBasket": "http://localhost:6221", | |||
"GrpcCatalog": "http://localhost:6222", | |||
"GrpcOrdering": "http://localhost:6224" | |||
}, | |||
"CatalogUrlHC": "http://localhost:5222/hc", | |||
"OrderingUrlHC": "http://localhost:5224/hc", | |||
"BasketUrlHC": "http://localhost:5221/hc", | |||
"IdentityUrlHC": "http://localhost:5223/hc" | |||
} |
@ -1,11 +0,0 @@ | |||
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Controllers; | |||
[Route("")] | |||
public class HomeController : Controller | |||
{ | |||
[HttpGet] | |||
public IActionResult Index() | |||
{ | |||
return new RedirectResult("~/swagger"); | |||
} | |||
} |
@ -0,0 +1,63 @@ | |||
internal static class Extensions | |||
{ | |||
public static IServiceCollection AddReverseProxy(this IServiceCollection services, IConfiguration configuration) | |||
{ | |||
services.AddReverseProxy().LoadFromConfig(configuration.GetRequiredSection("ReverseProxy")); | |||
return services; | |||
} | |||
public static IServiceCollection AddHealthChecks(this IServiceCollection services, IConfiguration configuration) | |||
{ | |||
services.AddHealthChecks() | |||
.AddUrlGroup(_ => new Uri(configuration.GetRequiredValue("CatalogUrlHC")), name: "catalogapi-check", tags: new string[] { "catalogapi" }) | |||
.AddUrlGroup(_ => new Uri(configuration.GetRequiredValue("OrderingUrlHC")), name: "orderingapi-check", tags: new string[] { "orderingapi" }) | |||
.AddUrlGroup(_ => new Uri(configuration.GetRequiredValue("BasketUrlHC")), name: "basketapi-check", tags: new string[] { "basketapi" }) | |||
.AddUrlGroup(_ => new Uri(configuration.GetRequiredValue("IdentityUrlHC")), name: "identityapi-check", tags: new string[] { "identityapi" }); | |||
return services; | |||
} | |||
public static IServiceCollection AddApplicationServices(this IServiceCollection services) | |||
{ | |||
// Register delegating handlers | |||
services.AddTransient<HttpClientAuthorizationDelegatingHandler>(); | |||
// Register http services | |||
services.AddHttpClient<IOrderApiClient, OrderApiClient>() | |||
.AddHttpMessageHandler<HttpClientAuthorizationDelegatingHandler>(); | |||
return services; | |||
} | |||
public static IServiceCollection AddGrpcServices(this IServiceCollection services) | |||
{ | |||
services.AddTransient<GrpcExceptionInterceptor>(); | |||
services.AddScoped<IBasketService, BasketService>(); | |||
services.AddGrpcClient<Basket.BasketClient>((services, options) => | |||
{ | |||
var basketApi = services.GetRequiredService<IOptions<UrlsConfig>>().Value.GrpcBasket; | |||
options.Address = new Uri(basketApi); | |||
}).AddInterceptor<GrpcExceptionInterceptor>(); | |||
services.AddScoped<ICatalogService, CatalogService>(); | |||
services.AddGrpcClient<Catalog.CatalogClient>((services, options) => | |||
{ | |||
var catalogApi = services.GetRequiredService<IOptions<UrlsConfig>>().Value.GrpcCatalog; | |||
options.Address = new Uri(catalogApi); | |||
}).AddInterceptor<GrpcExceptionInterceptor>(); | |||
services.AddScoped<IOrderingService, OrderingService>(); | |||
services.AddGrpcClient<GrpcOrdering.OrderingGrpc.OrderingGrpcClient>((services, options) => | |||
{ | |||
var orderingApi = services.GetRequiredService<IOptions<UrlsConfig>>().Value.GrpcOrdering; | |||
options.Address = new Uri(orderingApi); | |||
}).AddInterceptor<GrpcExceptionInterceptor>(); | |||
return services; | |||
} | |||
} |
@ -1,34 +0,0 @@ | |||
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Filters | |||
{ | |||
namespace Basket.API.Infrastructure.Filters | |||
{ | |||
public class AuthorizeCheckOperationFilter : IOperationFilter | |||
{ | |||
public void Apply(OpenApiOperation operation, OperationFilterContext context) | |||
{ | |||
// Check for authorize attribute | |||
var hasAuthorize = context.MethodInfo.DeclaringType.GetCustomAttributes(true).OfType<AuthorizeAttribute>().Any() || | |||
context.MethodInfo.GetCustomAttributes(true).OfType<AuthorizeAttribute>().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<OpenApiSecurityRequirement> | |||
{ | |||
new() | |||
{ | |||
[ oAuthScheme ] = new[] { "Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator" } | |||
} | |||
}; | |||
} | |||
} | |||
} | |||
} |
@ -1,41 +1,27 @@ | |||
global using CatalogApi; | |||
global using Grpc.Core.Interceptors; | |||
global using System; | |||
global using System.Collections.Generic; | |||
global using System.Linq; | |||
global using System.Net; | |||
global using System.Net.Http; | |||
global using System.Net.Http.Headers; | |||
global using System.Text.Json; | |||
global using System.Threading; | |||
global using System.Threading.Tasks; | |||
global using CatalogApi; | |||
global using Grpc.Core; | |||
global using Grpc.Core.Interceptors; | |||
global using GrpcBasket; | |||
global using GrpcOrdering; | |||
global using HealthChecks.UI.Client; | |||
global using Microsoft.AspNetCore.Authentication; | |||
global using Microsoft.AspNetCore.Authorization; | |||
global using Microsoft.AspNetCore.Builder; | |||
global using Microsoft.AspNetCore.Diagnostics.HealthChecks; | |||
global using Microsoft.AspNetCore.Hosting; | |||
global using Microsoft.AspNetCore.Http; | |||
global using Microsoft.AspNetCore.Mvc; | |||
global using Microsoft.AspNetCore; | |||
global using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Config; | |||
global using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Filters.Basket.API.Infrastructure.Filters; | |||
global using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Infrastructure; | |||
global using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models; | |||
global using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Services; | |||
global using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator; | |||
global using Microsoft.Extensions.Configuration; | |||
global using Microsoft.Extensions.DependencyInjection; | |||
global using Microsoft.Extensions.Diagnostics.HealthChecks; | |||
global using Microsoft.Extensions.Hosting; | |||
global using Microsoft.Extensions.Logging; | |||
global using Microsoft.Extensions.Options; | |||
global using Microsoft.OpenApi.Models; | |||
global using Serilog; | |||
global using Swashbuckle.AspNetCore.SwaggerGen; | |||
global using System.Collections.Generic; | |||
global using System.IdentityModel.Tokens.Jwt; | |||
global using System.Linq; | |||
global using System.Net.Http.Headers; | |||
global using System.Net.Http; | |||
global using System.Net; | |||
global using System.Text.Json; | |||
global using System.Threading.Tasks; | |||
global using System.Threading; | |||
global using System; | |||
global using Microsoft.IdentityModel.Tokens; | |||
global using Serilog.Context; | |||
global using Services.Common; |
@ -1,40 +0,0 @@ | |||
namespace Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Infrastructure; | |||
public class HttpClientAuthorizationDelegatingHandler | |||
: DelegatingHandler | |||
{ | |||
private readonly IHttpContextAccessor _httpContextAccessor; | |||
public HttpClientAuthorizationDelegatingHandler(IHttpContextAccessor httpContextAccessor) | |||
{ | |||
_httpContextAccessor = httpContextAccessor; | |||
} | |||
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) | |||
{ | |||
var authorizationHeader = _httpContextAccessor.HttpContext | |||
.Request.Headers["Authorization"]; | |||
if (!string.IsNullOrWhiteSpace(authorizationHeader)) | |||
{ | |||
request.Headers.Add("Authorization", new List<string>() { authorizationHeader }); | |||
} | |||
var token = await GetTokenAsync(); | |||
if (token != null) | |||
{ | |||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); | |||
} | |||
return await base.SendAsync(request, cancellationToken); | |||
} | |||
Task<string> GetTokenAsync() | |||
{ | |||
const string ACCESS_TOKEN = "access_token"; | |||
return _httpContextAccessor.HttpContext | |||
.GetTokenAsync(ACCESS_TOKEN); | |||
} | |||
} |
@ -1,208 +1,38 @@ | |||
var appName = "Web.Shopping.HttpAggregator"; | |||
var builder = WebApplication.CreateBuilder(args); | |||
var builder = WebApplication.CreateBuilder(args); | |||
builder.Host.UseSerilog(CreateSerilogLogger(builder.Configuration)); | |||
builder.Services.AddHealthChecks() | |||
.AddCheck("self", () => HealthCheckResult.Healthy()) | |||
.AddUrlGroup(new Uri(builder.Configuration["CatalogUrlHC"]), name: "catalogapi-check", tags: new string[] { "catalogapi" }) | |||
.AddUrlGroup(new Uri(builder.Configuration["OrderingUrlHC"]), name: "orderingapi-check", tags: new string[] { "orderingapi" }) | |||
.AddUrlGroup(new Uri(builder.Configuration["BasketUrlHC"]), name: "basketapi-check", tags: new string[] { "basketapi" }) | |||
.AddUrlGroup(new Uri(builder.Configuration["IdentityUrlHC"]), name: "identityapi-check", tags: new string[] { "identityapi" }) | |||
.AddUrlGroup(new Uri(builder.Configuration["PaymentUrlHC"]), name: "paymentapi-check", tags: new string[] { "paymentapi" }); | |||
builder.Services.AddCustomMvc(builder.Configuration) | |||
.AddCustomAuthentication(builder.Configuration) | |||
.AddApplicationServices() | |||
.AddGrpcServices(); | |||
var app = builder.Build(); | |||
if (app.Environment.IsDevelopment()) | |||
{ | |||
app.UseDeveloperExceptionPage(); | |||
} | |||
else | |||
{ | |||
app.UseExceptionHandler("/Home/Error"); | |||
} | |||
var pathBase = builder.Configuration["PATH_BASE"]; | |||
if (!string.IsNullOrEmpty(pathBase)) | |||
{ | |||
app.UsePathBase(pathBase); | |||
} | |||
builder.AddServiceDefaults(); | |||
app.UseHttpsRedirection(); | |||
builder.Services.AddReverseProxy(builder.Configuration); | |||
builder.Services.AddControllers(); | |||
app.UseSwagger().UseSwaggerUI(c => | |||
builder.Services.AddHealthChecks(builder.Configuration); | |||
builder.Services.AddCors(options => | |||
{ | |||
c.SwaggerEndpoint($"{(!string.IsNullOrEmpty(pathBase) ? pathBase : string.Empty)}/swagger/v1/swagger.json", "Purchase BFF V1"); | |||
c.OAuthClientId("webshoppingaggswaggerui"); | |||
c.OAuthClientSecret(string.Empty); | |||
c.OAuthRealm(string.Empty); | |||
c.OAuthAppName("web shopping bff Swagger UI"); | |||
// TODO: Read allowed origins from configuration | |||
options.AddPolicy("CorsPolicy", | |||
builder => builder | |||
.SetIsOriginAllowed((host) => true) | |||
.AllowAnyMethod() | |||
.AllowAnyHeader() | |||
.AllowCredentials()); | |||
}); | |||
app.UseRouting(); | |||
app.UseCors("CorsPolicy"); | |||
app.UseAuthentication(); | |||
app.UseAuthorization(); | |||
app.MapDefaultControllerRoute(); | |||
app.MapControllers(); | |||
app.MapHealthChecks("/hc", new HealthCheckOptions() | |||
{ | |||
Predicate = _ => true, | |||
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse | |||
}); | |||
app.MapHealthChecks("/liveness", new HealthCheckOptions | |||
{ | |||
Predicate = r => r.Name.Contains("self") | |||
}); | |||
try | |||
{ | |||
Log.Information("Starts Web Application ({ApplicationContext})...", Program.AppName); | |||
await app.RunAsync(); | |||
return 0; | |||
} | |||
catch (Exception ex) | |||
{ | |||
Log.Fatal(ex, "Program terminated unexpectedly ({ApplicationContext})!", Program.AppName); | |||
return 1; | |||
} | |||
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() | |||
.ReadFrom.Configuration(configuration) | |||
.CreateLogger(); | |||
} | |||
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 ServiceCollectionExtensions | |||
{ | |||
public static IServiceCollection AddCustomAuthentication(this IServiceCollection services, IConfiguration configuration) | |||
{ | |||
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("sub"); | |||
var identityUrl = configuration.GetValue<string>("urls:identity"); | |||
services.AddAuthentication("Bearer") | |||
.AddJwtBearer(options => | |||
{ | |||
options.Authority = identityUrl; | |||
options.RequireHttpsMetadata = false; | |||
options.Audience = "webshoppingagg"; | |||
options.TokenValidationParameters = new TokenValidationParameters | |||
{ | |||
ValidateAudience = false | |||
}; | |||
}); | |||
return services; | |||
} | |||
public static IServiceCollection AddCustomMvc(this IServiceCollection services, IConfiguration configuration) | |||
{ | |||
services.AddOptions(); | |||
services.Configure<UrlsConfig>(configuration.GetSection("urls")); | |||
services.AddControllers() | |||
.AddJsonOptions(options => options.JsonSerializerOptions.WriteIndented = true); | |||
services.AddSwaggerGen(options => | |||
{ | |||
options.SwaggerDoc("v1", new OpenApiInfo | |||
{ | |||
Title = "Shopping Aggregator for Web Clients", | |||
Version = "v1", | |||
Description = "Shopping Aggregator for Web Clients" | |||
}); | |||
options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme | |||
{ | |||
Type = SecuritySchemeType.OAuth2, | |||
Flows = new OpenApiOAuthFlows() | |||
{ | |||
Implicit = new OpenApiOAuthFlow() | |||
{ | |||
AuthorizationUrl = new Uri($"{configuration.GetValue<string>("IdentityUrlExternal")}/connect/authorize"), | |||
TokenUrl = new Uri($"{configuration.GetValue<string>("IdentityUrlExternal")}/connect/token"), | |||
Scopes = new Dictionary<string, string>() | |||
{ | |||
{ "webshoppingagg", "Shopping Aggregator for Web Clients" } | |||
} | |||
} | |||
} | |||
}); | |||
options.OperationFilter<AuthorizeCheckOperationFilter>(); | |||
}); | |||
services.AddCors(options => | |||
{ | |||
options.AddPolicy("CorsPolicy", | |||
builder => builder | |||
.SetIsOriginAllowed((host) => true) | |||
.AllowAnyMethod() | |||
.AllowAnyHeader() | |||
.AllowCredentials()); | |||
}); | |||
return services; | |||
} | |||
public static IServiceCollection AddApplicationServices(this IServiceCollection services) | |||
{ | |||
//register delegating handlers | |||
services.AddTransient<HttpClientAuthorizationDelegatingHandler>(); | |||
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>(); | |||
builder.Services.AddApplicationServices(); | |||
builder.Services.AddGrpcServices(); | |||
//register http services | |||
builder.Services.Configure<UrlsConfig>(builder.Configuration.GetSection("urls")); | |||
services.AddHttpClient<IOrderApiClient, OrderApiClient>() | |||
.AddHttpMessageHandler<HttpClientAuthorizationDelegatingHandler>(); | |||
return services; | |||
} | |||
public static IServiceCollection AddGrpcServices(this IServiceCollection services) | |||
{ | |||
services.AddTransient<GrpcExceptionInterceptor>(); | |||
services.AddScoped<IBasketService, BasketService>(); | |||
services.AddGrpcClient<Basket.BasketClient>((services, options) => | |||
{ | |||
var basketApi = services.GetRequiredService<IOptions<UrlsConfig>>().Value.GrpcBasket; | |||
options.Address = new Uri(basketApi); | |||
}).AddInterceptor<GrpcExceptionInterceptor>(); | |||
var app = builder.Build(); | |||
services.AddScoped<ICatalogService, CatalogService>(); | |||
app.UseServiceDefaults(); | |||
services.AddGrpcClient<Catalog.CatalogClient>((services, options) => | |||
{ | |||
var catalogApi = services.GetRequiredService<IOptions<UrlsConfig>>().Value.GrpcCatalog; | |||
options.Address = new Uri(catalogApi); | |||
}).AddInterceptor<GrpcExceptionInterceptor>(); | |||
app.UseHttpsRedirection(); | |||
services.AddScoped<IOrderingService, OrderingService>(); | |||
app.UseCors("CorsPolicy"); | |||
app.UseAuthentication(); | |||
app.UseAuthorization(); | |||
services.AddGrpcClient<OrderingGrpc.OrderingGrpcClient>((services, options) => | |||
{ | |||
var orderingApi = services.GetRequiredService<IOptions<UrlsConfig>>().Value.GrpcOrdering; | |||
options.Address = new Uri(orderingApi); | |||
}).AddInterceptor<GrpcExceptionInterceptor>(); | |||
app.MapControllers(); | |||
app.MapReverseProxy(); | |||
return services; | |||
} | |||
} | |||
await app.RunAsync(); |
@ -1,29 +1,12 @@ | |||
{ | |||
"iisSettings": { | |||
"windowsAuthentication": false, | |||
"anonymousAuthentication": true, | |||
"iisExpress": { | |||
"applicationUrl": "http://localhost:57425/", | |||
"sslPort": 0 | |||
} | |||
}, | |||
"profiles": { | |||
"IIS Express": { | |||
"commandName": "IISExpress", | |||
"launchBrowser": true, | |||
"launchUrl": "api/values", | |||
"environmentVariables": { | |||
"ASPNETCORE_ENVIRONMENT": "Development" | |||
} | |||
}, | |||
"PurchaseForMvc": { | |||
"Web.Shopping.HttpAggregator": { | |||
"commandName": "Project", | |||
"launchBrowser": true, | |||
"launchUrl": "api/values", | |||
"applicationUrl": "http://localhost:5229/", | |||
"environmentVariables": { | |||
"ASPNETCORE_ENVIRONMENT": "Development" | |||
}, | |||
"applicationUrl": "http://localhost:61632/" | |||
} | |||
} | |||
} | |||
} |
@ -1,15 +1,8 @@ | |||
{ | |||
"Logging": { | |||
"IncludeScopes": false, | |||
"Debug": { | |||
"LogLevel": { | |||
"Default": "Debug" | |||
} | |||
}, | |||
"Console": { | |||
"LogLevel": { | |||
"Default": "Debug" | |||
} | |||
"LogLevel": { | |||
"Default": "Information", | |||
"Microsoft.AspNetCore": "Warning" | |||
} | |||
} | |||
} |
@ -1,15 +1,138 @@ | |||
{ | |||
"Logging": { | |||
"IncludeScopes": false, | |||
"Debug": { | |||
"LogLevel": { | |||
"Default": "Warning" | |||
"LogLevel": { | |||
"Default": "Information", | |||
"Microsoft.AspNetCore": "Warning", | |||
"System.Net.Http": "Warning" | |||
} | |||
}, | |||
"OpenApi": { | |||
"Endpoint": { | |||
"Name": "Purchase BFF V1" | |||
}, | |||
"Document": { | |||
"Description": "Shopping Aggregator for Web Clients", | |||
"Title": "Shopping Aggregator for Web Clients", | |||
"Version": "v1" | |||
}, | |||
"Auth": { | |||
"ClientId": "webshoppingaggswaggerui", | |||
"AppName": "Web Shopping BFF Swagger UI" | |||
} | |||
}, | |||
"Identity": { | |||
"Url": "http://localhost:5223", | |||
"Audience": "webshoppingagg", | |||
"Scopes": { | |||
"webshoppingagg": "Shopping Aggregator for Web Clients" | |||
} | |||
}, | |||
"ReverseProxy": { | |||
"Routes": { | |||
"c-short": { | |||
"ClusterId": "catalog", | |||
"Match": { | |||
"Path": "c/{**catch-all}" | |||
}, | |||
"Transforms": [ | |||
{ "PathRemovePrefix": "/c" } | |||
] | |||
}, | |||
"c-long": { | |||
"ClusterId": "catalog", | |||
"Match": { | |||
"Path": "catalog-api/{**catch-all}" | |||
}, | |||
"Transforms": [ | |||
{ "PathRemovePrefix": "/catalog-api" } | |||
] | |||
}, | |||
"b-short": { | |||
"ClusterId": "basket", | |||
"Match": { | |||
"Path": "b/{**catch-all}" | |||
}, | |||
"Transforms": [ | |||
{ "PathRemovePrefix": "/b" } | |||
] | |||
}, | |||
"b-long": { | |||
"ClusterId": "basket", | |||
"Match": { | |||
"Path": "basket-api/{**catch-all}" | |||
}, | |||
"Transforms": [ | |||
{ "PathRemovePrefix": "/basket-api" } | |||
] | |||
}, | |||
"o-short": { | |||
"ClusterId": "orders", | |||
"Match": { | |||
"Path": "o/{**catch-all}" | |||
}, | |||
"Transforms": [ | |||
{ "PathRemovePrefix": "/o" } | |||
] | |||
}, | |||
"o-long": { | |||
"ClusterId": "orders", | |||
"Match": { | |||
"Path": "ordering-api/{**catch-all}" | |||
}, | |||
"Transforms": [ | |||
{ "PathRemovePrefix": "/ordering-api" } | |||
] | |||
}, | |||
"h-long": { | |||
"ClusterId": "signalr", | |||
"Match": { | |||
"Path": "hub/notificationhub/{**catch-all}" | |||
} | |||
} | |||
}, | |||
"Console": { | |||
"LogLevel": { | |||
"Default": "Warning" | |||
"Clusters": { | |||
"basket": { | |||
"Destinations": { | |||
"destination0": { | |||
"Address": "http://localhost:5221" | |||
} | |||
} | |||
}, | |||
"catalog": { | |||
"Destinations": { | |||
"destination0": { | |||
"Address": "http://localhost:5222" | |||
} | |||
} | |||
}, | |||
"orders": { | |||
"Destinations": { | |||
"destination0": { | |||
"Address": "http://localhost:5224" | |||
} | |||
} | |||
}, | |||
"signalr": { | |||
"Destinations": { | |||
"destination0": { | |||
"Address": "http://localhost:5225" | |||
} | |||
} | |||
} | |||
} | |||
} | |||
}, | |||
"Urls": { | |||
"Basket": "http://localhost:5221", | |||
"Catalog": "http://localhost:5222", | |||
"Orders": "http://localhost:5224", | |||
"Identity": "http://localhost:5223", | |||
"Signalr": "http://localhost:5225", | |||
"GrpcBasket": "http://localhost:6221", | |||
"GrpcCatalog": "http://localhost:6222", | |||
"GrpcOrdering": "http://localhost:6224" | |||
}, | |||
"CatalogUrlHC": "http://localhost:5222/hc", | |||
"OrderingUrlHC": "http://localhost:5224/hc", | |||
"BasketUrlHC": "http://localhost:5221/hc", | |||
"IdentityUrlHC": "http://localhost:5223/hc" | |||
} |
@ -1,11 +0,0 @@ | |||
{ | |||
"urls": { | |||
"basket": "http://localhost:55103", | |||
"catalog": "http://localhost:55101", | |||
"orders": "http://localhost:55102", | |||
"identity": "http://localhost:55105", | |||
"grpcBasket": "http://localhost:5580", | |||
"grpcCatalog": "http://localhost:81", | |||
"grpcOrdering": "http://localhost:5581" | |||
} | |||
} |
@ -1,28 +0,0 @@ | |||
(function ($, swaggerUi) { | |||
$(function () { | |||
var settings = { | |||
authority: 'https://localhost:5105', | |||
client_id: 'js', | |||
popup_redirect_uri: window.location.protocol | |||
+ '//' | |||
+ window.location.host | |||
+ '/tokenclient/popup.html', | |||
response_type: 'id_token token', | |||
scope: 'openid profile basket', | |||
filter_protocol_claims: true | |||
}, | |||
manager = new OidcTokenManager(settings), | |||
$inputApiKey = $('#input_apiKey'); | |||
$inputApiKey.on('dblclick', function () { | |||
manager.openPopupForTokenAsync() | |||
.then(function () { | |||
$inputApiKey.val(manager.access_token).change(); | |||
}, function (error) { | |||
console.error(error); | |||
}); | |||
}); | |||
}); | |||
})(jQuery, window.swaggerUi); |
@ -1,13 +0,0 @@ | |||
<!DOCTYPE html> | |||
<html> | |||
<head> | |||
<title></title> | |||
<meta charset="utf-8" /> | |||
</head> | |||
<body> | |||
<script type="text/javascript" src="oidc-token-manager.min.js"></script> | |||
<script type="text/javascript"> | |||
new OidcTokenManager().processTokenPopup(); | |||
</script> | |||
</body> | |||
</html> |
@ -1,25 +0,0 @@ | |||
namespace Microsoft.eShopOnContainers.Services.Basket.API.Auth.Server; | |||
public class AuthorizationHeaderParameterOperationFilter : IOperationFilter | |||
{ | |||
public void Apply(OpenApiOperation operation, OperationFilterContext context) | |||
{ | |||
var filterPipeline = context.ApiDescription.ActionDescriptor.FilterDescriptors; | |||
var isAuthorized = filterPipeline.Select(filterInfo => filterInfo.Filter).Any(filter => filter is AuthorizeFilter); | |||
var allowAnonymous = filterPipeline.Select(filterInfo => filterInfo.Filter).Any(filter => filter is IAllowAnonymousFilter); | |||
if (isAuthorized && !allowAnonymous) | |||
{ | |||
operation.Parameters ??= new List<OpenApiParameter>(); | |||
operation.Parameters.Add(new OpenApiParameter | |||
{ | |||
Name = "Authorization", | |||
In = ParameterLocation.Header, | |||
Description = "access token", | |||
Required = true | |||
}); | |||
} | |||
} | |||
} |
@ -1,59 +1,25 @@ | |||
<Project Sdk="Microsoft.NET.Sdk.Web"> | |||
<PropertyGroup> | |||
<TargetFramework>net7.0</TargetFramework> | |||
<AssetTargetFallback>$(AssetTargetFallback);portable-net45+win8+wp8+wpa81;</AssetTargetFallback> | |||
<DockerComposeProjectPath>..\..\..\..\docker-compose.dcproj</DockerComposeProjectPath> | |||
<GenerateErrorForMissingTargetingPacks>false</GenerateErrorForMissingTargetingPacks> | |||
<IsTransformWebConfigDisabled>true</IsTransformWebConfigDisabled> | |||
</PropertyGroup> | |||
<PropertyGroup> | |||
<TargetFramework>net7.0</TargetFramework> | |||
<DockerComposeProjectPath>..\..\..\..\docker-compose.dcproj</DockerComposeProjectPath> | |||
<UserSecretsId>2964ec8e-0d48-4541-b305-94cab537f867</UserSecretsId> | |||
</PropertyGroup> | |||
<ItemGroup> | |||
<Content Update="web.config"> | |||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory> | |||
</Content> | |||
</ItemGroup> | |||
<ItemGroup> | |||
<PackageReference Include="Grpc.AspNetCore" /> | |||
<PackageReference Include="AspNetCore.HealthChecks.Redis" /> | |||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" /> | |||
</ItemGroup> | |||
<ItemGroup> | |||
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" /> | |||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" /> | |||
<PackageReference Include="AspNetCore.HealthChecks.AzureServiceBus" /> | |||
<PackageReference Include="AspNetCore.HealthChecks.Rabbitmq" /> | |||
<PackageReference Include="AspNetCore.HealthChecks.Redis" /> | |||
<PackageReference Include="AspNetCore.HealthChecks.UI.Client" /> | |||
<PackageReference Include="Autofac.Extensions.DependencyInjection" /> | |||
<PackageReference Include="Azure.Extensions.AspNetCore.Configuration.Secrets" /> | |||
<PackageReference Include="Azure.Identity" /> | |||
<PackageReference Include="Google.Protobuf" /> | |||
<PackageReference Include="Grpc.AspNetCore.Server" /> | |||
<PackageReference Include="Grpc.Tools" PrivateAssets="All" /> | |||
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" /> | |||
<PackageReference Include="Microsoft.ApplicationInsights.DependencyCollector" /> | |||
<PackageReference Include="Microsoft.ApplicationInsights.Kubernetes" /> | |||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" /> | |||
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.HealthChecks" /> | |||
<PackageReference Include="Microsoft.AspNetCore.HealthChecks" /> | |||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" /> | |||
<PackageReference Include="Microsoft.Extensions.Logging.AzureAppServices" /> | |||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" /> | |||
<PackageReference Include="Serilog.AspNetCore" /> | |||
<PackageReference Include="Serilog.Enrichers.Environment" /> | |||
<PackageReference Include="Serilog.Settings.Configuration" /> | |||
<PackageReference Include="Serilog.Sinks.Console" /> | |||
<PackageReference Include="Serilog.Sinks.Http" /> | |||
<PackageReference Include="Serilog.Sinks.Seq" /> | |||
<PackageReference Include="Swashbuckle.AspNetCore" /> | |||
<PackageReference Include="Swashbuckle.AspNetCore.Newtonsoft" /> | |||
</ItemGroup> | |||
<ItemGroup> | |||
<Protobuf Include="Proto\basket.proto" GrpcServices="Server" /> | |||
</ItemGroup> | |||
<ItemGroup> | |||
<Protobuf Include="Proto\basket.proto" GrpcServices="Server" Generator="MSBuild:Compile" /> | |||
<Content Include="@(Protobuf)" /> | |||
<None Remove="@(Protobuf)" /> | |||
</ItemGroup> | |||
<ItemGroup> | |||
<ProjectReference Include="..\..\Services.Common\Services.Common.csproj" /> | |||
</ItemGroup> | |||
<ItemGroup> | |||
<ProjectReference Include="..\..\..\BuildingBlocks\EventBus\EventBusRabbitMQ\EventBusRabbitMQ.csproj" /> | |||
<ProjectReference Include="..\..\..\BuildingBlocks\EventBus\EventBusServiceBus\EventBusServiceBus.csproj" /> | |||
<ProjectReference Include="..\..\..\BuildingBlocks\EventBus\EventBus\EventBus.csproj" /> | |||
</ItemGroup> | |||
<ItemGroup> | |||
<InternalsVisibleTo Include="Basket.FunctionalTests" /> | |||
</ItemGroup> | |||
</Project> |
@ -1,11 +0,0 @@ | |||
namespace Microsoft.eShopOnContainers.Services.Basket.API.Controllers; | |||
public class HomeController : Controller | |||
{ | |||
// GET: /<controller>/ | |||
public IActionResult Index() | |||
{ | |||
return new RedirectResult("~/swagger"); | |||
} | |||
} | |||
@ -1,37 +0,0 @@ | |||
namespace Microsoft.eShopOnContainers.Services.Basket.API; | |||
public static class CustomExtensionMethods | |||
{ | |||
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<bool>("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; | |||
} | |||
} |
@ -0,0 +1,20 @@ | |||
public static class Extensions | |||
{ | |||
public static IServiceCollection AddHealthChecks(this IServiceCollection services, IConfiguration configuration) | |||
{ | |||
services.AddHealthChecks() | |||
.AddRedis(_ => configuration.GetRequiredConnectionString("redis"), "redis", tags: new[] { "ready", "liveness" }); | |||
return services; | |||
} | |||
public static IServiceCollection AddRedis(this IServiceCollection services, IConfiguration configuration) | |||
{ | |||
return services.AddSingleton(sp => | |||
{ | |||
var redisConfig = ConfigurationOptions.Parse(configuration.GetRequiredConnectionString("redis"), true); | |||
return ConnectionMultiplexer.Connect(redisConfig); | |||
}); | |||
} | |||
} |
@ -1,59 +1,29 @@ | |||
global using Autofac.Extensions.DependencyInjection; | |||
global using Autofac; | |||
global using Azure.Core; | |||
global using Azure.Identity; | |||
global using Basket.API.Infrastructure.ActionResults; | |||
global using Basket.API.Infrastructure.Exceptions; | |||
global using Basket.API.Infrastructure.Filters; | |||
global using Basket.API.Infrastructure.Middlewares; | |||
global using System; | |||
global using System.Collections.Generic; | |||
global using System.ComponentModel.DataAnnotations; | |||
global using System.Linq; | |||
global using System.Net; | |||
global using System.Security.Claims; | |||
global using System.Text.Json; | |||
global using System.Threading.Tasks; | |||
global using Basket.API.IntegrationEvents.EventHandling; | |||
global using Basket.API.IntegrationEvents.Events; | |||
global using Basket.API.Model; | |||
global using Basket.API.Repositories; | |||
global using Grpc.Core; | |||
global using GrpcBasket; | |||
global using HealthChecks.UI.Client; | |||
global using Microsoft.AspNetCore.Authorization; | |||
global using Microsoft.AspNetCore.Builder; | |||
global using Microsoft.AspNetCore.Diagnostics.HealthChecks; | |||
global using Microsoft.AspNetCore.Hosting; | |||
global using Microsoft.AspNetCore.Http.Features; | |||
global using Microsoft.AspNetCore.Http; | |||
global using Microsoft.AspNetCore.Mvc.Authorization; | |||
global using Microsoft.AspNetCore.Mvc.Filters; | |||
global using Microsoft.AspNetCore.Mvc; | |||
global using Microsoft.AspNetCore.Server.Kestrel.Core; | |||
global using Microsoft.AspNetCore; | |||
global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; | |||
global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; | |||
global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus; | |||
global using Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ; | |||
global using Microsoft.eShopOnContainers.BuildingBlocks.EventBusServiceBus; | |||
global using Microsoft.eShopOnContainers.Services.Basket.API.Controllers; | |||
global using Microsoft.eShopOnContainers.Services.Basket.API.Infrastructure.Repositories; | |||
global using Microsoft.eShopOnContainers.Services.Basket.API.IntegrationEvents.EventHandling; | |||
global using Microsoft.eShopOnContainers.Services.Basket.API.IntegrationEvents.Events; | |||
global using Microsoft.eShopOnContainers.Services.Basket.API.Model; | |||
global using Microsoft.eShopOnContainers.Services.Basket.API.Services; | |||
global using Microsoft.eShopOnContainers.Services.Basket.API; | |||
global using Microsoft.Extensions.Configuration; | |||
global using Microsoft.Extensions.DependencyInjection; | |||
global using Microsoft.Extensions.Diagnostics.HealthChecks; | |||
global using Microsoft.Extensions.Hosting; | |||
global using Microsoft.Extensions.Logging; | |||
global using Microsoft.Extensions.Options; | |||
global using Microsoft.OpenApi.Models; | |||
global using RabbitMQ.Client; | |||
global using Serilog.Context; | |||
global using Serilog; | |||
global using Services.Common; | |||
global using StackExchange.Redis; | |||
global using Swashbuckle.AspNetCore.SwaggerGen; | |||
global using System.Collections.Generic; | |||
global using System.ComponentModel.DataAnnotations; | |||
global using System.IdentityModel.Tokens.Jwt; | |||
global using System.IO; | |||
global using System.Linq; | |||
global using System.Net; | |||
global using System.Security.Claims; | |||
global using System.Text.Json; | |||
global using System.Threading.Tasks; | |||
global using System; |
@ -1,11 +0,0 @@ | |||
namespace Basket.API.Infrastructure.ActionResults; | |||
public class InternalServerErrorObjectResult : ObjectResult | |||
{ | |||
public InternalServerErrorObjectResult(object error) | |||
: base(error) | |||
{ | |||
StatusCode = StatusCodes.Status500InternalServerError; | |||
} | |||
} | |||
@ -1,16 +0,0 @@ | |||
namespace Basket.API.Infrastructure.Exceptions; | |||
public class BasketDomainException : Exception | |||
{ | |||
public BasketDomainException() | |||
{ } | |||
public BasketDomainException(string message) | |||
: base(message) | |||
{ } | |||
public BasketDomainException(string message, Exception innerException) | |||
: base(message, innerException) | |||
{ } | |||
} | |||
@ -1,18 +0,0 @@ | |||
namespace Basket.API.Infrastructure.Middlewares; | |||
public static class FailingMiddlewareAppBuilderExtensions | |||
{ | |||
public static IApplicationBuilder UseFailingMiddleware(this IApplicationBuilder builder) | |||
{ | |||
return UseFailingMiddleware(builder, null); | |||
} | |||
public static IApplicationBuilder UseFailingMiddleware(this IApplicationBuilder builder, Action<FailingOptions> action) | |||
{ | |||
var options = new FailingOptions(); | |||
action?.Invoke(options); | |||
builder.UseMiddleware<FailingMiddleware>(options); | |||
return builder; | |||
} | |||
} | |||
@ -1,47 +0,0 @@ | |||
namespace Basket.API.Infrastructure.Filters; | |||
public partial class HttpGlobalExceptionFilter : IExceptionFilter | |||
{ | |||
private readonly IWebHostEnvironment env; | |||
private readonly ILogger<HttpGlobalExceptionFilter> logger; | |||
public HttpGlobalExceptionFilter(IWebHostEnvironment env, ILogger<HttpGlobalExceptionFilter> logger) | |||
{ | |||
this.env = env; | |||
this.logger = logger; | |||
} | |||
public void OnException(ExceptionContext context) | |||
{ | |||
logger.LogError(new EventId(context.Exception.HResult), | |||
context.Exception, | |||
context.Exception.Message); | |||
if (context.Exception.GetType() == typeof(BasketDomainException)) | |||
{ | |||
var json = new JsonErrorResponse | |||
{ | |||
Messages = new[] { context.Exception.Message } | |||
}; | |||
context.Result = new BadRequestObjectResult(json); | |||
context.HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest; | |||
} | |||
else | |||
{ | |||
var json = new JsonErrorResponse | |||
{ | |||
Messages = new[] { "An error occurred. Try it again." } | |||
}; | |||
if (env.IsDevelopment()) | |||
{ | |||
json.DeveloperMessage = context.Exception; | |||
} | |||
context.Result = new InternalServerErrorObjectResult(json); | |||
context.HttpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError; | |||
} | |||
context.ExceptionHandled = true; | |||
} | |||
} |
@ -1,9 +0,0 @@ | |||
namespace Basket.API.Infrastructure.Filters; | |||
public class JsonErrorResponse | |||
{ | |||
public string[] Messages { get; set; } | |||
public object DeveloperMessage { get; set; } | |||
} | |||
@ -1,26 +0,0 @@ | |||
namespace Basket.API.Infrastructure.Filters; | |||
public class ValidateModelStateFilter : ActionFilterAttribute | |||
{ | |||
public override void OnActionExecuting(ActionExecutingContext context) | |||
{ | |||
if (context.ModelState.IsValid) | |||
{ | |||
return; | |||
} | |||
var validationErrors = context.ModelState | |||
.Keys | |||
.SelectMany(k => context.ModelState[k].Errors) | |||
.Select(e => e.ErrorMessage) | |||
.ToArray(); | |||
var json = new JsonErrorResponse | |||
{ | |||
Messages = validationErrors | |||
}; | |||
context.Result = new BadRequestObjectResult(json); | |||
} | |||
} | |||
@ -1,90 +0,0 @@ | |||
namespace Basket.API.Infrastructure.Middlewares; | |||
using Microsoft.Extensions.Logging; | |||
public class FailingMiddleware | |||
{ | |||
private readonly RequestDelegate _next; | |||
private bool _mustFail; | |||
private readonly FailingOptions _options; | |||
private readonly ILogger _logger; | |||
public FailingMiddleware(RequestDelegate next, ILogger<FailingMiddleware> logger, FailingOptions options) | |||
{ | |||
_next = next; | |||
_options = options; | |||
_mustFail = false; | |||
_logger = logger; | |||
} | |||
public async Task Invoke(HttpContext context) | |||
{ | |||
var path = context.Request.Path; | |||
if (path.Equals(_options.ConfigPath, StringComparison.OrdinalIgnoreCase)) | |||
{ | |||
await ProcessConfigRequest(context); | |||
return; | |||
} | |||
if (MustFail(context)) | |||
{ | |||
_logger.LogInformation("Response for path {Path} will fail.", path); | |||
context.Response.StatusCode = (int)System.Net.HttpStatusCode.InternalServerError; | |||
context.Response.ContentType = "text/plain"; | |||
await context.Response.WriteAsync("Failed due to FailingMiddleware enabled."); | |||
} | |||
else | |||
{ | |||
await _next.Invoke(context); | |||
} | |||
} | |||
private async Task ProcessConfigRequest(HttpContext context) | |||
{ | |||
var enable = context.Request.Query.Keys.Any(k => k == "enable"); | |||
var disable = context.Request.Query.Keys.Any(k => k == "disable"); | |||
if (enable && disable) | |||
{ | |||
throw new ArgumentException("Must use enable or disable querystring values, but not both"); | |||
} | |||
if (disable) | |||
{ | |||
_mustFail = false; | |||
await SendOkResponse(context, "FailingMiddleware disabled. Further requests will be processed."); | |||
return; | |||
} | |||
if (enable) | |||
{ | |||
_mustFail = true; | |||
await SendOkResponse(context, "FailingMiddleware enabled. Further requests will return HTTP 500"); | |||
return; | |||
} | |||
// If reach here, that means that no valid parameter has been passed. Just output status | |||
await SendOkResponse(context, string.Format("FailingMiddleware is {0}", _mustFail ? "enabled" : "disabled")); | |||
return; | |||
} | |||
private async Task SendOkResponse(HttpContext context, string message) | |||
{ | |||
context.Response.StatusCode = (int)System.Net.HttpStatusCode.OK; | |||
context.Response.ContentType = "text/plain"; | |||
await context.Response.WriteAsync(message); | |||
} | |||
private bool MustFail(HttpContext context) | |||
{ | |||
var rpath = context.Request.Path.Value; | |||
if (_options.NotFilteredPaths.Any(p => p.Equals(rpath, StringComparison.InvariantCultureIgnoreCase))) | |||
{ | |||
return false; | |||
} | |||
return _mustFail && | |||
(_options.EndpointPaths.Any(x => x == rpath) | |||
|| _options.EndpointPaths.Count == 0); | |||
} | |||
} |
@ -1,10 +0,0 @@ | |||
namespace Basket.API.Infrastructure.Middlewares; | |||
public class FailingOptions | |||
{ | |||
public string ConfigPath = "/Failing"; | |||
public List<string> EndpointPaths { get; set; } = new List<string>(); | |||
public List<string> NotFilteredPaths { get; set; } = new List<string>(); | |||
} | |||
@ -1,20 +0,0 @@ | |||
namespace Basket.API.Infrastructure.Middlewares; | |||
public class FailingStartupFilter : IStartupFilter | |||
{ | |||
private readonly Action<FailingOptions> _options; | |||
public FailingStartupFilter(Action<FailingOptions> optionsAction) | |||
{ | |||
_options = optionsAction; | |||
} | |||
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next) | |||
{ | |||
return app => | |||
{ | |||
app.UseFailingMiddleware(_options); | |||
next(app); | |||
}; | |||
} | |||
} | |||
@ -1,14 +0,0 @@ | |||
namespace Basket.API.Infrastructure.Middlewares; | |||
public static class WebHostBuildertExtensions | |||
{ | |||
public static IWebHostBuilder UseFailing(this IWebHostBuilder builder, Action<FailingOptions> options) | |||
{ | |||
builder.ConfigureServices(services => | |||
{ | |||
services.AddSingleton<IStartupFilter>(new FailingStartupFilter(options)); | |||
}); | |||
return builder; | |||
} | |||
} | |||
@ -1,333 +1,30 @@ | |||
using Autofac.Core; | |||
using Microsoft.Azure.Amqp.Framing; | |||
using Microsoft.Extensions.Configuration; | |||
var builder = WebApplication.CreateBuilder(args); | |||
var appName = "Basket.API"; | |||
var builder = WebApplication.CreateBuilder(new WebApplicationOptions | |||
{ | |||
Args = args, | |||
ApplicationName = typeof(Program).Assembly.FullName, | |||
ContentRootPath = Directory.GetCurrentDirectory() | |||
}); | |||
if (builder.Configuration.GetValue<bool>("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.AddServiceDefaults(); | |||
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.AddGrpc(); | |||
builder.Services.AddControllers(); | |||
builder.Services.AddProblemDetails(); | |||
}) // Added for functional tests | |||
.AddApplicationPart(typeof(BasketController).Assembly) | |||
.AddJsonOptions(options => options.JsonSerializerOptions.WriteIndented = true); | |||
builder.Services.AddSwaggerGen(options => | |||
{ | |||
options.SwaggerDoc("v1", new OpenApiInfo | |||
{ | |||
Title = "eShopOnContainers - Basket HTTP API", | |||
Version = "v1", | |||
Description = "The Basket Service HTTP API" | |||
}); | |||
builder.Services.AddHealthChecks(builder.Configuration); | |||
builder.Services.AddRedis(builder.Configuration); | |||
options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme | |||
{ | |||
Type = SecuritySchemeType.OAuth2, | |||
Flows = new OpenApiOAuthFlows() | |||
{ | |||
Implicit = new OpenApiOAuthFlow() | |||
{ | |||
AuthorizationUrl = new Uri($"{builder.Configuration.GetValue<string>("IdentityUrlExternal")}/connect/authorize"), | |||
TokenUrl = new Uri($"{builder.Configuration.GetValue<string>("IdentityUrlExternal")}/connect/token"), | |||
Scopes = new Dictionary<string, string>() { { "basket", "Basket API" } } | |||
} | |||
} | |||
}); | |||
builder.Services.AddTransient<ProductPriceChangedIntegrationEventHandler>(); | |||
builder.Services.AddTransient<OrderStartedIntegrationEventHandler>(); | |||
options.OperationFilter<AuthorizeCheckOperationFilter>(); | |||
}); | |||
// prevent from mapping "sub" claim to nameidentifier. | |||
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove("sub"); | |||
var identityUrl = builder.Configuration.GetValue<string>("IdentityUrl"); | |||
builder.Services.AddAuthentication("Bearer").AddJwtBearer(options => | |||
{ | |||
options.Authority = identityUrl; | |||
options.RequireHttpsMetadata = false; | |||
options.Audience = "basket"; | |||
options.TokenValidationParameters.ValidateAudience = false; | |||
}); | |||
builder.Services.AddAuthorization(options => | |||
{ | |||
options.AddPolicy("ApiScope", policy => | |||
{ | |||
policy.RequireAuthenticatedUser(); | |||
policy.RequireClaim("scope", "basket"); | |||
}); | |||
}); | |||
builder.Services.AddCustomHealthCheck(builder.Configuration); | |||
builder.Services.Configure<BasketSettings>(builder.Configuration); | |||
builder.Services.AddSingleton<ConnectionMultiplexer>(sp => | |||
{ | |||
var settings = sp.GetRequiredService<IOptions<BasketSettings>>().Value; | |||
var configuration = ConfigurationOptions.Parse(settings.ConnectionString, true); | |||
return ConnectionMultiplexer.Connect(configuration); | |||
}); | |||
if (builder.Configuration.GetValue<bool>("AzureServiceBusEnabled")) | |||
{ | |||
builder.Services.AddSingleton<IServiceBusPersisterConnection>(sp => | |||
{ | |||
var serviceBusConnectionString = builder.Configuration["EventBusConnection"]; | |||
return new DefaultServiceBusPersisterConnection(serviceBusConnectionString); | |||
}); | |||
} | |||
else | |||
{ | |||
builder.Services.AddSingleton<IRabbitMQPersistentConnection>(sp => | |||
{ | |||
var logger = sp.GetRequiredService<ILogger<DefaultRabbitMQPersistentConnection>>(); | |||
var factory = new ConnectionFactory() | |||
{ | |||
HostName = builder.Configuration["EventBusConnection"], | |||
DispatchConsumersAsync = true | |||
}; | |||
if (!string.IsNullOrEmpty(builder.Configuration["EventBusUserName"])) | |||
{ | |||
factory.UserName = builder.Configuration["EventBusUserName"]; | |||
} | |||
if (!string.IsNullOrEmpty(builder.Configuration["EventBusPassword"])) | |||
{ | |||
factory.Password = builder.Configuration["EventBusPassword"]; | |||
} | |||
var retryCount = 5; | |||
if (!string.IsNullOrEmpty(builder.Configuration["EventBusRetryCount"])) | |||
{ | |||
retryCount = int.Parse(builder.Configuration["EventBusRetryCount"]); | |||
} | |||
return new DefaultRabbitMQPersistentConnection(factory, logger, retryCount); | |||
}); | |||
} | |||
builder.Services.RegisterEventBus(builder.Configuration); | |||
builder.Services.AddCors(options => | |||
{ | |||
options.AddPolicy("CorsPolicy", | |||
builder => builder | |||
.SetIsOriginAllowed((host) => true) | |||
.AllowAnyMethod() | |||
.AllowAnyHeader() | |||
.AllowCredentials()); | |||
}); | |||
builder.Services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>(); | |||
builder.Services.AddTransient<IBasketRepository, RedisBasketRepository>(); | |||
builder.Services.AddTransient<IIdentityService, IdentityService>(); | |||
builder.Services.AddOptions(); | |||
builder.Configuration.SetBasePath(Directory.GetCurrentDirectory()); | |||
builder.Configuration.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true); | |||
builder.Configuration.AddEnvironmentVariables(); | |||
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.WebHost.CaptureStartupErrors(false); | |||
builder.Host.UseSerilog(CreateSerilogLogger(builder.Configuration)); | |||
builder.WebHost.UseFailing(options => | |||
{ | |||
options.ConfigPath = "/Failing"; | |||
options.NotFilteredPaths.AddRange(new[] { "/hc", "/liveness" }); | |||
}); | |||
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(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.UseRouting(); | |||
app.UseCors("CorsPolicy"); | |||
app.UseAuthentication(); | |||
app.UseAuthorization(); | |||
app.UseStaticFiles(); | |||
app.UseServiceDefaults(); | |||
app.MapGrpcService<BasketService>(); | |||
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("Configuring web host ({ApplicationContext})...", Program.AppName); | |||
Log.Information("Starting web host ({ApplicationContext})...", Program.AppName); | |||
await app.RunAsync(); | |||
return 0; | |||
} | |||
catch (Exception ex) | |||
{ | |||
Log.Fatal(ex, "Program terminated unexpectedly ({ApplicationContext})!", Program.AppName); | |||
return 1; | |||
} | |||
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<IEventBus>(); | |||
eventBus.Subscribe<ProductPriceChangedIntegrationEvent, ProductPriceChangedIntegrationEventHandler>(); | |||
eventBus.Subscribe<OrderStartedIntegrationEvent, OrderStartedIntegrationEventHandler>(); | |||
} | |||
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 RegisterEventBus(this IServiceCollection services, IConfiguration configuration) | |||
{ | |||
if (configuration.GetValue<bool>("AzureServiceBusEnabled")) | |||
{ | |||
services.AddSingleton<IEventBus, EventBusServiceBus>(sp => | |||
{ | |||
var serviceBusPersisterConnection = sp.GetRequiredService<IServiceBusPersisterConnection>(); | |||
var logger = sp.GetRequiredService<ILogger<EventBusServiceBus>>(); | |||
var eventBusSubscriptionsManager = sp.GetRequiredService<IEventBusSubscriptionsManager>(); | |||
string subscriptionName = configuration["SubscriptionClientName"]; | |||
return new EventBusServiceBus(serviceBusPersisterConnection, logger, | |||
eventBusSubscriptionsManager, sp, subscriptionName); | |||
}); | |||
} | |||
else | |||
{ | |||
services.AddSingleton<IEventBus, EventBusRabbitMQ>(sp => | |||
{ | |||
var subscriptionClientName = configuration["SubscriptionClientName"]; | |||
var rabbitMQPersistentConnection = sp.GetRequiredService<IRabbitMQPersistentConnection>(); | |||
var logger = sp.GetRequiredService<ILogger<EventBusRabbitMQ>>(); | |||
var eventBusSubscriptionsManager = sp.GetRequiredService<IEventBusSubscriptionsManager>(); | |||
var retryCount = 5; | |||
if (!string.IsNullOrEmpty(configuration["EventBusRetryCount"])) | |||
{ | |||
retryCount = int.Parse(configuration["EventBusRetryCount"]); | |||
} | |||
return new EventBusRabbitMQ(rabbitMQPersistentConnection, logger, sp, eventBusSubscriptionsManager, subscriptionClientName, retryCount); | |||
}); | |||
} | |||
var eventBus = app.Services.GetRequiredService<IEventBus>(); | |||
services.AddSingleton<IEventBusSubscriptionsManager, InMemoryEventBusSubscriptionsManager>(); | |||
eventBus.Subscribe<ProductPriceChangedIntegrationEvent, ProductPriceChangedIntegrationEventHandler>(); | |||
eventBus.Subscribe<OrderStartedIntegrationEvent, OrderStartedIntegrationEventHandler>(); | |||
services.AddTransient<ProductPriceChangedIntegrationEventHandler>(); | |||
services.AddTransient<OrderStartedIntegrationEventHandler>(); | |||
return services; | |||
} | |||
} | |||
await app.RunAsync(); |
@ -1,26 +1,11 @@ | |||
{ | |||
"iisSettings": { | |||
"windowsAuthentication": false, | |||
"anonymousAuthentication": true, | |||
"iisExpress": { | |||
"applicationUrl": "http://localhost:58017/", | |||
"sslPort": 0 | |||
} | |||
}, | |||
"profiles": { | |||
"IIS Express": { | |||
"commandName": "IISExpress", | |||
"launchBrowser": true, | |||
"launchUrl": "swagger", | |||
"environmentVariables": { | |||
"ASPNETCORE_ENVIRONMENT": "Development" | |||
} | |||
}, | |||
"Microsoft.eShopOnContainers.Services.Basket.API": { | |||
"Basket.API": { | |||
"commandName": "Project", | |||
"launchBrowser": true, | |||
"launchUrl": "http://localhost:55103/", | |||
"applicationUrl": "http://localhost:5221", | |||
"environmentVariables": { | |||
"Identity__Url": "http://localhost:5223", | |||
"ASPNETCORE_ENVIRONMENT": "Development" | |||
} | |||
} | |||
@ -0,0 +1,17 @@ | |||
{ | |||
"dependencies": { | |||
"secrets1": { | |||
"type": "secrets" | |||
}, | |||
"rabbitmq1": { | |||
"type": "rabbitmq", | |||
"connectionId": "eventbus", | |||
"dynamicId": null | |||
}, | |||
"redis1": { | |||
"type": "redis", | |||
"connectionId": "ConnectionStrings:Redis", | |||
"dynamicId": null | |||
} | |||
} | |||
} |
@ -0,0 +1,26 @@ | |||
{ | |||
"dependencies": { | |||
"secrets1": { | |||
"type": "secrets.user" | |||
}, | |||
"rabbitmq1": { | |||
"containerPorts": "5672:5672,15672:15672", | |||
"secretStore": "LocalSecretsFile", | |||
"containerName": "rabbitmq", | |||
"containerImage": "rabbitmq:3-management-alpine", | |||
"type": "rabbitmq.container", | |||
"connectionId": "eventbus", | |||
"dynamicId": null | |||
}, | |||
"redis1": { | |||
"serviceConnectorResourceId": "", | |||
"containerPorts": "6379:6379", | |||
"secretStore": "LocalSecretsFile", | |||
"containerName": "basket-redis", | |||
"containerImage": "redis:alpine", | |||
"type": "redis.container", | |||
"connectionId": "ConnectionStrings:Redis", | |||
"dynamicId": null | |||
} | |||
} | |||
} |
@ -1,6 +0,0 @@ | |||
namespace Microsoft.eShopOnContainers.Services.Basket.API; | |||
internal class TestHttpResponseTrailersFeature : IHttpResponseTrailersFeature | |||
{ | |||
public IHeaderDictionary Trailers { get; set; } | |||
} |
@ -1,16 +1,4 @@ | |||
{ | |||
"Serilog": { | |||
"MinimumLevel": { | |||
"Default": "Debug", | |||
"Override": { | |||
"Microsoft": "Warning", | |||
"Microsoft.eShopOnContainers": "Debug", | |||
"System": "Warning" | |||
} | |||
} | |||
}, | |||
"IdentityUrlExternal": "http://localhost:5105", | |||
"IdentityUrl": "http://localhost:5105", | |||
"ConnectionString": "127.0.0.1", | |||
"AzureServiceBusEnabled": false, | |||
"EventBusConnection": "localhost" |
@ -1,30 +1,48 @@ | |||
{ | |||
"Serilog": { | |||
"SeqServerUrl": null, | |||
"LogstashgUrl": null, | |||
"MinimumLevel": { | |||
"Logging": { | |||
"LogLevel": { | |||
"Default": "Information", | |||
"Override": { | |||
"Microsoft": "Warning", | |||
"Microsoft.eShopOnContainers": "Information", | |||
"System": "Warning" | |||
} | |||
"Microsoft.AspNetCore": "Warning" | |||
} | |||
}, | |||
"Kestrel": { | |||
"EndpointDefaults": { | |||
"Protocols": "Http2" | |||
"Endpoints": { | |||
"Http": { | |||
"Url": "http://localhost:5221" | |||
}, | |||
"gRPC": { | |||
"Url": "http://localhost:6221", | |||
"Protocols": "Http2" | |||
} | |||
} | |||
}, | |||
"OpenApi": { | |||
"Endpoint": { | |||
"Name": "Basket.API V1" | |||
}, | |||
"Document": { | |||
"Description": "The Basket Service HTTP API", | |||
"Title": "eShopOnContainers - Basket HTTP API", | |||
"Version": "v1" | |||
}, | |||
"Auth": { | |||
"ClientId": "basketswaggerui", | |||
"AppName": "Basket Swagger UI" | |||
} | |||
}, | |||
"SubscriptionClientName": "Basket", | |||
"ApplicationInsights": { | |||
"InstrumentationKey": "" | |||
"ConnectionStrings": { | |||
"Redis": "localhost", | |||
"EventBus": "localhost" | |||
}, | |||
"Identity": { | |||
"Audience": "basket", | |||
"Url": "http://localhost:5223", | |||
"Scopes": { | |||
"basket": "Basket API" | |||
} | |||
}, | |||
"EventBusRetryCount": 5, | |||
"UseVault": false, | |||
"Vault": { | |||
"Name": "eshop", | |||
"ClientId": "your-client-id", | |||
"ClientSecret": "your-client-secret" | |||
"EventBus": { | |||
"SubscriptionClientName": "Basket", | |||
"RetryCount": 5 | |||
} | |||
} |
@ -1,17 +0,0 @@ | |||
<?xml version="1.0" encoding="utf-8"?> | |||
<configuration> | |||
<!-- | |||
Configure your application settings in appsettings.json. Learn more at http://go.microsoft.com/fwlink/?LinkId=786380 | |||
--> | |||
<system.webServer> | |||
<handlers> | |||
<add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" /> | |||
</handlers> | |||
<aspNetCore processPath="%LAUNCHER_PATH%" arguments="%LAUNCHER_ARGS%" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" forwardWindowsAuthToken="false" hostingModel="InProcess"> | |||
<environmentVariables> | |||
<environmentVariable name="COMPLUS_ForceENC" value="1" /> | |||
<environmentVariable name="ASPNETCORE_ENVIRONMENT" value="Development" /> | |||
</environmentVariables> | |||
</aspNetCore> | |||
</system.webServer> | |||
</configuration> |
@ -1,13 +0,0 @@ | |||
namespace Basket.FunctionalTests.Base; | |||
static class HttpClientExtensions | |||
{ | |||
public static HttpClient CreateIdempotentClient(this TestServer server) | |||
{ | |||
var client = server.CreateClient(); | |||
client.DefaultRequestHeaders.Add("x-requestid", Guid.NewGuid().ToString()); | |||
return client; | |||
} | |||
} |
@ -1,23 +1,19 @@ | |||
global using Basket.FunctionalTests.Base; | |||
global using System; | |||
global using System.Collections.Generic; | |||
global using System.IO; | |||
global using System.Net.Http; | |||
global using System.Security.Claims; | |||
global using System.Text; | |||
global using System.Text.Json; | |||
global using System.Threading.Tasks; | |||
global using Basket.FunctionalTests.Base; | |||
global using Microsoft.AspNetCore.Builder; | |||
global using Microsoft.AspNetCore.Hosting; | |||
global using Microsoft.AspNetCore.Http; | |||
global using Microsoft.AspNetCore.Routing; | |||
global using Microsoft.AspNetCore.TestHost; | |||
global using Microsoft.eShopOnContainers.Services.Basket.API.Infrastructure.Repositories; | |||
global using Microsoft.eShopOnContainers.Services.Basket.API.Model; | |||
global using Microsoft.eShopOnContainers.Services.Basket.API; | |||
global using Microsoft.Extensions.Configuration; | |||
global using Microsoft.Extensions.DependencyInjection; | |||
global using Microsoft.Extensions.Logging; | |||
global using StackExchange.Redis; | |||
global using System.Collections.Generic; | |||
global using System.IO; | |||
global using System.Net.Http; | |||
global using System.Reflection; | |||
global using System.Security.Claims; | |||
global using System.Text.Json; | |||
global using System.Text; | |||
global using System.Threading.Tasks; | |||
global using System; | |||
global using Xunit; |
@ -0,0 +1,25 @@ | |||
{ | |||
"Logging": { | |||
"Console": { | |||
"IncludeScopes": false | |||
}, | |||
"LogLevel": { | |||
"Default": "Debug", | |||
"System": "Information", | |||
"Microsoft": "Information" | |||
} | |||
}, | |||
"Identity": { | |||
"ExternalUrl": "http://localhost:5105", | |||
"Url": "http://localhost:5105" | |||
}, | |||
"ConnectionStrings": { | |||
"Redis": "127.0.0.1" | |||
}, | |||
"EventBus": { | |||
"ConnectionString": "localhost", | |||
"SubscriptionClientName": "Basket" | |||
}, | |||
"isTest": "true", | |||
"SuppressCheckForUnhandledSecurityMetadata": true | |||
} |
@ -1,17 +0,0 @@ | |||
{ | |||
"Logging": { | |||
"IncludeScopes": false, | |||
"LogLevel": { | |||
"Default": "Debug", | |||
"System": "Information", | |||
"Microsoft": "Information" | |||
} | |||
}, | |||
"IdentityUrl": "http://localhost:5105", | |||
"IdentityUrlExternal": "http://localhost:5105", | |||
"ConnectionString": "127.0.0.1", | |||
"isTest": "true", | |||
"EventBusConnection": "localhost", | |||
"SubscriptionClientName": "Basket", | |||
"SuppressCheckForUnhandledSecurityMetadata": true | |||
} |
@ -0,0 +1,42 @@ | |||
using Microsoft.AspNetCore.Routing; | |||
namespace Catalog.API.Apis; | |||
public static class PicApi | |||
{ | |||
public static IEndpointConventionBuilder MapPicApi(this IEndpointRouteBuilder routes) | |||
{ | |||
return routes.MapGet("api/v1/catalog/items/{catalogItemId:int}/pic", | |||
async (int catalogItemId, CatalogContext db, IWebHostEnvironment environment) => | |||
{ | |||
var item = await db.CatalogItems.FindAsync(catalogItemId); | |||
if (item is null) | |||
{ | |||
return Results.NotFound(); | |||
} | |||
var path = Path.Combine(environment.ContentRootPath, "Pics", item.PictureFileName); | |||
string imageFileExtension = Path.GetExtension(item.PictureFileName); | |||
string mimetype = GetImageMimeTypeFromImageFileExtension(imageFileExtension); | |||
return Results.File(path, mimetype); | |||
}) | |||
.WithTags("Pic") | |||
.Produces(404); | |||
static string GetImageMimeTypeFromImageFileExtension(string extension) => extension switch | |||
{ | |||
".png" => "image/png", | |||
".gif" => "image/gif", | |||
".jpg" or ".jpeg" => "image/jpeg", | |||
".bmp" => "image/bmp", | |||
".tiff" => "image/tiff", | |||
".wmf" => "image/wmf", | |||
".jp2" => "image/jp2", | |||
".svg" => "image/svg+xml", | |||
_ => "application/octet-stream", | |||
}; | |||
} | |||
} |
@ -1,11 +0,0 @@ | |||
// For more information on enabling MVC for empty projects, visit http://go.microsoft.com/fwlink/?LinkID=397860 | |||
namespace Microsoft.eShopOnContainers.Services.Catalog.API.Controllers; | |||
public class HomeController : Controller | |||
{ | |||
// GET: /<controller>/ | |||
public IActionResult Index() | |||
{ | |||
return new RedirectResult("~/swagger"); | |||
} | |||
} |
@ -1,64 +0,0 @@ | |||
// For more information on enabling MVC for empty projects, visit http://go.microsoft.com/fwlink/?LinkID=397860 | |||
namespace Microsoft.eShopOnContainers.Services.Catalog.API.Controllers; | |||
[ApiController] | |||
public class PicController : ControllerBase | |||
{ | |||
private readonly IWebHostEnvironment _env; | |||
private readonly CatalogContext _catalogContext; | |||
public PicController(IWebHostEnvironment env, | |||
CatalogContext catalogContext) | |||
{ | |||
_env = env; | |||
_catalogContext = catalogContext; | |||
} | |||
[HttpGet] | |||
[Route("api/v1/catalog/items/{catalogItemId:int}/pic")] | |||
[ProducesResponseType((int)HttpStatusCode.NotFound)] | |||
[ProducesResponseType((int)HttpStatusCode.BadRequest)] | |||
// GET: /<controller>/ | |||
public async Task<ActionResult> GetImageAsync(int catalogItemId) | |||
{ | |||
if (catalogItemId <= 0) | |||
{ | |||
return BadRequest(); | |||
} | |||
var item = await _catalogContext.CatalogItems | |||
.SingleOrDefaultAsync(ci => ci.Id == catalogItemId); | |||
if (item != null) | |||
{ | |||
var webRoot = _env.WebRootPath; | |||
var path = Path.Combine(webRoot, item.PictureFileName); | |||
string imageFileExtension = Path.GetExtension(item.PictureFileName); | |||
string mimetype = GetImageMimeTypeFromImageFileExtension(imageFileExtension); | |||
var buffer = await System.IO.File.ReadAllBytesAsync(path); | |||
return File(buffer, mimetype); | |||
} | |||
return NotFound(); | |||
} | |||
private string GetImageMimeTypeFromImageFileExtension(string extension) | |||
{ | |||
string mimetype = extension switch | |||
{ | |||
".png" => "image/png", | |||
".gif" => "image/gif", | |||
".jpg" or ".jpeg" => "image/jpeg", | |||
".bmp" => "image/bmp", | |||
".tiff" => "image/tiff", | |||
".wmf" => "image/wmf", | |||
".jp2" => "image/jp2", | |||
".svg" => "image/svg+xml", | |||
_ => "application/octet-stream", | |||
}; | |||
return mimetype; | |||
} | |||
} |
@ -0,0 +1,92 @@ | |||
using Microsoft.EntityFrameworkCore.Infrastructure; | |||
public static class Extensions | |||
{ | |||
public static IServiceCollection AddHealthChecks(this IServiceCollection services, IConfiguration configuration) | |||
{ | |||
var hcBuilder = services.AddHealthChecks(); | |||
hcBuilder | |||
.AddSqlServer(_ => configuration.GetRequiredConnectionString("CatalogDB"), | |||
name: "CatalogDB-check", | |||
tags: new string[] { "ready" }); | |||
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[] { "ready" }); | |||
} | |||
return services; | |||
} | |||
public static IServiceCollection AddDbContexts(this IServiceCollection services, IConfiguration configuration) | |||
{ | |||
static void ConfigureSqlOptions(SqlServerDbContextOptionsBuilder sqlOptions) | |||
{ | |||
sqlOptions.MigrationsAssembly(typeof(Program).Assembly.FullName); | |||
// 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<CatalogContext>(options => | |||
{ | |||
var connectionString = configuration.GetRequiredConnectionString("CatalogDB"); | |||
options.UseSqlServer(connectionString, ConfigureSqlOptions); | |||
}); | |||
services.AddDbContext<IntegrationEventLogContext>(options => | |||
{ | |||
var connectionString = configuration.GetRequiredConnectionString("CatalogDB"); | |||
options.UseSqlServer(connectionString, ConfigureSqlOptions); | |||
}); | |||
return services; | |||
} | |||
public static IServiceCollection AddApplicationOptions(this IServiceCollection services, IConfiguration configuration) | |||
{ | |||
services.Configure<CatalogSettings>(configuration); | |||
// TODO: Move to the new problem details middleware | |||
services.Configure<ApiBehaviorOptions>(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<Func<DbConnection, IIntegrationEventLogService>>( | |||
sp => (DbConnection c) => new IntegrationEventLogService(c)); | |||
services.AddTransient<ICatalogIntegrationEventService, CatalogIntegrationEventService>(); | |||
return services; | |||
} | |||
} |
@ -1,68 +0,0 @@ | |||
namespace Microsoft.eShopOnContainers.Services.Catalog.API.Extensions; | |||
public static class WebHostExtensions | |||
{ | |||
public static bool IsInKubernetes(this IWebHost host) | |||
{ | |||
var cfg = host.Services.GetService<IConfiguration>(); | |||
var orchestratorType = cfg.GetValue<string>("OrchestratorType"); | |||
return orchestratorType?.ToUpper() == "K8S"; | |||
} | |||
public static IWebHost MigrateDbContext<TContext>(this IWebHost host, Action<TContext, IServiceProvider> seeder) where TContext : DbContext | |||
{ | |||
var underK8s = host.IsInKubernetes(); | |||
using var scope = host.Services.CreateScope(); | |||
var services = scope.ServiceProvider; | |||
var logger = services.GetRequiredService<ILogger<TContext>>(); | |||
var context = services.GetService<TContext>(); | |||
try | |||
{ | |||
logger.LogInformation("Migrating database associated with context {DbContextName}", typeof(TContext).Name); | |||
if (underK8s) | |||
{ | |||
InvokeSeeder(seeder, context, services); | |||
} | |||
else | |||
{ | |||
var retry = Policy.Handle<SqlException>() | |||
.WaitAndRetry(new TimeSpan[] | |||
{ | |||
TimeSpan.FromSeconds(3), | |||
TimeSpan.FromSeconds(5), | |||
TimeSpan.FromSeconds(8), | |||
}); | |||
//if the sql server container is not created on run docker compose this | |||
//migration can't fail for network related exception. The retry options for DbContext only | |||
//apply to transient exceptions | |||
// Note that this is NOT applied when running some orchestrators (let the orchestrator to recreate the failing service) | |||
retry.Execute(() => InvokeSeeder(seeder, context, services)); | |||
} | |||
logger.LogInformation("Migrated database associated with context {DbContextName}", typeof(TContext).Name); | |||
} | |||
catch (Exception ex) | |||
{ | |||
logger.LogError(ex, "An error occurred while migrating the database used on context {DbContextName}", typeof(TContext).Name); | |||
if (underK8s) | |||
{ | |||
throw; // Rethrow under k8s because we rely on k8s to re-run the pod | |||
} | |||
} | |||
return host; | |||
} | |||
private static void InvokeSeeder<TContext>(Action<TContext, IServiceProvider> seeder, TContext context, IServiceProvider services) | |||
where TContext : DbContext | |||
{ | |||
context.Database.Migrate(); | |||
seeder(context, services); | |||
} | |||
} |
@ -1,63 +1,43 @@ | |||
global using Azure.Core; | |||
global using Azure.Identity; | |||
global using Autofac.Extensions.DependencyInjection; | |||
global using Autofac; | |||
global using Microsoft.eShopOnContainers.Services.Catalog.API.Extensions; | |||
global using Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure.ActionResults; | |||
global using Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure.Exceptions; | |||
global using Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents; | |||
global using System; | |||
global using System.Collections.Generic; | |||
global using System.Data.Common; | |||
global using System.Data.SqlClient; | |||
global using System.Globalization; | |||
global using System.IO; | |||
global using System.IO.Compression; | |||
global using System.Linq; | |||
global using System.Net; | |||
global using System.Text.RegularExpressions; | |||
global using System.Threading.Tasks; | |||
global using Grpc.Core; | |||
global using Microsoft.AspNetCore.Builder; | |||
global using Microsoft.AspNetCore.Hosting; | |||
global using Microsoft.AspNetCore.Http; | |||
global using Microsoft.AspNetCore.Builder; | |||
global using Microsoft.AspNetCore.Mvc.Filters; | |||
global using Microsoft.AspNetCore.Mvc; | |||
global using Microsoft.AspNetCore.Server.Kestrel.Core; | |||
global using Microsoft.AspNetCore; | |||
global using Microsoft.Extensions.Logging; | |||
global using Microsoft.EntityFrameworkCore; | |||
global using Microsoft.EntityFrameworkCore.Design; | |||
global using Microsoft.EntityFrameworkCore.Metadata.Builders; | |||
global using Microsoft.EntityFrameworkCore; | |||
global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; | |||
global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; | |||
global using Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF; | |||
global using Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF.Services; | |||
global using Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF.Utilities; | |||
global using Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF; | |||
global using Microsoft.eShopOnContainers.Services.Catalog.API; | |||
global using Microsoft.eShopOnContainers.Services.Catalog.API.Extensions; | |||
global using Microsoft.eShopOnContainers.Services.Catalog.API.Grpc; | |||
global using Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure; | |||
global using Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure.EntityConfigurations; | |||
global using Microsoft.eShopOnContainers.Services.Catalog.API; | |||
global using Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure.Exceptions; | |||
global using Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents; | |||
global using Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents.EventHandling; | |||
global using Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents.Events; | |||
global using Microsoft.eShopOnContainers.Services.Catalog.API.Model; | |||
global using Microsoft.eShopOnContainers.Services.Catalog.API.ViewModel; | |||
global using Microsoft.eShopOnContainers.Services.Catalog.API.Grpc; | |||
global using Microsoft.Extensions.Configuration; | |||
global using Microsoft.Extensions.DependencyInjection; | |||
global using Microsoft.Extensions.Hosting; | |||
global using Microsoft.Extensions.Logging; | |||
global using Microsoft.Extensions.Options; | |||
global using Polly.Retry; | |||
global using Polly; | |||
global using Serilog.Context; | |||
global using Serilog; | |||
global using System.Collections.Generic; | |||
global using System.Data.Common; | |||
global using System.Data.SqlClient; | |||
global using System.Globalization; | |||
global using System.IO.Compression; | |||
global using System.IO; | |||
global using System.Linq; | |||
global using System.Net; | |||
global using System.Text.RegularExpressions; | |||
global using System.Threading.Tasks; | |||
global using System; | |||
global using Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure.Filters; | |||
global using HealthChecks.UI.Client; | |||
global using Microsoft.AspNetCore.Diagnostics.HealthChecks; | |||
global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus; | |||
global using Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ; | |||
global using Microsoft.eShopOnContainers.BuildingBlocks.EventBusServiceBus; | |||
global using Microsoft.eShopOnContainers.Services.Catalog.API.IntegrationEvents.EventHandling; | |||
global using Microsoft.Extensions.Diagnostics.HealthChecks; | |||
global using Microsoft.OpenApi.Models; | |||
global using RabbitMQ.Client; | |||
global using System.Reflection; | |||
global using Microsoft.Extensions.FileProviders; | |||
global using Polly.Retry; | |||
global using Services.Common; | |||
global using Catalog.API.Apis; |
@ -1,10 +0,0 @@ | |||
namespace Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure.ActionResults; | |||
public class InternalServerErrorObjectResult : ObjectResult | |||
{ | |||
public InternalServerErrorObjectResult(object error) | |||
: base(error) | |||
{ | |||
StatusCode = StatusCodes.Status500InternalServerError; | |||
} | |||
} |
@ -1,58 +0,0 @@ | |||
namespace Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure.Filters; | |||
public class HttpGlobalExceptionFilter : IExceptionFilter | |||
{ | |||
private readonly IWebHostEnvironment env; | |||
private readonly ILogger<HttpGlobalExceptionFilter> logger; | |||
public HttpGlobalExceptionFilter(IWebHostEnvironment env, ILogger<HttpGlobalExceptionFilter> logger) | |||
{ | |||
this.env = env; | |||
this.logger = logger; | |||
} | |||
public void OnException(ExceptionContext context) | |||
{ | |||
logger.LogError(new EventId(context.Exception.HResult), | |||
context.Exception, | |||
context.Exception.Message); | |||
if (context.Exception.GetType() == typeof(CatalogDomainException)) | |||
{ | |||
var problemDetails = new ValidationProblemDetails() | |||
{ | |||
Instance = context.HttpContext.Request.Path, | |||
Status = StatusCodes.Status400BadRequest, | |||
Detail = "Please refer to the errors property for additional details." | |||
}; | |||
problemDetails.Errors.Add("DomainValidations", new string[] { context.Exception.Message.ToString() }); | |||
context.Result = new BadRequestObjectResult(problemDetails); | |||
context.HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest; | |||
} | |||
else | |||
{ | |||
var json = new JsonErrorResponse | |||
{ | |||
Messages = new[] { "An error ocurred." } | |||
}; | |||
if (env.IsDevelopment()) | |||
{ | |||
json.DeveloperMessage = context.Exception; | |||
} | |||
context.Result = new InternalServerErrorObjectResult(json); | |||
context.HttpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError; | |||
} | |||
context.ExceptionHandled = true; | |||
} | |||
private class JsonErrorResponse | |||
{ | |||
public string[] Messages { get; set; } | |||
public object DeveloperMessage { get; set; } | |||
} | |||
} |
@ -1,388 +1,43 @@ | |||
var builder = WebApplication.CreateBuilder(new WebApplicationOptions | |||
{ | |||
Args = args, | |||
ApplicationName = typeof(Program).Assembly.FullName, | |||
ContentRootPath = Directory.GetCurrentDirectory(), | |||
WebRootPath = "Pics", | |||
}); | |||
if (builder.Configuration.GetValue<bool>("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; | |||
}); | |||
var builder = WebApplication.CreateBuilder(args); | |||
}); | |||
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); | |||
builder.AddServiceDefaults(); | |||
var app = builder.Build(); | |||
builder.Services.AddGrpc(); | |||
builder.Services.AddControllers(); | |||
if (app.Environment.IsDevelopment()) | |||
{ | |||
app.UseDeveloperExceptionPage(); | |||
} | |||
else | |||
{ | |||
app.UseExceptionHandler("/Home/Error"); | |||
} | |||
// Application specific services | |||
builder.Services.AddHealthChecks(builder.Configuration); | |||
builder.Services.AddDbContexts(builder.Configuration); | |||
builder.Services.AddApplicationOptions(builder.Configuration); | |||
builder.Services.AddIntegrationServices(); | |||
var pathBase = app.Configuration["PATH_BASE"]; | |||
if (!string.IsNullOrEmpty(pathBase)) | |||
{ | |||
app.UsePathBase(pathBase); | |||
} | |||
builder.Services.AddTransient<OrderStatusChangedToAwaitingValidationIntegrationEventHandler>(); | |||
builder.Services.AddTransient<OrderStatusChangedToPaidIntegrationEventHandler>(); | |||
app.UseSwagger() | |||
.UseSwaggerUI(c => | |||
{ | |||
c.SwaggerEndpoint($"{(!string.IsNullOrEmpty(pathBase) ? pathBase : string.Empty)}/swagger/v1/swagger.json", "Catalog.API V1"); | |||
}); | |||
var app = builder.Build(); | |||
app.UseRouting(); | |||
app.UseCors("CorsPolicy"); | |||
app.MapDefaultControllerRoute(); | |||
app.UseServiceDefaults(); | |||
app.MapPicApi(); | |||
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<CatalogService>(); | |||
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<IEventBus>(); | |||
try | |||
eventBus.Subscribe<OrderStatusChangedToAwaitingValidationIntegrationEvent, OrderStatusChangedToAwaitingValidationIntegrationEventHandler>(); | |||
eventBus.Subscribe<OrderStatusChangedToPaidIntegrationEvent, OrderStatusChangedToPaidIntegrationEventHandler>(); | |||
// REVIEW: This is done fore development east but shouldn't be here in production | |||
using (var scope = app.Services.CreateScope()) | |||
{ | |||
Log.Information("Configuring web host ({ApplicationContext})...", Program.AppName); | |||
using var scope = app.Services.CreateScope(); | |||
var context = scope.ServiceProvider.GetRequiredService<CatalogContext>(); | |||
var env = app.Services.GetService<IWebHostEnvironment>(); | |||
var settings = app.Services.GetService<IOptions<CatalogSettings>>(); | |||
var logger = app.Services.GetService<ILogger<CatalogContextSeed>>(); | |||
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<IntegrationEventLogContext>(); | |||
await integEventContext.Database.MigrateAsync(); | |||
app.Logger.LogInformation("Starting web host ({ApplicationName})...", AppName); | |||
await app.RunAsync(); | |||
return 0; | |||
} | |||
catch (Exception ex) | |||
{ | |||
Log.Fatal(ex, "Program terminated unexpectedly ({ApplicationContext})!", Program.AppName); | |||
return 1; | |||
} | |||
finally | |||
{ | |||
Log.CloseAndFlush(); | |||
} | |||
void ConfigureEventBus(IApplicationBuilder app) | |||
{ | |||
var eventBus = app.ApplicationServices.GetRequiredService<IEventBus>(); | |||
eventBus.Subscribe<OrderStatusChangedToAwaitingValidationIntegrationEvent, OrderStatusChangedToAwaitingValidationIntegrationEventHandler>(); | |||
eventBus.Subscribe<OrderStatusChangedToPaidIntegrationEvent, OrderStatusChangedToPaidIntegrationEventHandler>(); | |||
} | |||
(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<string>("AzureStorageAccountName"); | |||
var accountKey = configuration.GetValue<string>("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<bool>("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<CatalogContext>(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<IntegrationEventLogContext>(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<CatalogSettings>(configuration); | |||
services.Configure<ApiBehaviorOptions>(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<Func<DbConnection, IIntegrationEventLogService>>( | |||
sp => (DbConnection c) => new IntegrationEventLogService(c)); | |||
services.AddTransient<ICatalogIntegrationEventService, CatalogIntegrationEventService>(); | |||
if (configuration.GetValue<bool>("AzureServiceBusEnabled")) | |||
{ | |||
services.AddSingleton<IServiceBusPersisterConnection>(sp => | |||
{ | |||
var settings = sp.GetRequiredService<IOptions<CatalogSettings>>().Value; | |||
var serviceBusConnection = settings.EventBusConnection; | |||
return new DefaultServiceBusPersisterConnection(serviceBusConnection); | |||
}); | |||
} | |||
else | |||
{ | |||
services.AddSingleton<IRabbitMQPersistentConnection>(sp => | |||
{ | |||
var settings = sp.GetRequiredService<IOptions<CatalogSettings>>().Value; | |||
var logger = sp.GetRequiredService<ILogger<DefaultRabbitMQPersistentConnection>>(); | |||
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<bool>("AzureServiceBusEnabled")) | |||
{ | |||
services.AddSingleton<IEventBus, EventBusServiceBus>(sp => | |||
{ | |||
var serviceBusPersisterConnection = sp.GetRequiredService<IServiceBusPersisterConnection>(); | |||
var logger = sp.GetRequiredService<ILogger<EventBusServiceBus>>(); | |||
var eventBusSubcriptionsManager = sp.GetRequiredService<IEventBusSubscriptionsManager>(); | |||
string subscriptionName = configuration["SubscriptionClientName"]; | |||
return new EventBusServiceBus(serviceBusPersisterConnection, logger, | |||
eventBusSubcriptionsManager, sp, subscriptionName); | |||
}); | |||
} | |||
else | |||
{ | |||
services.AddSingleton<IEventBus, EventBusRabbitMQ>(sp => | |||
{ | |||
var subscriptionClientName = configuration["SubscriptionClientName"]; | |||
var rabbitMQPersistentConnection = sp.GetRequiredService<IRabbitMQPersistentConnection>(); | |||
var logger = sp.GetRequiredService<ILogger<EventBusRabbitMQ>>(); | |||
var eventBusSubcriptionsManager = sp.GetRequiredService<IEventBusSubscriptionsManager>(); | |||
var retryCount = 5; | |||
if (!string.IsNullOrEmpty(configuration["EventBusRetryCount"])) | |||
{ | |||
retryCount = int.Parse(configuration["EventBusRetryCount"]); | |||
} | |||
return new EventBusRabbitMQ(rabbitMQPersistentConnection, logger, sp, eventBusSubcriptionsManager, subscriptionClientName, retryCount); | |||
}); | |||
} | |||
services.AddSingleton<IEventBusSubscriptionsManager, InMemoryEventBusSubscriptionsManager>(); | |||
services.AddTransient<OrderStatusChangedToAwaitingValidationIntegrationEventHandler>(); | |||
services.AddTransient<OrderStatusChangedToPaidIntegrationEventHandler>(); | |||
return services; | |||
} | |||
} | |||
await app.RunAsync(); |