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.Entities\Domain.Entities.csproj" />
<ProjectReference Include="..\..\Domain\Domain.Exceptions\Domain.Exceptions.csproj" /> <ProjectReference Include="..\..\Domain\Domain.Exceptions\Domain.Exceptions.csproj" />
<ProjectReference Include="..\..\Database\Database.Connection\Database.Connection.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="..\..\Infrastructure\Infrastructure.Jwt\Infrastructure.Jwt.csproj" />
<ProjectReference Include="..\..\Features\Features.Breweries\Features.Breweries.csproj" /> <ProjectReference Include="..\..\Features\Features.Breweries\Features.Breweries.csproj" />
<ProjectReference Include="..\..\Features\Features.UserManagement\Features.UserManagement.csproj" /> <ProjectReference Include="..\..\Features\Features.UserManagement\Features.UserManagement.csproj" />

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
using FluentAssertions; using FluentAssertions;
using Infrastructure.Configuration;
using Infrastructure.Jwt; using Infrastructure.Jwt;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@@ -430,7 +431,7 @@ public class AuthSteps(ScenarioContext scenario)
? user ? user
: throw new InvalidOperationException("registered username not found in scenario"); : throw new InvalidOperationException("registered username not found in scenario");
string secret = GetRequiredConfigValue("ACCESS_TOKEN_SECRET"); string secret = GetRequiredConfigValue(ConfigurationKeys.AccessTokenSecret);
scenario["accessToken"] = GenerateJwtToken( scenario["accessToken"] = GenerateJwtToken(
userId, userId,
username, username,
@@ -449,7 +450,7 @@ public class AuthSteps(ScenarioContext scenario)
? user ? user
: throw new InvalidOperationException("registered username not found in scenario"); : throw new InvalidOperationException("registered username not found in scenario");
string secret = GetRequiredConfigValue("CONFIRMATION_TOKEN_SECRET"); string secret = GetRequiredConfigValue(ConfigurationKeys.ConfirmationTokenSecret);
scenario["confirmationToken"] = GenerateJwtToken( scenario["confirmationToken"] = GenerateJwtToken(
userId, userId,
username, username,
@@ -468,7 +469,7 @@ public class AuthSteps(ScenarioContext scenario)
? user ? user
: throw new InvalidOperationException("registered username not found in scenario"); : throw new InvalidOperationException("registered username not found in scenario");
string secret = GetRequiredConfigValue("CONFIRMATION_TOKEN_SECRET"); string secret = GetRequiredConfigValue(ConfigurationKeys.ConfirmationTokenSecret);
scenario["confirmationToken"] = GenerateJwtToken( scenario["confirmationToken"] = GenerateJwtToken(
userId, userId,
username, 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.SqlServer.Types" Version="160.1000.6" />
<PackageReference <PackageReference
Include="Microsoft.Extensions.Configuration.Abstractions" Include="Microsoft.Extensions.Configuration.Abstractions"
Version="9.0.0" Version="10.0.1"
/> />
<PackageReference <PackageReference
Include="Microsoft.Extensions.DependencyInjection.Abstractions" Include="Microsoft.Extensions.DependencyInjection.Abstractions"
Version="10.0.0" Version="10.0.0"
/> />
</ItemGroup> </ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Configuration\Infrastructure.Configuration.csproj" />
</ItemGroup>
</Project> </Project>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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