2023-05-02 08:00:17 -07:00
using System.IdentityModel.Tokens.Jwt ;
using Azure.Identity ;
using HealthChecks.UI.Client ;
using Microsoft.AspNetCore.Builder ;
using Microsoft.AspNetCore.Diagnostics.HealthChecks ;
using Microsoft.AspNetCore.Hosting ;
using Microsoft.AspNetCore.Routing ;
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus ;
using Microsoft.eShopOnContainers.BuildingBlocks.EventBus.Abstractions ;
using Microsoft.eShopOnContainers.BuildingBlocks.EventBusRabbitMQ ;
using Microsoft.eShopOnContainers.BuildingBlocks.EventBusServiceBus ;
using Microsoft.Extensions.Configuration ;
using Microsoft.Extensions.DependencyInjection ;
using Microsoft.Extensions.Diagnostics.HealthChecks ;
using Microsoft.Extensions.Hosting ;
using Microsoft.Extensions.Logging ;
using Microsoft.OpenApi.Models ;
using RabbitMQ.Client ;
using Serilog ;
namespace Services.Common ;
public static class CommonExtensions
{
public static WebApplicationBuilder AddServiceDefaults ( this WebApplicationBuilder builder )
{
// Shared configuration via key vault
builder . Configuration . AddKeyVault ( ) ;
// Shared app insights configuration
builder . Services . AddApplicationInsights ( builder . Configuration ) ;
// Default health checks assume the event bus and self health checks
builder . Services . AddDefaultHealthChecks ( builder . Configuration ) ;
// Configure the default logging for this application
2023-05-04 01:10:34 -07:00
// builder.Host.UseDefaultSerilog(builder.Configuration, builder.Environment.ApplicationName);
2023-05-02 08:00:17 -07:00
// Customizations for this application
// Add the event bus
builder . Services . AddEventBus ( builder . Configuration ) ;
builder . Services . AddDefaultAuthentication ( builder . Configuration ) ;
builder . Services . AddDefaultOpenApi ( builder . Configuration ) ;
// Add the accessor
builder . Services . AddHttpContextAccessor ( ) ;
return builder ;
}
public static WebApplication UseServiceDefaults ( this WebApplication app )
{
2023-05-02 08:11:55 -07:00
if ( ! app . Environment . IsDevelopment ( ) )
{
app . UseExceptionHandler ( "/Home/Error" ) ;
}
2023-05-02 08:00:17 -07:00
var pathBase = app . Configuration [ "PATH_BASE" ] ;
if ( ! string . IsNullOrEmpty ( pathBase ) )
{
app . UsePathBase ( pathBase ) ;
}
app . UseDefaultOpenApi ( app . Configuration ) ;
app . MapDefaultHealthChecks ( ) ;
return app ;
}
public static IApplicationBuilder UseDefaultOpenApi ( this IApplicationBuilder app , IConfiguration configuration )
{
2023-05-04 01:10:34 -07:00
var openApiSection = configuration . GetSection ( "OpenApi" ) ;
2023-05-03 08:07:50 -07:00
if ( ! openApiSection . Exists ( ) )
{
return app ;
}
2023-05-02 08:00:17 -07:00
app . UseSwagger ( ) ;
app . UseSwaggerUI ( setup = >
{
2023-05-02 21:15:43 -07:00
/// {
/// "OpenApi": {
/// "Endpoint: {
/// "Name":
/// },
/// "Auth": {
/// "ClientId": ..,
/// "AppName": ..
/// }
/// }
/// }
2023-05-02 08:00:17 -07:00
var pathBase = configuration [ "PATH_BASE" ] ;
2023-05-02 22:02:05 -07:00
var authSection = openApiSection . GetSection ( "Auth" ) ;
2023-05-02 08:00:17 -07:00
var endpointSection = openApiSection . GetRequiredSection ( "Endpoint" ) ;
var swaggerUrl = endpointSection [ "Url" ] ? ? $"{(!string.IsNullOrEmpty(pathBase) ? pathBase : string.Empty)}/swagger/v1/swagger.json" ;
setup . SwaggerEndpoint ( swaggerUrl , endpointSection . GetRequiredValue ( "Name" ) ) ;
2023-05-02 22:02:05 -07:00
if ( authSection . Exists ( ) )
{
setup . OAuthClientId ( authSection . GetRequiredValue ( "ClientId" ) ) ;
setup . OAuthAppName ( authSection . GetRequiredValue ( "AppName" ) ) ;
}
2023-05-02 08:00:17 -07:00
} ) ;
return app ;
}
2023-05-03 08:07:50 -07:00
public static IServiceCollection AddDefaultOpenApi ( this IServiceCollection services , IConfiguration configuration )
{
2023-05-04 01:10:34 -07:00
var openApi = configuration . GetSection ( "OpenApi" ) ;
2023-05-03 08:07:50 -07:00
if ( ! openApi . Exists ( ) )
2023-05-02 08:00:17 -07:00
{
2023-05-03 08:07:50 -07:00
return services ;
}
2023-05-02 08:00:17 -07:00
2023-05-03 08:07:50 -07:00
return services . AddSwaggerGen ( options = >
{
2023-05-02 08:00:17 -07:00
/// {
/// "OpenApi": {
/// "Document": {
/// "Title": ..
/// "Version": ..
/// "Description": ..
/// }
/// }
/// }
2023-05-02 21:15:43 -07:00
var document = openApi . GetRequiredSection ( "Document" ) ;
2023-05-02 08:00:17 -07:00
2023-05-02 21:15:43 -07:00
var version = document . GetRequiredValue ( "Version" ) ? ? "v1" ;
2023-05-02 08:00:17 -07:00
options . SwaggerDoc ( version , new OpenApiInfo
{
2023-05-02 21:15:43 -07:00
Title = document . GetRequiredValue ( "Title" ) ,
2023-05-02 08:00:17 -07:00
Version = version ,
2023-05-02 21:15:43 -07:00
Description = document . GetRequiredValue ( "Description" )
2023-05-02 08:00:17 -07:00
} ) ;
2023-05-02 21:15:43 -07:00
var identitySection = configuration . GetSection ( "Identity" ) ;
2023-05-02 22:02:05 -07:00
if ( ! identitySection . Exists ( ) )
2023-05-02 21:15:43 -07:00
{
// No identity section, so no authentication open api definition
return ;
}
// {
// "Identity": {
// "ExternalUrl": "http://identity",
// "Scopes": {
// "basket": "Basket API"
// }
// }
// }
var identityUrlExternal = identitySection . GetRequiredValue ( "ExternalUrl" ) ;
2023-05-04 01:10:34 -07:00
var scopes = identitySection . GetRequiredSection ( "Scopes" ) . GetChildren ( ) . ToDictionary ( p = > p . Key , p = > p . Value ) ;
2023-05-02 08:00:17 -07:00
options . AddSecurityDefinition ( "oauth2" , new OpenApiSecurityScheme
{
Type = SecuritySchemeType . OAuth2 ,
Flows = new OpenApiOAuthFlows ( )
{
Implicit = new OpenApiOAuthFlow ( )
{
AuthorizationUrl = new Uri ( $"{identityUrlExternal}/connect/authorize" ) ,
TokenUrl = new Uri ( $"{identityUrlExternal}/connect/token" ) ,
2023-05-02 21:15:43 -07:00
Scopes = scopes ,
2023-05-02 08:00:17 -07:00
}
}
} ) ;
options . OperationFilter < AuthorizeCheckOperationFilter > ( ) ;
} ) ;
2023-05-03 08:07:50 -07:00
}
2023-05-02 08:00:17 -07:00
public static IServiceCollection AddDefaultAuthentication ( this IServiceCollection services , IConfiguration configuration )
{
2023-05-02 11:11:13 -07:00
// {
// "Identity": {
// "Url": "http://identity",
2023-05-04 01:10:34 -07:00
// "Audience": "basket"
2023-05-02 11:11:13 -07:00
// }
// }
var identitySection = configuration . GetSection ( "Identity" ) ;
2023-05-02 22:02:05 -07:00
if ( ! identitySection . Exists ( ) )
2023-05-02 11:11:13 -07:00
{
// No identity section, so no authentication
return services ;
}
2023-05-02 08:00:17 -07:00
// prevent from mapping "sub" claim to nameidentifier.
JwtSecurityTokenHandler . DefaultInboundClaimTypeMap . Remove ( "sub" ) ;
services . AddAuthentication ( ) . AddJwtBearer ( options = >
{
2023-05-02 11:11:13 -07:00
var identityUrl = identitySection . GetRequiredValue ( "Url" ) ;
var audience = identitySection . GetRequiredValue ( "Audience" ) ;
2023-05-02 08:00:17 -07:00
options . Authority = identityUrl ;
options . RequireHttpsMetadata = false ;
options . Audience = audience ;
options . TokenValidationParameters . ValidateAudience = false ;
} ) ;
return services ;
}
public static ConfigurationManager AddKeyVault ( this ConfigurationManager configuration )
{
2023-05-03 08:07:50 -07:00
// {
// "Vault": {
// "Name": "myvault",
// "TenantId": "mytenantid",
// "ClientId": "myclientid",
// }
// }
2023-05-02 08:00:17 -07:00
2023-05-03 08:07:50 -07:00
var vaultSection = configuration . GetSection ( "Vault" ) ;
2023-05-02 08:00:17 -07:00
2023-05-03 08:07:50 -07:00
if ( ! vaultSection . Exists ( ) )
{
return configuration ;
}
2023-05-02 08:00:17 -07:00
2023-05-03 08:07:50 -07:00
var credential = new ClientSecretCredential (
vaultSection . GetRequiredValue ( "TenantId" ) ,
vaultSection . GetRequiredValue ( "ClientId" ) ,
vaultSection . GetRequiredValue ( "ClientSecret" ) ) ;
2023-05-02 08:00:17 -07:00
2023-05-03 08:07:50 -07:00
var name = vaultSection . GetRequiredValue ( "Name" ) ;
configuration . AddAzureKeyVault ( new Uri ( $"https://{name}.vault.azure.net/" ) , credential ) ;
2023-05-02 08:00:17 -07:00
return configuration ;
}
public static IServiceCollection AddApplicationInsights ( this IServiceCollection services , IConfiguration configuration )
{
2023-05-02 22:02:05 -07:00
var appInsightsSection = configuration . GetSection ( "ApplicationInsights" ) ;
// No instrumentation key, so no application insights
if ( string . IsNullOrEmpty ( appInsightsSection [ "InstrumentationKey" ] ) )
{
return services ;
}
2023-05-02 08:00:17 -07:00
services . AddApplicationInsightsTelemetry ( configuration ) ;
services . AddApplicationInsightsKubernetesEnricher ( ) ;
return services ;
}
public static IHealthChecksBuilder AddDefaultHealthChecks ( this IServiceCollection services , IConfiguration configuration )
{
var hcBuilder = services . AddHealthChecks ( ) ;
// Health check for the application itself
hcBuilder . AddCheck ( "self" , ( ) = > HealthCheckResult . Healthy ( ) ) ;
// {
// "EventBus": {
// "ProviderName": "ServiceBus | RabbitMQ",
2023-05-04 02:08:09 -07:00
2023-05-02 08:00:17 -07:00
// }
// }
var eventBusSection = configuration . GetRequiredSection ( "EventBus" ) ;
2023-05-04 02:08:09 -07:00
var eventBusConnectionString = configuration . GetRequiredConnectionString ( "EventBus" ) ;
2023-05-02 08:00:17 -07:00
2023-05-02 22:02:05 -07:00
return eventBusSection [ "ProviderName" ] ? . ToLowerInvariant ( ) switch
2023-05-02 08:00:17 -07:00
{
"servicebus" = > hcBuilder . AddAzureServiceBusTopic (
eventBusConnectionString ,
topicName : "eshop_event_bus" ,
name : "servicebus-check" ,
tags : new string [ ] { "servicebus" } ) ,
_ = > hcBuilder . AddRabbitMQ (
$"amqp://{eventBusConnectionString}" ,
name : "rabbitmqbus-check" ,
tags : new string [ ] { "rabbitmqbus" } )
} ;
}
public static IServiceCollection AddEventBus ( this IServiceCollection services , IConfiguration configuration )
{
2023-05-04 02:08:09 -07:00
// {
// "ConnectionStrings": {
// "EventBus": "..."
// },
2023-05-02 08:00:17 -07:00
// {
// "EventBus": {
// "ProviderName": "ServiceBus | RabbitMQ",
// ...
// }
// }
// {
// "EventBus": {
// "ProviderName": "ServiceBus",
// "SubscriptionClientName": "eshop_event_bus"
// }
// }
// {
// "EventBus": {
// "ProviderName": "RabbitMQ",
// "SubscriptionClientName": "...",
// "UserName": "...",
// "Password": "...",
// "RetryCount": 1
// }
// }
var eventBusSection = configuration . GetRequiredSection ( "EventBus" ) ;
if ( string . Equals ( eventBusSection [ "ProviderName" ] , "ServiceBus" , StringComparison . OrdinalIgnoreCase ) )
{
services . AddSingleton < IServiceBusPersisterConnection > ( sp = >
{
2023-05-04 02:08:09 -07:00
var serviceBusConnectionString = configuration . GetRequiredConnectionString ( "EventBus" ) ;
2023-05-02 08:00:17 -07:00
return new DefaultServiceBusPersisterConnection ( serviceBusConnectionString ) ;
} ) ;
services . AddSingleton < IEventBus , EventBusServiceBus > ( sp = >
{
var serviceBusPersisterConnection = sp . GetRequiredService < IServiceBusPersisterConnection > ( ) ;
var logger = sp . GetRequiredService < ILogger < EventBusServiceBus > > ( ) ;
var eventBusSubscriptionsManager = sp . GetRequiredService < IEventBusSubscriptionsManager > ( ) ;
string subscriptionName = eventBusSection . GetRequiredValue ( "SubscriptionClientName" ) ;
return new EventBusServiceBus ( serviceBusPersisterConnection , logger ,
eventBusSubscriptionsManager , sp , subscriptionName ) ;
} ) ;
}
else
{
services . AddSingleton < IRabbitMQPersistentConnection > ( sp = >
{
var logger = sp . GetRequiredService < ILogger < DefaultRabbitMQPersistentConnection > > ( ) ;
var factory = new ConnectionFactory ( )
{
2023-05-04 02:08:09 -07:00
HostName = configuration . GetRequiredConnectionString ( "EventBus" ) ,
2023-05-02 08:00:17 -07:00
DispatchConsumersAsync = true
} ;
if ( ! string . IsNullOrEmpty ( eventBusSection [ "UserName" ] ) )
{
factory . UserName = eventBusSection [ "UserName" ] ;
}
if ( ! string . IsNullOrEmpty ( eventBusSection [ "Password" ] ) )
{
factory . Password = eventBusSection [ "Password" ] ;
}
var retryCount = eventBusSection . GetValue ( "RetryCount" , 5 ) ;
return new DefaultRabbitMQPersistentConnection ( factory , logger , retryCount ) ;
} ) ;
services . AddSingleton < IEventBus , EventBusRabbitMQ > ( sp = >
{
var subscriptionClientName = eventBusSection . GetRequiredValue ( "SubscriptionClientName" ) ;
var rabbitMQPersistentConnection = sp . GetRequiredService < IRabbitMQPersistentConnection > ( ) ;
var logger = sp . GetRequiredService < ILogger < EventBusRabbitMQ > > ( ) ;
var eventBusSubscriptionsManager = sp . GetRequiredService < IEventBusSubscriptionsManager > ( ) ;
var retryCount = eventBusSection . GetValue ( "RetryCount" , 5 ) ;
return new EventBusRabbitMQ ( rabbitMQPersistentConnection , logger , sp , eventBusSubscriptionsManager , subscriptionClientName , retryCount ) ;
} ) ;
}
services . AddSingleton < IEventBusSubscriptionsManager , InMemoryEventBusSubscriptionsManager > ( ) ;
return services ;
}
public static void UseDefaultSerilog ( this IHostBuilder builder , IConfiguration configuration , string name )
{
builder . UseSerilog ( CreateSerilogLogger ( configuration ) ) ;
Serilog . ILogger CreateSerilogLogger ( IConfiguration configuration )
{
var seqServerUrl = configuration [ "Serilog:SeqServerUrl" ] ;
var logstashUrl = configuration [ "Serilog:LogstashgUrl" ] ;
2023-05-04 01:10:34 -07:00
var loggingConfiguration = new LoggerConfiguration ( )
2023-05-02 08:00:17 -07:00
. MinimumLevel . Verbose ( )
. Enrich . WithProperty ( "ApplicationContext" , name )
. Enrich . FromLogContext ( )
. WriteTo . Console ( )
2023-05-04 01:10:34 -07:00
. ReadFrom . Configuration ( configuration ) ;
if ( ! string . IsNullOrEmpty ( seqServerUrl ) )
{
loggingConfiguration . WriteTo . Seq ( seqServerUrl ) ;
}
if ( ! string . IsNullOrEmpty ( logstashUrl ) )
{
loggingConfiguration . WriteTo . Http ( logstashUrl , null ) ;
}
return loggingConfiguration . CreateLogger ( ) ;
2023-05-02 08:00:17 -07:00
}
}
public static void MapDefaultHealthChecks ( this IEndpointRouteBuilder routes )
{
routes . MapHealthChecks ( "/hc" , new HealthCheckOptions ( )
{
Predicate = _ = > true ,
ResponseWriter = UIResponseWriter . WriteHealthCheckUIResponse
} ) ;
routes . MapHealthChecks ( "/liveness" , new HealthCheckOptions
{
Predicate = r = > r . Name . Contains ( "self" )
} ) ;
}
private static string GetRequiredValue ( this IConfiguration configuration , string name ) = >
2023-05-02 22:02:05 -07:00
configuration [ name ] ? ? throw new InvalidOperationException ( $"Configuration missing value for: {(configuration is IConfigurationSection s ? s.Path + " : " + name : name)}" ) ;
2023-05-04 02:08:09 -07:00
private static string GetRequiredConnectionString ( this IConfiguration configuration , string name ) = >
configuration . GetConnectionString ( name ) ? ? throw new InvalidOperationException ( $"Configuration missing value for: {(configuration is IConfigurationSection s ? s.Path + " : ConnectionStrings : " + name : " ConnectionStrings : " + name)}" ) ;
2023-05-02 08:00:17 -07:00
}