Moved DI extensions to Appl;ication layer

This commit is contained in:
2026-08-30 14:49:10 +02:00
parent f51b1feb4b
commit 0f7f875abe
12 changed files with 51 additions and 52 deletions
@@ -16,4 +16,33 @@ public static class General
return services;
}
public static Guid GetUserId(this HttpContext context) => (Guid)context.Items[Domain.Extensions.Constants.UserIdItemKey]!;
public static string GetUserEmail(this HttpContext context) => (string)context.Items[Domain.Extensions.Constants.EmaiItemKey]!;
public static (bool Decoded, Guid userId) GetUserId(this ClaimsPrincipal principal)
{
var sidClaim = principal.FindFirst("sid")?.Value ?? principal.FindFirst(ClaimTypes.Sid)?.Value;
var decoded = Guid.TryParse(sidClaim, out var userId);
return (decoded, userId);
}
public static (bool Decoded, Guid email) GetUserEmail(this ClaimsPrincipal principal)
{
var emailClaim = principal.FindFirst("email")?.Value ?? principal.FindFirst(ClaimTypes.Email)?.Value;
var decoded = Guid.TryParse(emailClaim, out var email);
return (decoded, email);
}
public static Microsoft.AspNetCore.Mvc.ProblemDetails ToProblems(this IReadOnlyList<IError> errors) => new()
{
Detail = string.Join(',', errors.Select(e => e.Message)),
Status = StatusCodes.Status400BadRequest,
Title = errors[0]?.Message,
};
}
@@ -0,0 +1,58 @@
using OpenTelemetry.Exporter;
using OpenTelemetry.Logs;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
using OpenTelemetry.Metrics;
namespace PostFundManagement.Application.Extensions;
public static class Otel
{
public static WebApplicationBuilder AddWebTelemetry(this WebApplicationBuilder builder)
{
var serviceName = builder.Configuration.GetValue<string>("Monitoring:ServiceName") ?? "Pfm";
var endpoint = builder.Configuration.GetValue<string>("Monitoring:Address")!;
var apiKey = builder.Configuration.GetValue<string>("Monitoring:ApiKey");
var resourceBuilder = ResourceBuilder.CreateDefault()
.AddService(serviceName);
var otlpHeaders = !string.IsNullOrEmpty(apiKey) ? $"x-otlp-api-key={apiKey}" : null;
builder.Logging.AddOpenTelemetry(logging =>
{
logging.SetResourceBuilder(resourceBuilder);
logging.AddOtlpExporter(opt =>
{
opt.Endpoint = new Uri(endpoint);
opt.Protocol = OtlpExportProtocol.Grpc;
opt.Headers = otlpHeaders;
});
});
builder.Services.AddOpenTelemetry()
.WithTracing(tracing => tracing
.SetResourceBuilder(resourceBuilder)
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddOtlpExporter(opt =>
{
opt.Endpoint = new Uri(endpoint);
opt.Protocol = OtlpExportProtocol.Grpc;
opt.Headers = otlpHeaders;
}))
.WithMetrics(metrics => metrics
.SetResourceBuilder(resourceBuilder)
.AddMeter(serviceName)
.AddAspNetCoreInstrumentation()
.AddRuntimeInstrumentation()
.AddOtlpExporter(opt =>
{
opt.Endpoint = new Uri(endpoint);
opt.Protocol = OtlpExportProtocol.Grpc;
opt.Headers = otlpHeaders;
}));
return builder;
}
}
@@ -0,0 +1,27 @@
using PostFundManagement.Infrastructure.Database;
using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Application.Extensions;
public static class Postgres
{
public static IServiceCollection AddDataProtectionDatabase(this IServiceCollection services, IConfiguration configuration)
{
var connectionString = configuration.GetConnectionString(DatabaseConfigName);
services.AddPooledDbContextFactory<DataProtectionDbContext>(options =>
options.UseNpgsql(connectionString));
return services;
}
public static IServiceCollection AddApplicationDbContext(this IServiceCollection services, IConfiguration configuration)
{
var connectionString = configuration.GetConnectionString(DatabaseConfigName)
?? throw new InvalidOperationException($"Connection string '{DatabaseConfigName}' was not found in configuration.");
services.AddPooledDbContextFactory<ApplicationDbContext>(builder => builder.UseNpgsql(connectionString));
return services;
}
}
@@ -0,0 +1,108 @@
using PostFundManagement.Domain.Abstractions;
using PostFundManagement.Domain.Quartz;
using static PostFundManagement.Domain.Extensions.Constants;
namespace PostFundManagement.Domain.Extensions;
public static class Quartz
{
public static IServiceCollection AddQuartzSchedulerClient(this IServiceCollection services, string schedulerName, IConfiguration configuration)
{
var connectionString = configuration.GetConnectionString(DatabaseConfigName);
services.ConfigureCommon();
services.AddQuartz(config =>
{
config.SchedulerName = schedulerName;
config.SchedulerId = "AUTO";
config.UseSimpleTypeLoader();
config.UseDefaultThreadPool(options => options.MaxConcurrency = 0);
config.UseTimeZoneConverter();
config.UsePersistentStore(storage =>
{
storage.PerformSchemaValidation = false;
storage.UseSystemTextJsonSerializer();
storage.SetProperty("quartz.jobStore.clustered", "true");
storage.UsePostgres(options =>
{
options.ConnectionString = connectionString!;
options.TablePrefix = "quartz.qrtz_";
});
storage.UseClustering(cluster =>
{
cluster.CheckinInterval = TimeSpan.FromSeconds(30);
cluster.CheckinMisfireThreshold = TimeSpan.FromSeconds(90);
});
});
});
return services;
}
public static IServiceCollection AddQuartzScheduler(this IServiceCollection services, string schedulerName, IConfiguration configuration)
{
var connectionString = configuration.GetConnectionString(DatabaseConfigName);
services.ConfigureCommon();
services.AddQuartzHostedService(options => options.WaitForJobsToComplete = true);
services.AddQuartz(config =>
{
config.SchedulerName = schedulerName;
config.SchedulerId = "AUTO";
config.InterruptJobsOnShutdown = true;
config.InterruptJobsOnShutdownWithWait = true;
config.MaxBatchSize = 5;
config.UseSimpleTypeLoader();
config.UseDefaultThreadPool(options => options.MaxConcurrency = 1);
config.UseTimeZoneConverter();
config.SetProperty("quartz.jobStore.misfireThreshold", TimeSpan.FromMinutes(2).TotalMilliseconds.ToString(CultureInfo.InvariantCulture));
config.UsePersistentStore(storage =>
{
storage.PerformSchemaValidation = false;
storage.UseSystemTextJsonSerializer();
storage.SetProperty("quartz.jobStore.clustered", "true");
storage.UsePostgres(options =>
{
options.ConnectionString = connectionString!;
options.TablePrefix = "quartz.qrtz_";
});
storage.UseClustering(cluster =>
{
cluster.CheckinInterval = TimeSpan.FromSeconds(30);
cluster.CheckinMisfireThreshold = TimeSpan.FromSeconds(90);
});
});
});
return services;
}
private static IServiceCollection ConfigureCommon(this IServiceCollection services)
{
services.Configure<QuartzOptions>(options =>
{
options.Scheduling.IgnoreDuplicates = true;
options.Scheduling.OverWriteExistingData = true;
options["quartz.plugin.jobHistory.type"] = "Quartz.Plugin.History.LoggingJobHistoryPlugin, Quartz.Plugins";
options["quartz.plugin.triggerHistory.type"] = "Quartz.Plugin.History.LoggingTriggerHistoryPlugin, Quartz.Plugins";
});
services.AddTransient<RetryJobListener>();
services.AddTransient<IJobOrchestrator, JobOrchestrator>();
return services;
}
}
@@ -0,0 +1,152 @@
using PostFundManagement.Application.Shared;
using PostFundManagement.Domain.Api.Configuration;
using PostFundManagement.Domain.Sdk;
using PostFundManagement.Infrastructure.Database;
namespace PostFundManagement.Application.Extensions;
public static class Security
{
public static IServiceCollection AddSecurityApiSdk(this IServiceCollection services, IConfiguration configuration)
{
var configSection = configuration.GetSection(nameof(SecurityClientSettings));
var authOptions = new SecurityClientSettings();
configSection.Bind(authOptions);
services.Configure<SecurityClientSettings>(configSection);
if (string.IsNullOrWhiteSpace(authOptions.Authority))
return services;
if (!authOptions.Authority.EndsWith("/", StringComparison.Ordinal)) authOptions.Authority += "/";
services.AddRefitClient<ISecurityConnectApi>()
.ConfigureHttpClient(config =>
{
config.BaseAddress = new Uri(authOptions.Authority);
config.Timeout = TimeSpan.FromSeconds(15);
})
.AddStandardResilienceHandler(options =>
{
options.Retry.MaxRetryAttempts = 3;
options.Retry.Delay = TimeSpan.FromSeconds(1);
options.Retry.BackoffType = Polly.DelayBackoffType.Exponential;
});
services.AddScoped<TokenService>();
return services;
}
public static IServiceCollection AddWebSecurity(this IServiceCollection services, IConfiguration configuration)
{
services.AddAuthorization();
var certString = configuration["DataProtection:Certificate"] ?? configuration["DataProtection__Certificate"];
var certPassword = configuration["DataProtection:Password"] ?? configuration["DataProtection__Password"];
if (string.IsNullOrEmpty(certString))
throw new InvalidOperationException("Data Protection Certificate configuration is missing.");
var certificate = X509CertificateLoader.LoadPkcs12(Convert.FromBase64String(certString), certPassword);
services.AddDataProtection().PersistKeysToDbContext<DataProtectionDbContext>()
.ProtectKeysWithCertificate(certificate)
.SetApplicationName("PfmApp");
services.Configure<DataProtectionOptions>(options => options.ApplicationDiscriminator = "PfmApp");
services.ConfigureCookieOidcSameSiteSupport();
var configSection = configuration.GetSection(nameof(SecurityClientSettings));
var authOptions = new SecurityClientSettings();
configSection.Bind(authOptions);
services.Configure<SecurityClientSettings>(configSection);
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
{
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
options.Cookie.SameSite = SameSiteMode.Lax;
options.Cookie.Name = "PfmApp.Session";
})
.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
options.Authority = authOptions.Authority;
options.ClientId = authOptions.ClientId;
options.ClientSecret = authOptions.ClientSecret;
options.ResponseType = "code";
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.CorrelationCookie.SecurePolicy = CookieSecurePolicy.Always;
options.CorrelationCookie.SameSite = SameSiteMode.None;
options.NonceCookie.SecurePolicy = CookieSecurePolicy.Always;
options.NonceCookie.SameSite = SameSiteMode.None;
options.ForwardSignOut = CookieAuthenticationDefaults.AuthenticationScheme;
options.Scope.Clear();
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("email");
options.Events = new OpenIdConnectEvents
{
OnRedirectToIdentityProviderForSignOut = context =>
{
var idToken = context.ProtocolMessage.IdTokenHint;
if (string.IsNullOrEmpty(idToken))
{
var tokens = context.Properties.GetTokens();
var idTokenItem = tokens.FirstOrDefault(t => string.Equals(t.Name, "id_token", StringComparison.Ordinal));
if (idTokenItem != null) context.ProtocolMessage.IdTokenHint = idTokenItem.Value;
}
return Task.CompletedTask;
},
};
});
services.AddCascadingAuthenticationState();
return services;
}
public static IServiceCollection AddApiSecurity(this IServiceCollection services, IConfiguration configuration)
{
var configSection = configuration.GetSection(nameof(SecuritySettings));
var authOptions = new SecuritySettings();
configSection.Bind(authOptions);
services.Configure<SecuritySettings>(configSection);
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = authOptions.Authority;
options.Audience = authOptions.Audience;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidIssuer = authOptions.Authority,
ValidateAudience = true,
ValidateIssuer = true,
};
});
services.AddAuthorization();
return services;
}
}