Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,18 @@

<!-- Infrastructure Layer - EF Core -->
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="8.0.10" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.10" />
<PackageVersion Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.2" />

<!-- Infrastructure Layer - AWS SDK -->
<PackageVersion Include="AWSSDK.Core" Version="3.7.400.57" />
<PackageVersion Include="AWSSDK.SecretsManager" Version="3.7.400.57" />
<PackageVersion Include="AWSSDK.SQS" Version="3.7.400.57" />

<!-- Infrastructure Layer - Cloudvelous AWS SDK -->
<PackageVersion Include="Cloudvelous.Aws.Core" Version="1.0.8" />
<PackageVersion Include="Cloudvelous.Aws.SecretsManager" Version="1.0.8" />
<PackageVersion Include="Cloudvelous.Aws.Rds" Version="1.0.8" />
<PackageVersion Include="Cloudvelous.Aws.Sqs" Version="1.0.8" />

<!-- Lambda Layer -->
<PackageVersion Include="Amazon.Lambda.Core" Version="2.4.0" />
Expand Down
5 changes: 0 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,11 +195,6 @@ See [`.cursor/rules/ai-agent.mdc`](.cursor/rules/ai-agent.mdc) for complete codi

## Next Steps

### Phase 3: Application Layer
- Create DTOs and MediatR commands
- Implement FluentValidation validators
- Build command handlers

### Phase 4: Infrastructure Layer
- Configure EF Core DbContext
- Implement repository pattern
Expand Down
140 changes: 140 additions & 0 deletions src/LeadProcessor.Infrastructure/Configuration/AwsSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
namespace LeadProcessor.Infrastructure.Configuration;

/// <summary>
/// Configuration settings for AWS services (SQS, SecretsManager, RDS).
/// </summary>
/// <remarks>
/// This class is designed to be bound from configuration via the IOptions pattern.
/// Use in Startup/Program.cs:
/// <code>
/// services.Configure&lt;AwsSettings&gt;(configuration.GetSection("AWS"));
/// </code>
/// All string properties use the 'required' modifier to enforce configuration at startup.
/// Credentials should be managed via IAM roles (for Lambda) or environment variables (for local development).
/// </remarks>
public sealed class AwsSettings
{
/// <summary>
/// Gets or sets the AWS region for all services.
/// </summary>
/// <remarks>
/// Examples: "us-east-1", "eu-west-1", "ap-southeast-1"
/// Should match the region where RDS, SQS, and Secrets Manager resources are provisioned.
/// </remarks>
public required string Region { get; set; }

/// <summary>
/// Gets or sets the SQS queue URL for receiving messages.
/// </summary>
/// <remarks>
/// Format: "https://sqs.{region}.amazonaws.com/{account-id}/{queue-name}"
/// Used by the Lambda function handler to process incoming lead events.
/// </remarks>
public required string SqsQueueUrl { get; set; }

/// <summary>
/// Gets or sets the SQS Dead Letter Queue (DLQ) URL for failed messages.
/// </summary>
/// <remarks>
/// Format: "https://sqs.{region}.amazonaws.com/{account-id}/{dlq-name}"
/// Messages that fail after MaxRetryAttempts are moved to this queue.
/// </remarks>
public required string SqsDlqUrl { get; set; }

/// <summary>
/// Gets or sets the AWS Secrets Manager secret name for database credentials.
/// </summary>
/// <remarks>
/// Should contain JSON with keys: Server, Port, Database, User, Password
/// Example: "leadprocessor/rds/credentials"
/// </remarks>
public required string SecretsManagerSecretName { get; set; }

/// <summary>
/// Gets or sets the RDS cluster/instance endpoint.
/// </summary>
/// <remarks>
/// Format: "leadprocessor-db.c9akciq32.{region}.rds.amazonaws.com"
/// Used to construct the database connection string.
/// </remarks>
public required string RdsEndpoint { get; set; }

/// <summary>
/// Gets or sets the maximum number of retry attempts for AWS SDK operations.
/// </summary>
/// <remarks>
/// Default is 3 retries for transient AWS errors.
/// Set to 0 to disable retries.
/// </remarks>
public int MaxRetryAttempts { get; set; } = 3;

/// <summary>
/// Gets or sets a value indicating whether to use IAM authentication for RDS.
/// </summary>
/// <remarks>
/// When enabled (true), uses temporary security credentials from IAM role instead of stored password.
/// Recommended for production Lambda deployments.
/// Default is false for simplicity in development.
/// </remarks>
public bool UseIamDatabaseAuthentication { get; set; } = false;

/// <summary>
/// Gets or sets the IAM database authentication token lifetime in seconds.
/// </summary>
/// <remarks>
/// Default is 900 seconds (15 minutes).
/// Tokens are cached and reused within their lifetime to reduce API calls.
/// </remarks>
public int IamTokenLifetimeSeconds { get; set; } = 900;

/// <summary>
/// Gets or sets a value indicating whether to validate AWS credentials at startup.
/// </summary>
/// <remarks>
/// When enabled, performs a test call to verify AWS credentials are valid.
/// Useful for catching configuration issues early in the Lambda initialization phase.
/// </remarks>
public bool ValidateCredentialsAtStartup { get; set; } = true;

/// <summary>
/// Validates the configuration settings.
/// </summary>
/// <returns>A list of validation errors, empty if valid.</returns>
/// <remarks>
/// Called during startup to ensure all required settings are present and valid.
/// </remarks>
public IEnumerable<string> Validate()
{
if (string.IsNullOrWhiteSpace(Region))
yield return "Region is required";

if (string.IsNullOrWhiteSpace(SqsQueueUrl))
yield return "SqsQueueUrl is required";

if (string.IsNullOrWhiteSpace(SqsDlqUrl))
yield return "SqsDlqUrl is required";

if (string.IsNullOrWhiteSpace(SecretsManagerSecretName))
yield return "SecretsManagerSecretName is required";

if (string.IsNullOrWhiteSpace(RdsEndpoint))
yield return "RdsEndpoint is required";

if (MaxRetryAttempts < 0)
yield return $"MaxRetryAttempts must be >= 0, got {MaxRetryAttempts}";

if (IamTokenLifetimeSeconds <= 0)
yield return $"IamTokenLifetimeSeconds must be > 0, got {IamTokenLifetimeSeconds}";

// Validate SQS URLs format (only if not null/empty)
if (!string.IsNullOrWhiteSpace(SqsQueueUrl) && !SqsQueueUrl.StartsWith("https://sqs."))
yield return "SqsQueueUrl should be in format: https://sqs.{region}.amazonaws.com/{account-id}/{queue-name}";

if (!string.IsNullOrWhiteSpace(SqsDlqUrl) && !SqsDlqUrl.StartsWith("https://sqs."))
yield return "SqsDlqUrl should be in format: https://sqs.{region}.amazonaws.com/{account-id}/{dlq-name}";

// Validate RDS endpoint format (only if not null/empty)
if (!string.IsNullOrWhiteSpace(RdsEndpoint) && !RdsEndpoint.Contains(".rds.amazonaws.com"))
yield return "RdsEndpoint should be in format: {identifier}.{region}.rds.amazonaws.com";
}
}
142 changes: 142 additions & 0 deletions src/LeadProcessor.Infrastructure/Configuration/DatabaseSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
namespace LeadProcessor.Infrastructure.Configuration;

/// <summary>
/// Configuration settings for database connections and EF Core behavior.
/// </summary>
/// <remarks>
/// This class is designed to be bound from configuration via the IOptions pattern.
/// Use in Startup/Program.cs:
/// <code>
/// services.Configure&lt;DatabaseSettings&gt;(configuration.GetSection("Database"));
/// </code>
/// All string properties use the 'required' modifier to enforce configuration at startup.
/// </remarks>
public sealed class DatabaseSettings
{
/// <summary>
/// Gets or sets the database connection string for RDS MySQL database.
/// </summary>
/// <remarks>
/// Should be in format: "Server=hostname;Port=3306;Database=dbname;User=user;Password=password"
/// Can be loaded from AWS Secrets Manager or environment-specific appsettings.json
/// </remarks>
public required string ConnectionString { get; set; }

/// <summary>
/// Gets or sets the database server hostname or endpoint.
/// </summary>
/// <remarks>
/// For RDS: format is typically "leadprocessor-db.c9akciq32.us-east-1.rds.amazonaws.com"
/// For local development: "localhost"
/// </remarks>
public required string Server { get; set; }

/// <summary>
/// Gets or sets the database server port.
/// </summary>
/// <remarks>
/// Default MySQL port is 3306.
/// </remarks>
public int Port { get; set; } = 3306;

/// <summary>
/// Gets or sets the database name.
/// </summary>
/// <remarks>
/// For this application: "leadprocessor" or similar.
/// </remarks>
public required string Database { get; set; }

/// <summary>
/// Gets or sets the database user for authentication.
/// </summary>
public required string User { get; set; }

/// <summary>
/// Gets or sets the database password for authentication.
/// </summary>
/// <remarks>
/// In production, should be loaded from AWS Secrets Manager, not hardcoded or in appsettings.
/// </remarks>
public required string Password { get; set; }

/// <summary>
/// Gets or sets the maximum number of retry attempts for transient database failures.
/// </summary>
/// <remarks>
/// Default is 3 retries for transient failures (connection timeouts, deadlocks, etc.).
/// Set to 0 to disable retries.
/// </remarks>
public int MaxRetryAttempts { get; set; } = 3;

/// <summary>
/// Gets or sets the maximum delay between retry attempts in seconds.
/// </summary>
/// <remarks>
/// Default is 10 seconds. Actual delay is randomized between 0 and this value.
/// </remarks>
public int MaxRetryDelaySeconds { get; set; } = 10;

/// <summary>
/// Gets or sets the command timeout in seconds for database operations.
/// </summary>
/// <remarks>
/// Default is 30 seconds. Increase for long-running queries or bulk operations.
/// </remarks>
public int CommandTimeoutSeconds { get; set; } = 30;

/// <summary>
/// Gets or sets a value indicating whether to enable detailed error logging.
/// </summary>
/// <remarks>
/// When enabled, provides more detailed diagnostics about database operations.
/// Should be disabled in production for security and performance.
/// </remarks>
public bool EnableDetailedErrors { get; set; } = false;

/// <summary>
/// Gets or sets a value indicating whether to enable EF Core query logging.
/// </summary>
/// <remarks>
/// When enabled, logs all generated SQL queries. Should only be used for debugging.
/// Can impact performance and expose sensitive data in logs.
/// </remarks>
public bool EnableQueryLogging { get; set; } = false;

/// <summary>
/// Validates the configuration settings.
/// </summary>
/// <returns>A list of validation errors, empty if valid.</returns>
/// <remarks>
/// Called during startup to ensure all required settings are present and valid.
/// </remarks>
public IEnumerable<string> Validate()
{
if (string.IsNullOrWhiteSpace(ConnectionString))
yield return "ConnectionString is required";

if (string.IsNullOrWhiteSpace(Server))
yield return "Server is required";

if (Port <= 0 || Port > 65535)
yield return $"Port must be between 1 and 65535, got {Port}";

if (string.IsNullOrWhiteSpace(Database))
yield return "Database is required";

if (string.IsNullOrWhiteSpace(User))
yield return "User is required";

if (string.IsNullOrWhiteSpace(Password))
yield return "Password is required";

if (MaxRetryAttempts < 0)
yield return $"MaxRetryAttempts must be >= 0, got {MaxRetryAttempts}";

if (MaxRetryDelaySeconds <= 0)
yield return $"MaxRetryDelaySeconds must be > 0, got {MaxRetryDelaySeconds}";

if (CommandTimeoutSeconds <= 0)
yield return $"CommandTimeoutSeconds must be > 0, got {CommandTimeoutSeconds}";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="AWSSDK.SecretsManager" />
<PackageReference Include="AWSSDK.SQS" />
<PackageReference Include="Cloudvelous.Aws.Core" />
<PackageReference Include="Cloudvelous.Aws.SecretsManager" />
<PackageReference Include="Cloudvelous.Aws.Rds" />
<PackageReference Include="Cloudvelous.Aws.Sqs" />
<PackageReference Include="Microsoft.EntityFrameworkCore" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" />
</ItemGroup>
Expand Down
56 changes: 56 additions & 0 deletions src/LeadProcessor.Infrastructure/Models/DatabaseCredentials.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
namespace LeadProcessor.Infrastructure.Models;

/// <summary>
/// Represents database credentials retrieved from AWS Secrets Manager.
/// </summary>
/// <remarks>
/// This immutable record type ensures credentials are not modified after retrieval.
/// Used to construct database connection strings from AWS Secrets Manager secrets.
/// </remarks>
public sealed record DatabaseCredentials
{
/// <summary>
/// Gets the database server hostname or endpoint.
/// </summary>
/// <remarks>
/// For RDS: typically in format "leadprocessor-db.c9akciq32.us-east-1.rds.amazonaws.com"
/// For local development: "localhost"
/// </remarks>
public required string Host { get; init; }

/// <summary>
/// Gets the database server port.
/// </summary>
/// <remarks>
/// Default MySQL port is 3306, PostgreSQL is 5432.
/// </remarks>
public required int Port { get; init; }

/// <summary>
/// Gets the database name.
/// </summary>
public required string Database { get; init; }

/// <summary>
/// Gets the database username for authentication.
/// </summary>
public required string Username { get; init; }

/// <summary>
/// Gets the database password for authentication.
/// </summary>
/// <remarks>
/// This value should be kept secure and not logged.
/// In production, retrieved from AWS Secrets Manager with automatic rotation support.
/// </remarks>
public required string Password { get; init; }

/// <summary>
/// Gets the database engine type (e.g., "mysql", "postgres").
/// </summary>
/// <remarks>
/// Optional field that can be used for engine-specific connection string formatting.
/// </remarks>
public string? Engine { get; init; }
}

Loading