Skip to content

Commit a9082e9

Browse files
committed
refactor: implement AWS client factory pattern and consolidate configuration
- Add conditional AWS client builder with LocalStack support for development environments - Create factory pattern for AWS service clients with lazy initialization and proper disposal - Implement GitHub package service factory with environment-based configuration validation - Consolidate application settings into unified Settings class with environment variable parsing - Remove ILLink descriptors and optimize project dependencies for Native AOT compilation - Simplify main program structure with cleaner conditional compilation directives Improves testability, reduces memory footprint, and provides better separation between local development and production AWS service usage.
1 parent 7a8d634 commit a9082e9

13 files changed

Lines changed: 210 additions & 93 deletions

src/BadgeSmith.Api/BadgeSmith.Api.csproj

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,17 @@
2121
<EventSourceSupport>false</EventSourceSupport>
2222

2323
<EnableTelemetry>true</EnableTelemetry>
24+
<EnableLocalStack>true</EnableLocalStack>
2425
</PropertyGroup>
2526

2627
<PropertyGroup Condition="'$(EnableTelemetry)' == 'true'">
2728
<DefineConstants>$(DefineConstants);ENABLE_TELEMETRY</DefineConstants>
2829
</PropertyGroup>
2930

31+
<PropertyGroup Condition="'$(EnableLocalStack)' == 'true'">
32+
<DefineConstants>$(DefineConstants);ENABLE_LOCALSTACK</DefineConstants>
33+
</PropertyGroup>
34+
3035
<ItemGroup>
3136
<PackageReference Include="Amazon.Lambda.RuntimeSupport"/>
3237
<PackageReference Include="Amazon.Lambda.Core"/>
@@ -39,10 +44,10 @@
3944
<PackageReference Include="OneOf"/>
4045
<PackageReference Include="OneOf.SourceGenerator"/>
4146

42-
<PackageReference Include="Microsoft.Extensions.Caching.Memory" />
47+
<PackageReference Include="Microsoft.Extensions.Caching.Memory"/>
4348
<PackageReference Include="Microsoft.Extensions.Logging.Console"/>
4449

45-
<PackageReference Include="LocalStack.Client"/>
50+
<PackageReference Include="LocalStack.Client" Condition="'$(EnableLocalStack)' == 'true'"/>
4651

4752
<PackageReference Include="NuGet.Versioning"/>
4853
<PackageReference Include="ZLinq"/>
@@ -79,8 +84,4 @@
7984
<_Parameter1>DynamicProxyGenAssembly2</_Parameter1>
8085
</AssemblyAttribute>
8186
</ItemGroup>
82-
83-
<ItemGroup>
84-
<TrimmerRootDescriptor Include="ILLink.Descriptors.xml"/>
85-
</ItemGroup>
8687
</Project>

src/BadgeSmith.Api/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ RUN --mount=type=cache,id=nuget,target=/root/.nuget/packages \
2727

2828
RUN --mount=type=cache,id=nuget,target=/root/.nuget/packages \
2929
dotnet publish ${PROJECT} -c ${CONFIG} -r ${RID} --self-contained true \
30-
-p:PublishAot=true -p:StripSymbols=true -p:DebugType=none -p:EnableTelemetry=false \
30+
-p:PublishAot=true -p:StripSymbols=true -p:DebugType=none -p:EnableTelemetry=false -p:EnableLocalStack=false \
3131
-p:EnableSourceControlManagerQueries=false \
3232
-p:EmbedUntrackedSources=false \
3333
-o ${PUBLISH_DIR} \
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
#if !ENABLE_LOCALSTACK
2+
using Amazon.Runtime;
3+
4+
namespace BadgeSmith.Api.Domain.AWS;
5+
6+
internal static class AwsClientBuilder
7+
{
8+
public static TClient CreateAwsClient<TClient>() where TClient : AmazonServiceClient, IAmazonService, new()
9+
{
10+
return new TClient();
11+
}
12+
}
13+
#endif
14+
15+
#if ENABLE_LOCALSTACK
16+
17+
using System.Globalization;
18+
using Amazon.Runtime;
19+
using LocalStack.Client;
20+
using LocalStack.Client.Contracts;
21+
using LocalStack.Client.Options;
22+
23+
namespace BadgeSmith.Api.Domain.AWS;
24+
25+
internal static class AwsClientBuilder
26+
{
27+
private static readonly ISession Session = SessionStandalone
28+
.Init()
29+
.WithConfigurationOptions(BuildConfigOptions())
30+
.WithSessionOptions(BuildSessionOptions())
31+
.Create();
32+
33+
public static TClient CreateAwsClient<TClient>() where TClient : AmazonServiceClient, IAmazonService, new()
34+
{
35+
return Settings.UseLocalStack ? Session.CreateClientByImplementation<TClient>() : new TClient();
36+
}
37+
38+
public static ConfigOptions BuildConfigOptions()
39+
{
40+
var localStackHost = Environment.GetEnvironmentVariable("LocalStack__Config__LocalStackHost");
41+
var localStackPort = Environment.GetEnvironmentVariable("LocalStack__Config__EdgePort");
42+
var useLegacyPorts = Environment.GetEnvironmentVariable("LocalStack__Config__UseLegacyPorts");
43+
var useSsl = Environment.GetEnvironmentVariable("LocalStack__Config__UseSsl");
44+
45+
if (string.IsNullOrEmpty(localStackHost) || string.IsNullOrEmpty(localStackPort))
46+
{
47+
var localStackConnection = Environment.GetEnvironmentVariable("ConnectionStrings__localstack");
48+
if (!string.IsNullOrEmpty(localStackConnection) && Uri.TryCreate(localStackConnection, UriKind.Absolute, out var uri))
49+
{
50+
localStackHost ??= uri.Host;
51+
localStackPort ??= uri.Port.ToString(CultureInfo.InvariantCulture);
52+
}
53+
}
54+
55+
if (string.IsNullOrEmpty(localStackHost) && string.IsNullOrEmpty(localStackPort) && string.IsNullOrEmpty(useLegacyPorts) && string.IsNullOrEmpty(useSsl))
56+
{
57+
return new ConfigOptions();
58+
}
59+
60+
int? edgePort;
61+
if (string.IsNullOrEmpty(localStackPort))
62+
{
63+
edgePort = null;
64+
}
65+
else if (int.TryParse(localStackPort, CultureInfo.InvariantCulture, out var port))
66+
{
67+
edgePort = port;
68+
}
69+
else
70+
{
71+
edgePort = null;
72+
}
73+
74+
bool? legacyPorts;
75+
if (string.IsNullOrEmpty(useLegacyPorts))
76+
{
77+
legacyPorts = null;
78+
}
79+
else if (bool.TryParse(useLegacyPorts, out var legacy))
80+
{
81+
legacyPorts = legacy;
82+
}
83+
else
84+
{
85+
legacyPorts = null;
86+
}
87+
88+
bool? ssl;
89+
if (string.IsNullOrEmpty(useSsl))
90+
{
91+
ssl = null;
92+
}
93+
else if (bool.TryParse(useSsl, out var sslValue))
94+
{
95+
ssl = sslValue;
96+
}
97+
else
98+
{
99+
ssl = null;
100+
}
101+
102+
return new ConfigOptions(
103+
localStackHost ?? LocalStack.Client.Models.Constants.LocalStackHost,
104+
legacyPorts ?? LocalStack.Client.Models.Constants.UseLegacyPorts,
105+
ssl ?? LocalStack.Client.Models.Constants.UseSsl,
106+
edgePort ?? LocalStack.Client.Models.Constants.EdgePort
107+
);
108+
}
109+
110+
public static SessionOptions BuildSessionOptions()
111+
{
112+
var awsAccessKey = Environment.GetEnvironmentVariable("LocalStack__Session__AwsAccessKey");
113+
var awsAccessKeyId = Environment.GetEnvironmentVariable("LocalStack__Session__AwsAccessKeyId");
114+
var awsSessionToken = Environment.GetEnvironmentVariable("LocalStack__Session__AwsSessionToken");
115+
var regionName = Environment.GetEnvironmentVariable("LocalStack__Session__RegionName");
116+
117+
if (string.IsNullOrEmpty(awsAccessKey) && string.IsNullOrEmpty(awsAccessKeyId) && string.IsNullOrEmpty(awsSessionToken) && string.IsNullOrEmpty(regionName))
118+
{
119+
return new SessionOptions();
120+
}
121+
122+
return new SessionOptions(
123+
awsAccessKeyId: string.IsNullOrEmpty(awsAccessKeyId) ? LocalStack.Client.Models.Constants.AwsAccessKeyId : awsAccessKeyId,
124+
awsAccessKey: string.IsNullOrEmpty(awsAccessKey) ? LocalStack.Client.Models.Constants.AwsAccessKey : awsAccessKey,
125+
awsSessionToken: string.IsNullOrEmpty(awsSessionToken) ? LocalStack.Client.Models.Constants.AwsSessionToken : awsSessionToken,
126+
regionName: string.IsNullOrEmpty(regionName) ? LocalStack.Client.Models.Constants.RegionName : regionName
127+
);
128+
}
129+
}
130+
#endif
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using Amazon.DynamoDBv2;
2+
using Amazon.SecretsManager;
3+
4+
namespace BadgeSmith.Api.Domain.AWS;
5+
6+
internal static class AwsClientFactory
7+
{
8+
private static readonly Lazy<AmazonDynamoDBClient> DynamoDbClientLazy = new(AwsClientBuilder.CreateAwsClient<AmazonDynamoDBClient>);
9+
private static readonly Lazy<AmazonSecretsManagerClient> AmazonSecretsManagerClientLazy = new(AwsClientBuilder.CreateAwsClient<AmazonSecretsManagerClient>);
10+
11+
public static AmazonDynamoDBClient AmazonDynamoDbClient => DynamoDbClientLazy.Value;
12+
public static AmazonSecretsManagerClient AmazonSecretsManagerClient => AmazonSecretsManagerClientLazy.Value;
13+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
using BadgeSmith.Api.Domain.Services.Github;
2+
3+
namespace BadgeSmith.Api.Domain.Services.Contracts;
4+
5+
internal interface IGithubPackageServiceFactory
6+
{
7+
public GithubOrgSecretsService GithubOrgSecretsService { get; }
8+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
using BadgeSmith.Api.Domain.AWS;
2+
using BadgeSmith.Api.Domain.Services.Contracts;
3+
using BadgeSmith.Api.Infrastructure.Caching;
4+
using BadgeSmith.Api.Infrastructure.Observability;
5+
6+
namespace BadgeSmith.Api.Domain.Services.Github;
7+
8+
internal class GithubPackageServiceFactory : IGithubPackageServiceFactory
9+
{
10+
private static readonly Lazy<GithubOrgSecretsService> GithubOrgSecretsServiceLazy = new(CreateGithubOrgSecretsService);
11+
12+
public GithubOrgSecretsService GithubOrgSecretsService => GithubOrgSecretsServiceLazy.Value;
13+
14+
private static GithubOrgSecretsService CreateGithubOrgSecretsService()
15+
{
16+
var amazonDynamoDbClient = AwsClientFactory.AmazonDynamoDbClient;
17+
var amazonSecretsManagerClient = AwsClientFactory.AmazonSecretsManagerClient;
18+
19+
var secretsTableName = Environment.GetEnvironmentVariable("AWS_RESOURCE_ORG_SECRETS_TABLE");
20+
21+
if (string.IsNullOrWhiteSpace(secretsTableName))
22+
{
23+
throw new InvalidOperationException("AWS_RESOURCE_ORG_SECRETS_TABLE environment variable is not set");
24+
}
25+
26+
var githubSecretsLogger = LoggerFactory.CreateLogger<GithubOrgSecretsService>();
27+
28+
var memoryAppCache = new MemoryAppCache();
29+
return new GithubOrgSecretsService(amazonSecretsManagerClient, amazonDynamoDbClient, secretsTableName, memoryAppCache, githubSecretsLogger);
30+
}
31+
}

src/BadgeSmith.Api/ILLink.Descriptors.xml

Lines changed: 0 additions & 17 deletions
This file was deleted.

src/BadgeSmith.Api/Infrastructure/Caching/MemoryAppCache.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ namespace BadgeSmith.Api.Infrastructure.Caching;
44

55
internal sealed class MemoryAppCache : IAppCache, IDisposable
66
{
7-
private readonly MemoryCache _cache = new(new MemoryCacheOptions());
7+
private static readonly MemoryCache Cache = new(new MemoryCacheOptions());
88

99
public bool TryGetValue<T>(string key, out T? value)
1010
{
11-
if (_cache.TryGetValue(key, out var obj) && obj is T v)
11+
if (Cache.TryGetValue(key, out var obj) && obj is T v)
1212
{
1313
value = v;
1414
return true;
@@ -20,13 +20,13 @@ public bool TryGetValue<T>(string key, out T? value)
2020

2121
public void Set<T>(string key, T value, TimeSpan ttl)
2222
{
23-
using var entry = _cache.CreateEntry(key);
23+
using var entry = Cache.CreateEntry(key);
2424
entry.AbsoluteExpirationRelativeToNow = ttl;
2525
entry.Value = value;
2626
}
2727

2828
public void Dispose()
2929
{
30-
_cache.Dispose();
30+
Cache.Dispose();
3131
}
3232
}
Lines changed: 3 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,9 @@
11
#pragma warning disable S125, RCS1093
22

3-
using Amazon.DynamoDBv2;
4-
using Amazon.SecretsManager;
53
using BadgeSmith.Api.Domain.Services.Github;
64
using BadgeSmith.Api.Domain.Services.Nuget;
7-
using BadgeSmith.Api.Infrastructure.Caching;
85
using BadgeSmith.Api.Infrastructure.Handlers.Contracts;
96
using BadgeSmith.Api.Infrastructure.Observability;
10-
using LocalStack.Client;
11-
using LocalStack.Client.Options;
127

138
namespace BadgeSmith.Api.Infrastructure.Handlers;
149

@@ -21,9 +16,6 @@ internal class HandlerFactory : IHandlerFactory
2116
private static readonly Lazy<ITestResultRedirectionHandler> TestResultRedirectionHandlerLazy = new(CreateTestResultRedirectionHandler);
2217
private static readonly Lazy<ITestResultIngestionHandler> TestResultIngestionHandlerLazy = new(CreateTestResultIngestionHandler);
2318

24-
private static readonly Lazy<IAmazonSecretsManager> SecretsManagerLazy = new(CreateSecretsManagerClient());
25-
private static readonly Lazy<IAmazonDynamoDB> DynamoDbClientLazy = new(CreateDynamoDbClient);
26-
2719
public IHealthCheckHandler HealthCheckHandler => HealthCheckHandlerLazy.Value;
2820

2921
public INugetPackageBadgeHandler NugetPackageBadgeHandler => NugetPackageBadgeHandlerLazy.Value;
@@ -47,24 +39,14 @@ private static NuGetPackageBadgeHandler CreateNugetPackageBadgeHandler()
4739
var logger = LoggerFactory.CreateLogger<NuGetPackageBadgeHandler>();
4840
var nugetPackageServiceFactory = new NuGetPackageServiceFactory();
4941

50-
return new NuGetPackageBadgeHandler(logger, nugetPackageServiceFactory);
42+
return new NuGetPackageBadgeHandler(logger, nugetPackageServiceFactory.NuGetPackageService);
5143
}
5244

5345
private static GithubPackagesBadgeHandler CreateGithubPackagesBadgeHandler()
5446
{
5547
var githubPackagesLogger = LoggerFactory.CreateLogger<GithubPackagesBadgeHandler>();
56-
var githubSecretsLogger = LoggerFactory.CreateLogger<GithubOrgSecretsService>();
57-
58-
var secretsTableName = Environment.GetEnvironmentVariable("AWS_RESOURCE_ORG_SECRETS_TABLE");
59-
60-
if (string.IsNullOrWhiteSpace(secretsTableName))
61-
{
62-
throw new InvalidOperationException("AWS_RESOURCE_ORG_SECRETS_TABLE environment variable is not set");
63-
}
64-
65-
var memoryAppCache = new MemoryAppCache();
66-
var githubOrgSecretsService = new GithubOrgSecretsService(SecretsManagerLazy.Value, DynamoDbClientLazy.Value, secretsTableName, memoryAppCache, githubSecretsLogger);
67-
return new GithubPackagesBadgeHandler(githubPackagesLogger, githubOrgSecretsService);
48+
var githubPackageServiceFactory = new GithubPackageServiceFactory();
49+
return new GithubPackagesBadgeHandler(githubPackagesLogger, githubPackageServiceFactory.GithubOrgSecretsService);
6850
}
6951

7052
private static TestResultsBadgeHandler CreateTestResultsBadgeHandler()
@@ -84,40 +66,4 @@ private static TestResultIngestionHandler CreateTestResultIngestionHandler()
8466
var logger = LoggerFactory.CreateLogger<TestResultIngestionHandler>();
8567
return new TestResultIngestionHandler(logger);
8668
}
87-
88-
private static AmazonDynamoDBClient CreateDynamoDbClient()
89-
{
90-
if (!Settings.UseLocalStack)
91-
{
92-
return new AmazonDynamoDBClient();
93-
}
94-
95-
var uri = new Uri(Settings.LocalStackEndpoint!);
96-
var localStackHost = uri.Host;
97-
var localStackPort = uri.Port;
98-
return SessionStandalone
99-
.Init()
100-
.WithConfigurationOptions(new ConfigOptions(localStackHost, edgePort: localStackPort))
101-
.WithSessionOptions(new SessionOptions(regionName: Environment.GetEnvironmentVariable("AWS_REGION")!))
102-
.Create()
103-
.CreateClientByImplementation<AmazonDynamoDBClient>();
104-
}
105-
106-
private static AmazonSecretsManagerClient CreateSecretsManagerClient()
107-
{
108-
if (!Settings.UseLocalStack)
109-
{
110-
return new AmazonSecretsManagerClient();
111-
}
112-
113-
var uri = new Uri(Settings.LocalStackEndpoint!);
114-
var localStackHost = uri.Host;
115-
var localStackPort = uri.Port;
116-
return SessionStandalone
117-
.Init()
118-
.WithConfigurationOptions(new ConfigOptions(localStackHost, edgePort: localStackPort))
119-
.WithSessionOptions(new SessionOptions(regionName: Environment.GetEnvironmentVariable("AWS_REGION")!))
120-
.Create()
121-
.CreateClientByImplementation<AmazonSecretsManagerClient>();
122-
}
12369
}

src/BadgeSmith.Api/Infrastructure/Handlers/NuGetPackageBadgeHandler.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ internal class NuGetPackageBadgeHandler : INugetPackageBadgeHandler
1515
private readonly ILogger<NuGetPackageBadgeHandler> _logger;
1616
private readonly INuGetPackageService _nugetPackageService;
1717

18-
public NuGetPackageBadgeHandler(ILogger<NuGetPackageBadgeHandler> logger, INugetPackageServiceFactory nugetPackageServiceFactory)
18+
public NuGetPackageBadgeHandler(ILogger<NuGetPackageBadgeHandler> logger, INuGetPackageService nuGetPackageService)
1919
{
2020
_logger = logger;
21-
_nugetPackageService = nugetPackageServiceFactory.NuGetPackageService;
21+
_nugetPackageService = nuGetPackageService;
2222
}
2323

2424
public async Task<APIGatewayHttpApiV2ProxyResponse> HandleAsync(RouteContext routeContext, CancellationToken ct = default)

0 commit comments

Comments
 (0)