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": { | "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; | ||||
global using Grpc.Core.Interceptors; | |||||
global using GrpcBasket; | global using GrpcBasket; | ||||
global using GrpcOrdering; | |||||
global using HealthChecks.UI.Client; | |||||
global using Microsoft.AspNetCore.Authentication; | global using Microsoft.AspNetCore.Authentication; | ||||
global using Microsoft.AspNetCore.Authorization; | global using Microsoft.AspNetCore.Authorization; | ||||
global using Microsoft.AspNetCore.Builder; | 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.Http; | ||||
global using Microsoft.AspNetCore.Mvc; | global using Microsoft.AspNetCore.Mvc; | ||||
global using Microsoft.AspNetCore; | |||||
global using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Config; | 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.Infrastructure; | ||||
global using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models; | global using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Models; | ||||
global using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Services; | global using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator.Services; | ||||
global using Microsoft.eShopOnContainers.Web.Shopping.HttpAggregator; | |||||
global using Microsoft.Extensions.Configuration; | global using Microsoft.Extensions.Configuration; | ||||
global using Microsoft.Extensions.DependencyInjection; | 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.Logging; | ||||
global using Microsoft.Extensions.Options; | 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": { | "profiles": { | ||||
"IIS Express": { | |||||
"commandName": "IISExpress", | |||||
"launchBrowser": true, | |||||
"launchUrl": "api/values", | |||||
"environmentVariables": { | |||||
"ASPNETCORE_ENVIRONMENT": "Development" | |||||
} | |||||
}, | |||||
"PurchaseForMvc": { | |||||
"Web.Shopping.HttpAggregator": { | |||||
"commandName": "Project", | "commandName": "Project", | ||||
"launchBrowser": true, | "launchBrowser": true, | ||||
"launchUrl": "api/values", | |||||
"applicationUrl": "http://localhost:5229/", | |||||
"environmentVariables": { | "environmentVariables": { | ||||
"ASPNETCORE_ENVIRONMENT": "Development" | "ASPNETCORE_ENVIRONMENT": "Development" | ||||
}, | |||||
"applicationUrl": "http://localhost:61632/" | |||||
} | |||||
} | } | ||||
} | } | ||||
} | } |
@ -1,15 +1,8 @@ | |||||
{ | { | ||||
"Logging": { | "Logging": { | ||||
"IncludeScopes": false, | |||||
"Debug": { | |||||
"LogLevel": { | |||||
"Default": "Debug" | |||||
} | |||||
}, | |||||
"Console": { | |||||
"LogLevel": { | |||||
"Default": "Debug" | |||||
} | |||||
"LogLevel": { | |||||
"Default": "Information", | |||||
"Microsoft.AspNetCore": "Warning" | |||||
} | } | ||||
} | } | ||||
} | } |
@ -1,15 +1,138 @@ | |||||
{ | { | ||||
"Logging": { | "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"> | <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> | </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.EventHandling; | ||||
global using Basket.API.IntegrationEvents.Events; | global using Basket.API.IntegrationEvents.Events; | ||||
global using Basket.API.Model; | global using Basket.API.Model; | ||||
global using Basket.API.Repositories; | |||||
global using Grpc.Core; | global using Grpc.Core; | ||||
global using GrpcBasket; | global using GrpcBasket; | ||||
global using HealthChecks.UI.Client; | |||||
global using Microsoft.AspNetCore.Authorization; | global using Microsoft.AspNetCore.Authorization; | ||||
global using Microsoft.AspNetCore.Builder; | 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.Http; | ||||
global using Microsoft.AspNetCore.Mvc.Authorization; | |||||
global using Microsoft.AspNetCore.Mvc.Filters; | |||||
global using Microsoft.AspNetCore.Mvc; | 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.Abstractions; | ||||
global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; | 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.EventHandling; | ||||
global using Microsoft.eShopOnContainers.Services.Basket.API.IntegrationEvents.Events; | global using Microsoft.eShopOnContainers.Services.Basket.API.IntegrationEvents.Events; | ||||
global using Microsoft.eShopOnContainers.Services.Basket.API.Model; | global using Microsoft.eShopOnContainers.Services.Basket.API.Model; | ||||
global using Microsoft.eShopOnContainers.Services.Basket.API.Services; | global using Microsoft.eShopOnContainers.Services.Basket.API.Services; | ||||
global using Microsoft.eShopOnContainers.Services.Basket.API; | |||||
global using Microsoft.Extensions.Configuration; | global using Microsoft.Extensions.Configuration; | ||||
global using Microsoft.Extensions.DependencyInjection; | 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.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 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<IBasketRepository, RedisBasketRepository>(); | ||||
builder.Services.AddTransient<IIdentityService, IdentityService>(); | 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(); | 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.MapGrpcService<BasketService>(); | ||||
app.MapDefaultControllerRoute(); | |||||
app.MapControllers(); | 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": { | "profiles": { | ||||
"IIS Express": { | |||||
"commandName": "IISExpress", | |||||
"launchBrowser": true, | |||||
"launchUrl": "swagger", | |||||
"environmentVariables": { | |||||
"ASPNETCORE_ENVIRONMENT": "Development" | |||||
} | |||||
}, | |||||
"Microsoft.eShopOnContainers.Services.Basket.API": { | |||||
"Basket.API": { | |||||
"commandName": "Project", | "commandName": "Project", | ||||
"launchBrowser": true, | "launchBrowser": true, | ||||
"launchUrl": "http://localhost:55103/", | |||||
"applicationUrl": "http://localhost:5221", | |||||
"environmentVariables": { | "environmentVariables": { | ||||
"Identity__Url": "http://localhost:5223", | |||||
"ASPNETCORE_ENVIRONMENT": "Development" | "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", | "ConnectionString": "127.0.0.1", | ||||
"AzureServiceBusEnabled": false, | "AzureServiceBusEnabled": false, | ||||
"EventBusConnection": "localhost" | "EventBusConnection": "localhost" |
@ -1,30 +1,48 @@ | |||||
{ | { | ||||
"Serilog": { | |||||
"SeqServerUrl": null, | |||||
"LogstashgUrl": null, | |||||
"MinimumLevel": { | |||||
"Logging": { | |||||
"LogLevel": { | |||||
"Default": "Information", | "Default": "Information", | ||||
"Override": { | |||||
"Microsoft": "Warning", | |||||
"Microsoft.eShopOnContainers": "Information", | |||||
"System": "Warning" | |||||
} | |||||
"Microsoft.AspNetCore": "Warning" | |||||
} | } | ||||
}, | }, | ||||
"Kestrel": { | "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.Builder; | ||||
global using Microsoft.AspNetCore.Hosting; | global using Microsoft.AspNetCore.Hosting; | ||||
global using Microsoft.AspNetCore.Http; | global using Microsoft.AspNetCore.Http; | ||||
global using Microsoft.AspNetCore.Routing; | |||||
global using Microsoft.AspNetCore.TestHost; | 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.Model; | ||||
global using Microsoft.eShopOnContainers.Services.Basket.API; | |||||
global using Microsoft.Extensions.Configuration; | global using Microsoft.Extensions.Configuration; | ||||
global using Microsoft.Extensions.DependencyInjection; | global using Microsoft.Extensions.DependencyInjection; | ||||
global using Microsoft.Extensions.Logging; | global using Microsoft.Extensions.Logging; | ||||
global using StackExchange.Redis; | 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; | 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 Grpc.Core; | ||||
global using Microsoft.AspNetCore.Builder; | |||||
global using Microsoft.AspNetCore.Hosting; | global using Microsoft.AspNetCore.Hosting; | ||||
global using Microsoft.AspNetCore.Http; | 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.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.Design; | ||||
global using Microsoft.EntityFrameworkCore.Metadata.Builders; | global using Microsoft.EntityFrameworkCore.Metadata.Builders; | ||||
global using Microsoft.EntityFrameworkCore; | |||||
global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; | global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions; | ||||
global using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Events; | 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.Services; | ||||
global using Microsoft.eShopOnContainers.BuildingBlocks.IntegrationEventLogEF.Utilities; | 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; | ||||
global using Microsoft.eShopOnContainers.Services.Catalog.API.Infrastructure.EntityConfigurations; | 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.IntegrationEvents.Events; | ||||
global using Microsoft.eShopOnContainers.Services.Catalog.API.Model; | global using Microsoft.eShopOnContainers.Services.Catalog.API.Model; | ||||
global using Microsoft.eShopOnContainers.Services.Catalog.API.ViewModel; | 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.Configuration; | ||||
global using Microsoft.Extensions.DependencyInjection; | global using Microsoft.Extensions.DependencyInjection; | ||||
global using Microsoft.Extensions.Hosting; | |||||
global using Microsoft.Extensions.Logging; | |||||
global using Microsoft.Extensions.Options; | global using Microsoft.Extensions.Options; | ||||
global using Polly.Retry; | |||||
global using Polly; | 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.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.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 context = scope.ServiceProvider.GetRequiredService<CatalogContext>(); | ||||
var env = app.Services.GetService<IWebHostEnvironment>(); | |||||
var settings = app.Services.GetService<IOptions<CatalogSettings>>(); | var settings = app.Services.GetService<IOptions<CatalogSettings>>(); | ||||
var logger = app.Services.GetService<ILogger<CatalogContextSeed>>(); | var logger = app.Services.GetService<ILogger<CatalogContextSeed>>(); | ||||
await context.Database.MigrateAsync(); | 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>(); | var integEventContext = scope.ServiceProvider.GetRequiredService<IntegrationEventLogContext>(); | ||||
await integEventContext.Database.MigrateAsync(); | 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(); |