@ -1,49 +1,23 @@
var builder = WebApplication . CreateBuilder ( args ) ;
using Services.Common ;
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 ) ;
}
var builder = WebApplication . CreateBuilder ( args ) ;
builder . WebHost . ConfigureKestrel ( options = >
{
var httpPort = builder . Configuration . GetValue ( "PORT" , 8 0 ) ;
options . Listen ( IPAddress . Any , httpPort , listenOptions = >
{
listenOptions . Protocols = HttpProtocols . Http1AndHttp2 ;
} ) ;
builder . AddServiceDefaults ( ) ;
var grpcPort = builder . Configuration . GetValue ( "GRPC_PORT" , 5 0 0 1 ) ;
options . Listen ( IPAddress . Any , grpcPort , listenOptions = >
{
listenOptions . Protocols = HttpProtocols . Http2 ;
} ) ;
} ) ;
builder . Services . AddGrpc ( ) ;
builder . Services . AddControllers ( ) ;
builder . Services . AddGrpc ( options = > options . EnableDetailedErrors = true ) ;
builder . Services . AddApplicationInsightsTelemetry ( builder . Configuration ) ;
builder . Services . AddApplicationInsightsKubernetesEnricher ( ) ;
builder . Services
. AddCustomMvc ( )
. AddHealthChecks ( builder . Configuration )
. AddCustomDbContext ( builder . Configuration )
. AddCustomSwagger ( builder . Configuration )
. AddCustomAuthentication ( builder . Configuration )
. AddCustomAuthorization ( builder . Configuration )
. AddCustomIntegrations ( builder . Configuration )
. AddCustomConfiguration ( builder . Configuration )
. AddEventBus ( builder . Configuration ) ;
builder . Services . AddHealthChecks ( builder . Configuration ) ;
builder . Services . AddDbContexts ( builder . Configuration ) ;
builder . Services . AddCustomIntegrations ( builder . Configuration ) ;
builder . Services . AddCustomConfiguration ( builder . Configuration ) ;
var services = builder . Services ;
services . AddMediatR ( cfg = >
{
cfg . RegisterServicesFromAssemblyContaining ( typeof ( Program ) ) ;
cfg . AddOpenBehavior ( typeof ( LoggingBehavior < , > ) ) ;
cfg . AddOpenBehavior ( typeof ( ValidatorBehavior < , > ) ) ;
cfg . AddOpenBehavior ( typeof ( TransactionBehavior < , > ) ) ;
@ -55,9 +29,7 @@ services.AddSingleton<IValidator<CreateOrderCommand>, CreateOrderCommandValidato
services . AddSingleton < IValidator < IdentifiedCommand < CreateOrderCommand , bool > > , IdentifiedCommandValidator > ( ) ;
services . AddSingleton < IValidator < ShipOrderCommand > , ShipOrderCommandValidator > ( ) ;
var queriesConnectionString = builder . Configuration [ "ConnectionString" ] ;
services . AddScoped < IOrderQueries > ( sp = > new OrderQueries ( queriesConnectionString ) ) ;
services . AddScoped < IOrderQueries > ( sp = > new OrderQueries ( builder . Configuration . GetConnectionString ( "OrderingDB" ) ) ) ;
services . AddScoped < IBuyerRepository , BuyerRepository > ( ) ;
services . AddScoped < IOrderRepository , OrderRepository > ( ) ;
services . AddScoped < IRequestManager , RequestManager > ( ) ;
@ -71,55 +43,27 @@ services.AddSingleton<IIntegrationEventHandler<OrderStockRejectedIntegrationEven
services . AddSingleton < IIntegrationEventHandler < UserCheckoutAcceptedIntegrationEvent > , UserCheckoutAcceptedIntegrationEventHandler > ( ) ;
var app = builder . Build ( ) ;
if ( ! app . Environment . IsDevelopment ( ) )
{
app . UseExceptionHandler ( "/Home/Error" ) ;
}
var pathBase = app . Configuration [ "PATH_BASE" ] ;
if ( ! string . IsNullOrEmpty ( pathBase ) )
if ( ! await app . CheckHealthAsync ( ) )
{
app . UsePathBase ( pathBase ) ;
return ;
}
app . UseSwagger ( ) . UseSwaggerUI ( c = >
{
c . SwaggerEndpoint ( $"{(!string.IsNullOrEmpty(pathBase) ? pathBase : string.Empty)}/swagger/v1/swagger.json" , "Ordering.API V1" ) ;
c . OAuthClientId ( "orderingswaggerui" ) ;
c . OAuthAppName ( "Ordering Swagger UI" ) ;
} ) ;
app . UseServiceDefaults ( ) ;
app . MapGet ( "/" , ( ) = > Results . Redirect ( "/swagger" ) ) ;
app . UseRouting ( ) ;
app . UseCors ( "CorsPolicy" ) ;
app . UseAuthentication ( ) ;
app . UseAuthorization ( ) ;
app . MapGrpcService < OrderingService > ( ) ;
app . MapDefaultControllerRoute ( ) ;
app . MapControllers ( ) ;
app . MapGet ( "/_proto/" , async ctx = >
{
ctx . Response . ContentType = "text/plain" ;
using var fs = new FileStream ( Path . Combine ( app . Environment . ContentRootPath , "Proto" , "basket.proto" ) , FileMode . Open , FileAccess . Read ) ;
using var sr = new StreamReader ( fs ) ;
while ( ! sr . EndOfStream )
{
var line = await sr . ReadLineAsync ( ) ;
if ( line ! = "/* >>" | | line ! = "<< */" )
{
await ctx . Response . WriteAsync ( line ) ;
}
}
} ) ;
app . MapHealthChecks ( "/hc" , new HealthCheckOptions ( )
{
Predicate = _ = > true ,
ResponseWriter = UIResponseWriter . WriteHealthCheckUIResponse
} ) ;
app . MapHealthChecks ( "/liveness" , new HealthCheckOptions
{
Predicate = r = > r . Name . Contains ( "self" )
} ) ;
ConfigureEventBus ( app ) ;
var eventBus = app . Services . GetRequiredService < IEventBus > ( ) ;
eventBus . Subscribe < UserCheckoutAcceptedIntegrationEvent , IIntegrationEventHandler < UserCheckoutAcceptedIntegrationEvent > > ( ) ;
eventBus . Subscribe < GracePeriodConfirmedIntegrationEvent , IIntegrationEventHandler < GracePeriodConfirmedIntegrationEvent > > ( ) ;
eventBus . Subscribe < OrderStockConfirmedIntegrationEvent , IIntegrationEventHandler < OrderStockConfirmedIntegrationEvent > > ( ) ;
eventBus . Subscribe < OrderStockRejectedIntegrationEvent , IIntegrationEventHandler < OrderStockRejectedIntegrationEvent > > ( ) ;
eventBus . Subscribe < OrderPaymentFailedIntegrationEvent , IIntegrationEventHandler < OrderPaymentFailedIntegrationEvent > > ( ) ;
eventBus . Subscribe < OrderPaymentSucceededIntegrationEvent , IIntegrationEventHandler < OrderPaymentSucceededIntegrationEvent > > ( ) ;
using ( var scope = app . Services . CreateScope ( ) )
{
@ -135,284 +79,3 @@ using (var scope = app.Services.CreateScope())
}
await app . RunAsync ( ) ;
void ConfigureEventBus ( IApplicationBuilder app )
{
var eventBus = app . ApplicationServices . GetRequiredService < IEventBus > ( ) ;
eventBus . Subscribe < UserCheckoutAcceptedIntegrationEvent , IIntegrationEventHandler < UserCheckoutAcceptedIntegrationEvent > > ( ) ;
eventBus . Subscribe < GracePeriodConfirmedIntegrationEvent , IIntegrationEventHandler < GracePeriodConfirmedIntegrationEvent > > ( ) ;
eventBus . Subscribe < OrderStockConfirmedIntegrationEvent , IIntegrationEventHandler < OrderStockConfirmedIntegrationEvent > > ( ) ;
eventBus . Subscribe < OrderStockRejectedIntegrationEvent , IIntegrationEventHandler < OrderStockRejectedIntegrationEvent > > ( ) ;
eventBus . Subscribe < OrderPaymentFailedIntegrationEvent , IIntegrationEventHandler < OrderPaymentFailedIntegrationEvent > > ( ) ;
eventBus . Subscribe < OrderPaymentSucceededIntegrationEvent , IIntegrationEventHandler < OrderPaymentSucceededIntegrationEvent > > ( ) ;
}
static class CustomExtensionsMethods
{
public static IServiceCollection AddCustomMvc ( this IServiceCollection services )
{
// Add framework services.
services . AddControllers ( options = >
{
options . Filters . Add ( typeof ( HttpGlobalExceptionFilter ) ) ;
} )
// Added for functional tests
. AddApplicationPart ( typeof ( OrdersController ) . Assembly )
. AddJsonOptions ( options = > options . JsonSerializerOptions . WriteIndented = true ) ;
services . AddCors ( options = >
{
options . AddPolicy ( "CorsPolicy" ,
builder = > builder
. SetIsOriginAllowed ( ( host ) = > true )
. AllowAnyMethod ( )
. AllowAnyHeader ( )
. AllowCredentials ( ) ) ;
} ) ;
return services ;
}
public static IServiceCollection AddHealthChecks ( this IServiceCollection services , IConfiguration configuration )
{
var hcBuilder = services . AddHealthChecks ( ) ;
hcBuilder . AddCheck ( "self" , ( ) = > HealthCheckResult . Healthy ( ) ) ;
hcBuilder
. AddSqlServer (
configuration [ "ConnectionString" ] ,
name : "OrderingDB-check" ,
tags : new string [ ] { "orderingdb" } ) ;
if ( configuration . GetValue < bool > ( "AzureServiceBusEnabled" ) )
{
hcBuilder
. AddAzureServiceBusTopic (
configuration [ "EventBusConnection" ] ,
topicName : "eshop_event_bus" ,
name : "ordering-servicebus-check" ,
tags : new string [ ] { "servicebus" } ) ;
}
else
{
hcBuilder
. AddRabbitMQ (
$"amqp://{configuration[" EventBusConnection "]}" ,
name : "ordering-rabbitmqbus-check" ,
tags : new string [ ] { "rabbitmqbus" } ) ;
}
return services ;
}
public static IServiceCollection AddCustomDbContext ( this IServiceCollection services , IConfiguration configuration )
{
services . AddDbContext < OrderingContext > ( options = >
{
options . UseSqlServer ( configuration [ "ConnectionString" ] ,
sqlServerOptionsAction : sqlOptions = >
{
sqlOptions . MigrationsAssembly ( typeof ( Program ) . GetTypeInfo ( ) . Assembly . GetName ( ) . Name ) ;
sqlOptions . EnableRetryOnFailure ( maxRetryCount : 1 5 , maxRetryDelay : TimeSpan . FromSeconds ( 3 0 ) , errorNumbersToAdd : null ) ;
} ) ;
} ,
ServiceLifetime . Scoped //Showing explicitly that the DbContext is shared across the HTTP request scope (graph of objects started in the HTTP request)
) ;
services . AddDbContext < 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 : 1 5 , maxRetryDelay : TimeSpan . FromSeconds ( 3 0 ) , errorNumbersToAdd : null ) ;
} ) ;
} ) ;
return services ;
}
public static IServiceCollection AddCustomSwagger ( this IServiceCollection services , IConfiguration configuration )
{
return services . AddSwaggerGen ( options = >
{
options . SwaggerDoc ( "v1" , new OpenApiInfo
{
Title = "eShopOnContainers - Ordering HTTP API" ,
Version = "v1" ,
Description = "The Ordering Service HTTP API"
} ) ;
var identityUrl = configuration . GetSection ( "Identity" ) [ "ExternalUrl" ] ;
options . AddSecurityDefinition ( "oauth2" , new OpenApiSecurityScheme
{
Type = SecuritySchemeType . OAuth2 ,
Flows = new OpenApiOAuthFlows ( )
{
Implicit = new OpenApiOAuthFlow ( )
{
AuthorizationUrl = new Uri ( $"{identityUrl}/connect/authorize" ) ,
TokenUrl = new Uri ( $"{identityUrl}/connect/token" ) ,
Scopes = new Dictionary < string , string > ( )
{
{ "orders" , "Ordering API" }
}
}
}
} ) ;
options . OperationFilter < AuthorizeCheckOperationFilter > ( ) ;
} ) ;
}
public static IServiceCollection AddCustomIntegrations ( this IServiceCollection services , IConfiguration configuration )
{
services . AddSingleton < IHttpContextAccessor , HttpContextAccessor > ( ) ;
services . AddTransient < IIdentityService , IdentityService > ( ) ;
services . AddTransient < Func < DbConnection , IIntegrationEventLogService > > (
sp = > ( DbConnection c ) = > new IntegrationEventLogService ( c ) ) ;
services . AddTransient < IOrderingIntegrationEventService , OrderingIntegrationEventService > ( ) ;
if ( configuration . GetValue < bool > ( "AzureServiceBusEnabled" ) )
{
services . AddSingleton < IServiceBusPersisterConnection > ( sp = >
{
var serviceBusConnectionString = configuration [ "EventBusConnection" ] ;
var subscriptionClientName = configuration [ "SubscriptionClientName" ] ;
return new DefaultServiceBusPersisterConnection ( serviceBusConnectionString ) ;
} ) ;
}
else
{
services . AddSingleton < IRabbitMQPersistentConnection > ( sp = >
{
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 AddCustomConfiguration ( this IServiceCollection services , IConfiguration configuration )
{
services . Configure < OrderingSettings > ( 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 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 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 > ( ) ;
if ( ! int . TryParse ( configuration [ "EventBusRetryCount" ] , out var retryCount ) )
{
retryCount = 5 ;
}
return new EventBusRabbitMQ ( rabbitMQPersistentConnection , logger , sp , eventBusSubscriptionsManager , subscriptionClientName , retryCount ) ;
} ) ;
}
services . AddSingleton < IEventBusSubscriptionsManager , InMemoryEventBusSubscriptionsManager > ( ) ;
return services ;
}
public static IServiceCollection AddCustomAuthentication ( this IServiceCollection services , IConfiguration configuration )
{
// prevent from mapping "sub" claim to nameidentifier.
JwtSecurityTokenHandler . DefaultInboundClaimTypeMap . Remove ( "sub" ) ;
var identityUrl = configuration . GetValue < string > ( "IdentityUrl" ) ;
services . AddAuthentication ( "Bearer" ) . AddJwtBearer ( options = >
{
options . Authority = identityUrl ;
options . RequireHttpsMetadata = false ;
options . Audience = "orders" ;
options . TokenValidationParameters . ValidateAudience = false ;
} ) ;
return services ;
}
public static IServiceCollection AddCustomAuthorization ( this IServiceCollection services , IConfiguration configuration )
{
services . AddAuthorization ( options = >
{
options . AddPolicy ( "ApiScope" , policy = >
{
policy . RequireAuthenticatedUser ( ) ;
policy . RequireClaim ( "scope" , "orders" ) ;
} ) ;
} ) ;
return services ;
}
}