Extract shared configuration helpers into Infrastructure.Configuration library (#248)

This commit is contained in:
2026-08-25 14:16:57 -04:00
committed by GitHub
parent 8a047e8d9f
commit 2b82d1918b
22 changed files with 161 additions and 64 deletions

View File

@@ -18,6 +18,7 @@
<ProjectReference Include="..\..\Domain\Domain.Entities\Domain.Entities.csproj" />
<ProjectReference Include="..\..\Domain\Domain.Exceptions\Domain.Exceptions.csproj" />
<ProjectReference Include="..\..\Database\Database.Connection\Database.Connection.csproj" />
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Configuration\Infrastructure.Configuration.csproj" />
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Jwt\Infrastructure.Jwt.csproj" />
<ProjectReference Include="..\..\Features\Features.Breweries\Features.Breweries.csproj" />
<ProjectReference Include="..\..\Features\Features.UserManagement\Features.UserManagement.csproj" />

View File

@@ -1,5 +1,6 @@
using System.Security.Claims;
using System.Text.Encodings.Web;
using Infrastructure.Configuration;
using Infrastructure.Jwt;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Options;
@@ -32,9 +33,9 @@ public class JwtAuthenticationHandler(
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
// Use the same access-token secret source as TokenService to avoid mismatched validation.
string? secret = configuration["ACCESS_TOKEN_SECRET"];
string? secret = configuration[ConfigurationKeys.AccessTokenSecret];
if (string.IsNullOrWhiteSpace(secret))
secret = configuration["Jwt:SecretKey"];
secret = configuration[ConfigurationKeys.JwtSecretKeyFallback];
if (string.IsNullOrWhiteSpace(secret))
return AuthenticateResult.Fail("JWT secret is not configured");

View File

@@ -38,6 +38,7 @@
<ItemGroup>
<ProjectReference Include="..\API.Core\API.Core.csproj" />
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Configuration\Infrastructure.Configuration.csproj" />
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Email\Infrastructure.Email.csproj" />
<ProjectReference Include="..\..\Features\Features.Emails\Features.Emails.csproj" />
</ItemGroup>

View File

@@ -1,6 +1,7 @@
using System.Text;
using System.Text.Json;
using FluentAssertions;
using Infrastructure.Configuration;
using Infrastructure.Jwt;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
@@ -430,7 +431,7 @@ public class AuthSteps(ScenarioContext scenario)
? user
: throw new InvalidOperationException("registered username not found in scenario");
string secret = GetRequiredConfigValue("ACCESS_TOKEN_SECRET");
string secret = GetRequiredConfigValue(ConfigurationKeys.AccessTokenSecret);
scenario["accessToken"] = GenerateJwtToken(
userId,
username,
@@ -449,7 +450,7 @@ public class AuthSteps(ScenarioContext scenario)
? user
: throw new InvalidOperationException("registered username not found in scenario");
string secret = GetRequiredConfigValue("CONFIRMATION_TOKEN_SECRET");
string secret = GetRequiredConfigValue(ConfigurationKeys.ConfirmationTokenSecret);
scenario["confirmationToken"] = GenerateJwtToken(
userId,
username,
@@ -468,7 +469,7 @@ public class AuthSteps(ScenarioContext scenario)
? user
: throw new InvalidOperationException("registered username not found in scenario");
string secret = GetRequiredConfigValue("CONFIRMATION_TOKEN_SECRET");
string secret = GetRequiredConfigValue(ConfigurationKeys.ConfirmationTokenSecret);
scenario["confirmationToken"] = GenerateJwtToken(
userId,
username,

View File

@@ -1,10 +0,0 @@
using Microsoft.Extensions.Configuration;
namespace Database.Connection;
public static class ConfigurationHelpers
{
public static string GetKeyOrThrow(IConfiguration configuration, string key) =>
configuration[key]
?? throw new InvalidOperationException($"The {key} environment variable is not set.");
}

View File

@@ -11,11 +11,14 @@
<PackageReference Include="Microsoft.SqlServer.Types" Version="160.1000.6" />
<PackageReference
Include="Microsoft.Extensions.Configuration.Abstractions"
Version="9.0.0"
Version="10.0.1"
/>
<PackageReference
Include="Microsoft.Extensions.DependencyInjection.Abstractions"
Version="10.0.0"
/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Configuration\Infrastructure.Configuration.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,4 +1,4 @@
using System.Data.Common;
using Infrastructure.Configuration;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Configuration;
@@ -19,7 +19,7 @@ public class DefaultSqlConnectionFactory(IConfiguration configuration) : ISqlCon
{
get
{
string? fullConnectionString = configuration["DB_CONNECTION_STRING"];
string? fullConnectionString = configuration[ConfigurationKeys.DbConnectionString];
if (!string.IsNullOrEmpty(fullConnectionString))
return fullConnectionString;

View File

@@ -1,3 +1,4 @@
using Infrastructure.Configuration;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Configuration;
@@ -14,9 +15,15 @@ public static class SqlConnectionStringHelper
) =>
new()
{
DataSource = ConfigurationHelpers.GetKeyOrThrow(configuration, "DB_SERVER"),
UserID = ConfigurationHelpers.GetKeyOrThrow(configuration, "DB_USER"),
Password = ConfigurationHelpers.GetKeyOrThrow(configuration, "DB_PASSWORD"),
DataSource = ConfigurationHelpers.GetKeyOrThrow(
configuration,
ConfigurationKeys.DbServer
),
UserID = ConfigurationHelpers.GetKeyOrThrow(configuration, ConfigurationKeys.DbUser),
Password = ConfigurationHelpers.GetKeyOrThrow(
configuration,
ConfigurationKeys.DbPassword
),
TrustServerCertificate = ResolveTrustServerCertificate(configuration),
Encrypt = true,
};
@@ -38,7 +45,10 @@ public static class SqlConnectionStringHelper
try
{
SqlConnectionStringBuilder builder = MakeConfiguredConnectionBuilder(configuration);
builder.InitialCatalog = ConfigurationHelpers.GetKeyOrThrow(configuration, "DB_NAME");
builder.InitialCatalog = ConfigurationHelpers.GetKeyOrThrow(
configuration,
ConfigurationKeys.DbName
);
connectionString = builder.ConnectionString;
return true;
}
@@ -95,7 +105,7 @@ public static class SqlConnectionStringHelper
private static bool ResolveTrustServerCertificate(IConfiguration configuration)
{
return !bool.TryParse(
configuration["DB_TRUST_SERVER_CERTIFICATE"],
configuration[ConfigurationKeys.DbTrustServerCertificate],
out bool trustServerCertificate
) || trustServerCertificate;
}

View File

@@ -19,6 +19,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Database\Database.Connection\Database.Connection.csproj" />
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Configuration\Infrastructure.Configuration.csproj" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="scripts/**/*.sql" />

View File

@@ -1,4 +1,5 @@
using Database.Connection;
using Infrastructure.Configuration;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Configuration;
@@ -21,7 +22,7 @@ internal static class Program
.Build();
bool clearDatabase = string.Equals(
configuration["CLEAR_DATABASE"],
configuration[ConfigurationKeys.ClearDatabase],
"true",
StringComparison.OrdinalIgnoreCase
);

View File

@@ -21,5 +21,6 @@
<ItemGroup>
<ProjectReference Include="..\Features.Auth\Features.Auth.csproj" />
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Configuration\Infrastructure.Configuration.csproj" />
</ItemGroup>
</Project>

View File

@@ -5,6 +5,7 @@ using Features.Auth.Identity;
using Features.Auth.Services;
using Features.Auth.Tests.TestSupport;
using FluentAssertions;
using Infrastructure.Configuration;
using Infrastructure.Jwt;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Configuration;
@@ -27,9 +28,11 @@ public class TokenServiceRefreshTests
.AddInMemoryCollection(
new Dictionary<string, string?>
{
["ACCESS_TOKEN_SECRET"] = "test-access-secret-that-is-very-long-1234567890",
["REFRESH_TOKEN_SECRET"] = "test-refresh-secret-that-is-very-long-1234567890",
["CONFIRMATION_TOKEN_SECRET"] =
[ConfigurationKeys.AccessTokenSecret] =
"test-access-secret-that-is-very-long-1234567890",
[ConfigurationKeys.RefreshTokenSecret] =
"test-refresh-secret-that-is-very-long-1234567890",
[ConfigurationKeys.ConfirmationTokenSecret] =
"test-confirmation-secret-that-is-very-long-1234567890",
}
)

View File

@@ -4,6 +4,7 @@ using Domain.Exceptions;
using Features.Auth.Services;
using Features.Auth.Tests.TestSupport;
using FluentAssertions;
using Infrastructure.Configuration;
using Infrastructure.Jwt;
using Microsoft.Extensions.Configuration;
using Moq;
@@ -23,9 +24,11 @@ public class TokenServiceValidationTests
.AddInMemoryCollection(
new Dictionary<string, string?>
{
["ACCESS_TOKEN_SECRET"] = "test-access-secret-that-is-very-long-1234567890",
["REFRESH_TOKEN_SECRET"] = "test-refresh-secret-that-is-very-long-1234567890",
["CONFIRMATION_TOKEN_SECRET"] =
[ConfigurationKeys.AccessTokenSecret] =
"test-access-secret-that-is-very-long-1234567890",
[ConfigurationKeys.RefreshTokenSecret] =
"test-refresh-secret-that-is-very-long-1234567890",
[ConfigurationKeys.ConfirmationTokenSecret] =
"test-confirmation-secret-that-is-very-long-1234567890",
}
)

View File

@@ -14,6 +14,7 @@
<ItemGroup>
<ProjectReference Include="..\..\Domain\Domain.Entities\Domain.Entities.csproj" />
<ProjectReference Include="..\..\Domain\Domain.Exceptions\Domain.Exceptions.csproj" />
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Configuration\Infrastructure.Configuration.csproj" />
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Jwt\Infrastructure.Jwt.csproj" />
<ProjectReference Include="..\..\Infrastructure\Infrastructure.PasswordHashing\Infrastructure.PasswordHashing.csproj" />
<ProjectReference Include="..\..\Database\Database.Connection\Database.Connection.csproj" />

View File

@@ -2,6 +2,7 @@ using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using Domain.Exceptions;
using Features.Auth.Identity;
using Infrastructure.Configuration;
using Infrastructure.Jwt;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Configuration;
@@ -33,23 +34,20 @@ public class TokenService : ITokenService
_tokenInfrastructure = tokenInfrastructure;
_userManager = userManager;
_accessTokenSecret =
configuration["ACCESS_TOKEN_SECRET"]
?? throw new InvalidOperationException(
"ACCESS_TOKEN_SECRET environment variable is not set"
);
_accessTokenSecret = ConfigurationHelpers.GetKeyOrThrow(
configuration,
ConfigurationKeys.AccessTokenSecret
);
_refreshTokenSecret =
configuration["REFRESH_TOKEN_SECRET"]
?? throw new InvalidOperationException(
"REFRESH_TOKEN_SECRET environment variable is not set"
);
_refreshTokenSecret = ConfigurationHelpers.GetKeyOrThrow(
configuration,
ConfigurationKeys.RefreshTokenSecret
);
_confirmationTokenSecret =
configuration["CONFIRMATION_TOKEN_SECRET"]
?? throw new InvalidOperationException(
"CONFIRMATION_TOKEN_SECRET environment variable is not set"
);
_confirmationTokenSecret = ConfigurationHelpers.GetKeyOrThrow(
configuration,
ConfigurationKeys.ConfirmationTokenSecret
);
}
/// <inheritdoc />

View File

@@ -14,6 +14,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Configuration\Infrastructure.Configuration.csproj" />
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Email\Infrastructure.Email.csproj" />
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Email.Templates\Infrastructure.Email.Templates.csproj" />
<ProjectReference Include="..\..\Shared\Shared.Application\Shared.Application.csproj" />

View File

@@ -1,3 +1,4 @@
using Infrastructure.Configuration;
using Infrastructure.Email;
using Infrastructure.Email.Templates.Rendering;
using Microsoft.Extensions.Configuration;
@@ -21,8 +22,7 @@ public class EmailDispatcher(
/// Thrown when the <c>WEBSITE_BASE_URL</c> configuration value is not set.
/// </exception>
private string WebsiteBaseUrl =>
configuration["WEBSITE_BASE_URL"]
?? throw new InvalidOperationException("WEBSITE_BASE_URL environment variable is not set");
ConfigurationHelpers.GetKeyOrThrow(configuration, ConfigurationKeys.WebsiteBaseUrl);
/// <inheritdoc />
public async Task SendRegistrationEmailAsync(

View File

@@ -0,0 +1,32 @@
using Microsoft.Extensions.Configuration;
namespace Infrastructure.Configuration;
public static class ConfigurationHelpers
{
public static string GetKeyOrThrow(IConfiguration configuration, string key) =>
configuration[key]
?? throw new InvalidOperationException($"The {key} environment variable is not set.");
public static bool GetBooleanKeyOrThrow(IConfiguration configuration, string key)
{
string value = GetKeyOrThrow(configuration, key);
return bool.TryParse(value, out bool result)
? result
: throw new InvalidOperationException(
$"The {key} environment variable is set to '{value}', which is not a valid boolean."
);
}
public static int GetIntKeyOrThrow(IConfiguration configuration, string key)
{
string value = GetKeyOrThrow(configuration, key);
return int.TryParse(value, out int result)
? result
: throw new InvalidOperationException(
$"The {key} environment variable is set to '{value}', which is not a valid integer."
);
}
}

View File

@@ -0,0 +1,33 @@
namespace Infrastructure.Configuration;
public static class ConfigurationKeys
{
public const string AccessTokenSecret = "ACCESS_TOKEN_SECRET";
public const string RefreshTokenSecret = "REFRESH_TOKEN_SECRET";
public const string ConfirmationTokenSecret = "CONFIRMATION_TOKEN_SECRET";
/// <summary>
/// Fallback configuration section path for the access-token signing key, checked when
/// <see cref="AccessTokenSecret" /> is not set.
/// </summary>
public const string JwtSecretKeyFallback = "Jwt:SecretKey";
public const string WebsiteBaseUrl = "WEBSITE_BASE_URL";
public const string SmtpHost = "SMTP_HOST";
public const string SmtpPort = "SMTP_PORT";
public const string SmtpUsername = "SMTP_USERNAME";
public const string SmtpPassword = "SMTP_PASSWORD";
public const string SmtpUseSsl = "SMTP_USE_SSL";
public const string SmtpFromEmail = "SMTP_FROM_EMAIL";
public const string SmtpFromName = "SMTP_FROM_NAME";
public const string DbServer = "DB_SERVER";
public const string DbUser = "DB_USER";
public const string DbPassword = "DB_PASSWORD";
public const string DbName = "DB_NAME";
public const string DbConnectionString = "DB_CONNECTION_STRING";
public const string DbTrustServerCertificate = "DB_TRUST_SERVER_CERTIFICATE";
public const string ClearDatabase = "CLEAR_DATABASE";
}

View File

@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>Infrastructure.Configuration</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference
Include="Microsoft.Extensions.Configuration.Abstractions"
Version="10.0.1"
/>
</ItemGroup>
</Project>

View File

@@ -13,4 +13,8 @@
Version="10.0.1"
/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Infrastructure.Configuration\Infrastructure.Configuration.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,4 +1,5 @@
using MailKit.Net.Smtp;
using Infrastructure.Configuration;
using MailKit.Net.Smtp;
using MailKit.Security;
using Microsoft.Extensions.Configuration;
using MimeKit;
@@ -25,31 +26,27 @@ public class SmtpEmailProvider : IEmailProvider
/// <c>SMTP_FROM_EMAIL</c>, <c>SMTP_FROM_NAME</c>) from <paramref name="configuration" />.
/// </summary>
/// <exception cref="InvalidOperationException">
/// Thrown when <c>SMTP_HOST</c> or <c>SMTP_FROM_EMAIL</c> is not set, or when <c>SMTP_PORT</c> is not a valid integer.
/// Thrown when <c>SMTP_HOST</c>, <c>SMTP_PORT</c>, or <c>SMTP_FROM_EMAIL</c> is not set,
/// or when <c>SMTP_PORT</c> is not a valid integer.
/// </exception>
public SmtpEmailProvider(IConfiguration configuration)
{
_host =
configuration["SMTP_HOST"]
?? throw new InvalidOperationException("SMTP_HOST environment variable is not set");
_host = ConfigurationHelpers.GetKeyOrThrow(configuration, ConfigurationKeys.SmtpHost);
string portString = configuration["SMTP_PORT"] ?? "587";
if (!int.TryParse(portString, out _port))
throw new InvalidOperationException($"SMTP_PORT '{portString}' is not a valid integer");
_port = ConfigurationHelpers.GetIntKeyOrThrow(configuration, ConfigurationKeys.SmtpPort);
_username = configuration["SMTP_USERNAME"];
_password = configuration["SMTP_PASSWORD"];
_username = configuration[ConfigurationKeys.SmtpUsername];
_password = configuration[ConfigurationKeys.SmtpPassword];
string useSslString = configuration["SMTP_USE_SSL"] ?? "true";
string useSslString = configuration[ConfigurationKeys.SmtpUseSsl] ?? "true";
_useSsl = bool.Parse(useSslString);
_fromEmail =
configuration["SMTP_FROM_EMAIL"]
?? throw new InvalidOperationException(
"SMTP_FROM_EMAIL environment variable is not set"
);
_fromEmail = ConfigurationHelpers.GetKeyOrThrow(
configuration,
ConfigurationKeys.SmtpFromEmail
);
_fromName = configuration["SMTP_FROM_NAME"] ?? "The Biergarten";
_fromName = configuration[ConfigurationKeys.SmtpFromName] ?? "The Biergarten";
}
/// <inheritdoc/>