From 3d1eeec1dbd00551aa1ea4f073970e270956ad25 Mon Sep 17 00:00:00 2001 From: Hydra-Slayer Date: Sat, 12 Jul 2025 22:15:10 +0530 Subject: [PATCH 1/2] Add configurable application settings - Create PredictorSettings configuration model - Move hard-coded values to appsettings.json - Support environment-specific configuration - Add Docker environment variable support - Include configuration documentation - Update .gitignore for .env files Fixes calculation limits and default values being configurable without code changes. Supports Docker and local development. Changes: - MaxCalculationPeriodMonths: configurable (default 36) - DefaultInitialBudget: configurable (default 48750) - EnableExampleData: configurable (default true) - MaxAllowedCalculationPeriod: configurable (default 120) This follows twelve-factor app principles and makes the application more flexible for different deployment environments. --- .env.example | 16 ++++ docs/CONFIGURATION.md | 81 +++++++++++++++++++ src/.gitignore | 7 ++ src/Predictor.Web/ExampleData.cs | 28 ++++--- src/Predictor.Web/Models/PredictorSettings.cs | 26 ++++++ src/Predictor.Web/Program.cs | 21 ++++- .../appsettings.Development.json | 6 ++ src/Predictor.Web/appsettings.json | 8 +- 8 files changed, 178 insertions(+), 15 deletions(-) create mode 100644 .env.example create mode 100644 docs/CONFIGURATION.md create mode 100644 src/Predictor.Web/Models/PredictorSettings.cs diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..7512b27 --- /dev/null +++ b/.env.example @@ -0,0 +1,16 @@ +# Example environment variables for Docker deployment +# Copy this file to .env and adjust values as needed + +# Predictor Settings +PREDICTOR__MAXCALCULATIONPERIODMONTHS=36 +PREDICTOR__DEFAULTINITIALBUDGET=48750 +PREDICTOR__ENABLEEXAMPLEDATA=true +PREDICTOR__MAXALLOWEDCALCULATIONPERIOD=120 + +# ASP.NET Core Settings +ASPNETCORE_ENVIRONMENT=Production +ASPNETCORE_URLS=http://+:8080 + +# Logging +LOGGING__LOGLEVEL__DEFAULT=Information +LOGGING__LOGLEVEL__MICROSOFT_ASPNETCORE=Warning diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md new file mode 100644 index 0000000..601290d --- /dev/null +++ b/docs/CONFIGURATION.md @@ -0,0 +1,81 @@ +# Configuration Guide + +## Application Settings + +The Predictor application is fully configurable through the `PredictorSettings` section in `appsettings.json`. All settings can be overridden using environment variables or other ASP.NET Core configuration sources. + +### Available Settings + +| Setting | Default | Description | +|---------|---------|-------------| +| `MaxCalculationPeriodMonths` | 36 | Maximum number of months to calculate predictions for | +| `DefaultInitialBudget` | 48750 | Default initial budget used in example data | +| `EnableExampleData` | true | Enable/disable the `/example-data` endpoint | +| `MaxAllowedCalculationPeriod` | 120 | Maximum allowed calculation period to prevent abuse (10 years) | + +### Configuration Examples + +#### appsettings.json +```json +{ + "PredictorSettings": { + "MaxCalculationPeriodMonths": 24, + "DefaultInitialBudget": 25000, + "EnableExampleData": true, + "MaxAllowedCalculationPeriod": 60 + } +} +``` + +#### Environment Variables +```bash +# Docker/Container deployment +PREDICTOR__MAXCALCULATIONPERIODMONTHS=24 +PREDICTOR__DEFAULTINITIALBUDGET=25000 +PREDICTOR__ENABLEEXAMPLEDATA=true +PREDICTOR__MAXALLOWEDCALCULATIONPERIOD=60 + +# Or use the ASPNETCORE prefix +ASPNETCORE_PREDICTORSETTINGS__MAXCALCULATIONPERIODMONTHS=24 +``` + +#### Docker Compose +```yaml +version: '3.8' +services: + predictor: + image: predictor:latest + environment: + - PREDICTOR__MAXCALCULATIONPERIODMONTHS=24 + - PREDICTOR__DEFAULTINITIALBUDGET=25000 + - PREDICTOR__ENABLEEXAMPLEDATA=true + - PREDICTOR__MAXALLOWEDCALCULATIONPERIOD=60 + ports: + - "8080:8080" +``` + +### Environment-Specific Configuration + +The application uses standard ASP.NET Core configuration layering: + +1. **appsettings.json** - Base configuration +2. **appsettings.{Environment}.json** - Environment-specific overrides +3. **Environment variables** - Runtime overrides +4. **Command line arguments** - Highest priority + +#### Development vs Production + +- **Development**: Uses `appsettings.Development.json` with shorter calculation periods for faster testing +- **Production**: Uses `appsettings.json` with full calculation periods + +### Configuration Validation + +The application validates configuration at startup and uses sensible defaults if values are missing. The calculation period is automatically capped at `MaxAllowedCalculationPeriod` to prevent resource abuse. + +### Why This Matters + +✅ **Deployment Flexibility**: Change limits without code changes +✅ **Environment-Specific**: Different settings for dev/staging/production +✅ **Docker Ready**: Full support for container deployment +✅ **Twelve-Factor**: Follows twelve-factor app configuration principles +✅ **Security**: Sensitive settings can be injected via environment variables diff --git a/src/.gitignore b/src/.gitignore index df0dc0a..1f7e0c3 100644 --- a/src/.gitignore +++ b/src/.gitignore @@ -6,6 +6,13 @@ # Project private files DownloadResults/ +# Environment files +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + # User-specific files *.rsuser *.suo diff --git a/src/Predictor.Web/ExampleData.cs b/src/Predictor.Web/ExampleData.cs index a3fae95..2e01bfe 100644 --- a/src/Predictor.Web/ExampleData.cs +++ b/src/Predictor.Web/ExampleData.cs @@ -4,17 +4,22 @@ namespace Predictor.Web; public static class ExampleData { - public static CalculateInput CalculateInputExample { get; } = new( - InitialBudget: 48_750, - StartCalculationMonth: MonthDate.Now, - Incomes: [ - // Infinite recurring income (no EndDate) - new("Primary Salary", 5_400, MonthDate.Now, new RecurringConfig(1)), - new("Spouse Salary", 4_100, MonthDate.Now, new RecurringConfig(1)), - new("Rental Property A", 1_500, MonthDate.Now, new RecurringConfig(1)), - new("Rental Property B", 1_100, MonthDate.Now.AddMonths(3), new RecurringConfig(1)), - new("Investment Dividends", 320, MonthDate.Now.AddMonths(1), new RecurringConfig(3)), - new("Side Business", 850, MonthDate.Now.AddMonths(2), new RecurringConfig(1)), + [Obsolete("Use GetCalculateInputExample(PredictorSettings settings) instead")] + public static CalculateInput CalculateInputExample => GetCalculateInputExample(new PredictorSettings()); + + public static CalculateInput GetCalculateInputExample(PredictorSettings settings) + { + return new CalculateInput( + InitialBudget: settings.DefaultInitialBudget, + StartCalculationMonth: MonthDate.Now, + Incomes: [ + // Infinite recurring income (no EndDate) + new("Primary Salary", 5_400, MonthDate.Now, new RecurringConfig(1)), + new("Spouse Salary", 4_100, MonthDate.Now, new RecurringConfig(1)), + new("Rental Property A", 1_500, MonthDate.Now, new RecurringConfig(1)), + new("Rental Property B", 1_100, MonthDate.Now.AddMonths(3), new RecurringConfig(1)), + new("Investment Dividends", 320, MonthDate.Now.AddMonths(1), new RecurringConfig(3)), + new("Side Business", 850, MonthDate.Now.AddMonths(2), new RecurringConfig(1)), // Finite recurring income (with EndDate) new("Contract Work", 2_200, MonthDate.Now, new RecurringConfig(1, MonthDate.Now.AddMonths(18))), @@ -123,4 +128,5 @@ public static class ExampleData new("Luxury Purchase", 18_000, MonthDate.Now.AddMonths(42)), new("Investment Property Down Payment", 25_000, MonthDate.Now.AddMonths(45)), new("Retirement Catch-up", 30_000, MonthDate.Now.AddMonths(48))]); + } } diff --git a/src/Predictor.Web/Models/PredictorSettings.cs b/src/Predictor.Web/Models/PredictorSettings.cs new file mode 100644 index 0000000..0c57c8a --- /dev/null +++ b/src/Predictor.Web/Models/PredictorSettings.cs @@ -0,0 +1,26 @@ +namespace Predictor.Web.Models; + +public class PredictorSettings +{ + public const string SectionName = "PredictorSettings"; + + /// + /// Maximum number of months to calculate predictions for + /// + public int MaxCalculationPeriodMonths { get; set; } = 36; + + /// + /// Default initial budget for examples + /// + public decimal DefaultInitialBudget { get; set; } = 48_750m; + + /// + /// Enable/disable example data endpoint + /// + public bool EnableExampleData { get; set; } = true; + + /// + /// Maximum allowed calculation period to prevent abuse + /// + public int MaxAllowedCalculationPeriod { get; set; } = 120; // 10 years +} diff --git a/src/Predictor.Web/Program.cs b/src/Predictor.Web/Program.cs index e0d6be4..6139386 100644 --- a/src/Predictor.Web/Program.cs +++ b/src/Predictor.Web/Program.cs @@ -3,6 +3,10 @@ var builder = WebApplication.CreateBuilder(args); +// Configure PredictorSettings +builder.Services.Configure( + builder.Configuration.GetSection(PredictorSettings.SectionName)); + builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); @@ -19,20 +23,31 @@ app.UseAuthorization(); app.MapControllers(); -app.MapGet("/example-data", () => ExampleData.CalculateInputExample); +// Get settings +var settings = app.Services.GetRequiredService() + .GetSection(PredictorSettings.SectionName) + .Get() ?? new PredictorSettings(); + +// Conditionally register example data endpoint +if (settings.EnableExampleData) +{ + app.MapGet("/example-data", () => ExampleData.GetCalculateInputExample(settings)); +} app.MapPost("/calc", (CalculateInput input) => { var months = new List(); var budget = input.InitialBudget; - foreach (var currentMonth in MonthDate.Range(input.StartCalculationMonth, 12 * 3 - 1)) + var calculationPeriod = Math.Min(settings.MaxCalculationPeriodMonths, settings.MaxAllowedCalculationPeriod); + + foreach (var currentMonth in MonthDate.Range(input.StartCalculationMonth, calculationPeriod - 1)) { var month = Calculator.CalculateMonth(input, currentMonth, budget); budget = month.BudgetAfter; months.Add(month); } - return new CalculationOutput([.. months]); + return Results.Ok(new CalculationOutput([.. months])); }); app.Run(); \ No newline at end of file diff --git a/src/Predictor.Web/appsettings.Development.json b/src/Predictor.Web/appsettings.Development.json index 0c208ae..b451ebd 100644 --- a/src/Predictor.Web/appsettings.Development.json +++ b/src/Predictor.Web/appsettings.Development.json @@ -4,5 +4,11 @@ "Default": "Information", "Microsoft.AspNetCore": "Warning" } + }, + "PredictorSettings": { + "MaxCalculationPeriodMonths": 24, + "DefaultInitialBudget": 25000, + "EnableExampleData": true, + "MaxAllowedCalculationPeriod": 60 } } diff --git a/src/Predictor.Web/appsettings.json b/src/Predictor.Web/appsettings.json index 10f68b8..59b2773 100644 --- a/src/Predictor.Web/appsettings.json +++ b/src/Predictor.Web/appsettings.json @@ -5,5 +5,11 @@ "Microsoft.AspNetCore": "Warning" } }, - "AllowedHosts": "*" + "AllowedHosts": "*", + "PredictorSettings": { + "MaxCalculationPeriodMonths": 36, + "DefaultInitialBudget": 48750, + "EnableExampleData": true, + "MaxAllowedCalculationPeriod": 120 + } } From 7d1ee623e900a1e43995d4ebb789df363ccc34ac Mon Sep 17 00:00:00 2001 From: Hydra-Slayer Date: Sat, 12 Jul 2025 22:23:02 +0530 Subject: [PATCH 2/2] Refine configuration settings and documentation - Update PredictorSettings with better documentation - Improve configuration documentation - Refine Program.cs implementation - Add additional configuration examples --- docs/CONFIGURATION.md | 19 +++++++++++-------- src/Predictor.Web/Models/PredictorSettings.cs | 8 ++++---- src/Predictor.Web/Program.cs | 2 +- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 601290d..2a0e45f 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -6,16 +6,17 @@ The Predictor application is fully configurable through the `PredictorSettings` ### Available Settings -| Setting | Default | Description | -|---------|---------|-------------| -| `MaxCalculationPeriodMonths` | 36 | Maximum number of months to calculate predictions for | -| `DefaultInitialBudget` | 48750 | Default initial budget used in example data | -| `EnableExampleData` | true | Enable/disable the `/example-data` endpoint | -| `MaxAllowedCalculationPeriod` | 120 | Maximum allowed calculation period to prevent abuse (10 years) | +| Setting | Default | Description | +| ----------------------------- | ------- | -------------------------------------------------------------- | +| `MaxCalculationPeriodMonths` | 36 | Maximum number of months to calculate predictions for | +| `DefaultInitialBudget` | 48750 | Default initial budget used in example data | +| `EnableExampleData` | true | Enable/disable the `/example-data` endpoint | +| `MaxAllowedCalculationPeriod` | 120 | Maximum allowed calculation period to prevent abuse (10 years) | ### Configuration Examples #### appsettings.json + ```json { "PredictorSettings": { @@ -28,6 +29,7 @@ The Predictor application is fully configurable through the `PredictorSettings` ``` #### Environment Variables + ```bash # Docker/Container deployment PREDICTOR__MAXCALCULATIONPERIODMONTHS=24 @@ -40,8 +42,9 @@ ASPNETCORE_PREDICTORSETTINGS__MAXCALCULATIONPERIODMONTHS=24 ``` #### Docker Compose + ```yaml -version: '3.8' +version: "3.8" services: predictor: image: predictor:latest @@ -78,4 +81,4 @@ The application validates configuration at startup and uses sensible defaults if ✅ **Environment-Specific**: Different settings for dev/staging/production ✅ **Docker Ready**: Full support for container deployment ✅ **Twelve-Factor**: Follows twelve-factor app configuration principles -✅ **Security**: Sensitive settings can be injected via environment variables +✅ **Security**: Sensitive settings can be injected via environment variables diff --git a/src/Predictor.Web/Models/PredictorSettings.cs b/src/Predictor.Web/Models/PredictorSettings.cs index 0c57c8a..71bccc0 100644 --- a/src/Predictor.Web/Models/PredictorSettings.cs +++ b/src/Predictor.Web/Models/PredictorSettings.cs @@ -3,22 +3,22 @@ namespace Predictor.Web.Models; public class PredictorSettings { public const string SectionName = "PredictorSettings"; - + /// /// Maximum number of months to calculate predictions for /// public int MaxCalculationPeriodMonths { get; set; } = 36; - + /// /// Default initial budget for examples /// public decimal DefaultInitialBudget { get; set; } = 48_750m; - + /// /// Enable/disable example data endpoint /// public bool EnableExampleData { get; set; } = true; - + /// /// Maximum allowed calculation period to prevent abuse /// diff --git a/src/Predictor.Web/Program.cs b/src/Predictor.Web/Program.cs index 6139386..36bb1a9 100644 --- a/src/Predictor.Web/Program.cs +++ b/src/Predictor.Web/Program.cs @@ -39,7 +39,7 @@ var months = new List(); var budget = input.InitialBudget; var calculationPeriod = Math.Min(settings.MaxCalculationPeriodMonths, settings.MaxAllowedCalculationPeriod); - + foreach (var currentMonth in MonthDate.Range(input.StartCalculationMonth, calculationPeriod - 1)) { var month = Calculator.CalculateMonth(input, currentMonth, budget);