diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 928d35b..1653a13 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -14,7 +14,8 @@ "forwardPorts": [5000, 27017], "portsAttributes": { "5000": { - "label": "ASP.NET API" + "label": "ASP.NET API", + "visibility": "public" }, "27017": { "label": "MongoDB Atlas Local" @@ -27,5 +28,5 @@ "ASPNETCORE_ENVIRONMENT": "Development" }, "postCreateCommand": "dotnet restore", - "postStartCommand": "echo 'Startup seed runs when the API starts (SEED_ON_STARTUP=true by default).'" + "postStartCommand": "echo 'Startup seed runs when the API starts (StartupBehaviorSettings:SeedOnStartup=true by default).'" } diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index bd16077..dacf40d 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -1,6 +1,6 @@ services: app: - image: mcr.microsoft.com/devcontainers/dotnet:1-10.0-bookworm + "image": "mcr.microsoft.com/devcontainers/dotnet:dev-10.0" volumes: - ..:/workspaces/mongodb-dotnet-example:cached command: sleep infinity diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 542956f..13dcf71 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -55,7 +55,7 @@ jobs: - name: Wait for health run: | for i in {1..45}; do - if curl -fsS http://localhost:5050/healthz >/dev/null; then + if curl -fsS http://localhost:5050/health >/dev/null; then exit 0 fi sleep 2 diff --git a/AGENTS.md b/AGENTS.md index dd472eb..64c0b7c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,11 +18,11 @@ If port 5000 is already in use, stop the conflicting process or pass a different After any change that affects runtime behavior (`Program.cs`, `Startup.cs`, `Controllers`, `Services`, or `Models`), start the server and run the two curl commands below. Both must return HTTP 200 before considering the task complete. ```bash -curl http://localhost:5000/healthz +curl http://localhost:5000/health curl http://localhost:5000/api/games ``` -If the server fails to connect to MongoDB, verify the `.devcontainer` Atlas Local instance is running and that `GamesDatabaseSettings__ConnectionString` is set. Do not modify application code to bypass the connection failure. +If the server fails to connect to MongoDB, verify the `.devcontainer` Atlas Local instance is running and that `GamesDatabaseSettings:ConnectionString` is set in app settings. Do not modify application code to bypass the connection failure. ## Style @@ -40,17 +40,17 @@ Run `dotnet format` before completing any C# code change. - `.github/workflows/ci.yml`: build and integration smoke checks - `EDD.md`: MongoDB data model contract -## Environment Variables And Configuration +## Configuration -Required configuration (via environment variables or appsettings): +Primary configuration (app settings): -- `GamesDatabaseSettings__ConnectionString` (example: `mongodb://localhost:27017`) -- `GamesDatabaseSettings__DatabaseName` (example: `GamesDB`) -- `GamesDatabaseSettings__GamesCollectionName` (example: `Games`) +- `GamesDatabaseSettings:ConnectionString` (example: `mongodb://localhost:27017`) +- `GamesDatabaseSettings:DatabaseName` (example: `GamesDB`) +- `GamesDatabaseSettings:GamesCollectionName` (example: `Games`) +- `StartupBehaviorSettings:SeedOnStartup` (`true` by default; set `false` to disable startup seeding) -Optional: +Optional runtime override: -- `SEED_ON_STARTUP` (`true` by default; set `false` to disable startup seeding) - `ASPNETCORE_ENVIRONMENT` (`Development`, `Production`, etc.) ## MongoDB Skills diff --git a/EDD.md b/EDD.md index f9856be..6bcafab 100644 --- a/EDD.md +++ b/EDD.md @@ -33,7 +33,7 @@ Collection: `Games` ### Seed Behavior -- On API startup, if `SEED_ON_STARTUP` is not `false`, the app checks collection emptiness +- On API startup, if `StartupBehaviorSettings:SeedOnStartup` is `true`, the app checks collection emptiness - If empty, inserts default records from `Models/GameSeedData.cs` - Seed operation is idempotent by emptiness guard diff --git a/Models/StartupBehaviorSettings.cs b/Models/StartupBehaviorSettings.cs new file mode 100644 index 0000000..607a2c5 --- /dev/null +++ b/Models/StartupBehaviorSettings.cs @@ -0,0 +1,7 @@ +namespace mongodb_dotnet_example.Models +{ + public class StartupBehaviorSettings + { + public bool SeedOnStartup { get; set; } = true; + } +} diff --git a/README.md b/README.md index f2db3de..5d7286f 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ dotnet run --urls http://localhost:5000 3. Verify service health and seeded data: ```bash -curl http://localhost:5000/healthz +curl http://localhost:5000/health curl http://localhost:5000/api/games ``` @@ -77,19 +77,20 @@ dotnet run --urls http://0.0.0.0:5000 4. Open: - Swagger UI: `http://localhost:5000/swagger/index.html` -- Health endpoint: `http://localhost:5000/healthz` +- Health endpoint: `http://localhost:5000/health` -## Environment Variables +## App Settings -The app reads nested configuration via ASP.NET Core environment binding. +Application behavior is configured through `appsettings.json` and `appsettings.Development.json`. -| Name | Required | Example | Description | +| Section | Key | Default | Description | |---|---|---|---| -| `GamesDatabaseSettings__ConnectionString` | Yes | `mongodb://localhost:27017` | MongoDB connection string | -| `GamesDatabaseSettings__DatabaseName` | Yes | `GamesDB` | MongoDB database name | -| `GamesDatabaseSettings__GamesCollectionName` | Yes | `Games` | MongoDB collection name | -| `SEED_ON_STARTUP` | No | `true` | Inserts default games if collection is empty (`false` disables) | -| `ASPNETCORE_ENVIRONMENT` | No | `Development` | ASP.NET Core environment | +| `GamesDatabaseSettings` | `ConnectionString` | `mongodb://localhost:27017` | MongoDB connection string | +| `GamesDatabaseSettings` | `DatabaseName` | `GamesDB` | MongoDB database name | +| `GamesDatabaseSettings` | `GamesCollectionName` | `Games` | MongoDB collection name | +| `StartupBehaviorSettings` | `SeedOnStartup` | `true` | Inserts default games when collection is empty | + +Environment variables are still supported as optional ASP.NET Core overrides (for example in CI/CD), but the app no longer depends on direct environment-variable reads. ## MongoDB Features Demonstrated @@ -111,7 +112,7 @@ Relevant docs: | `POST` | `/api/games` | Create a game | | `PUT` | `/api/games/{id}` | Replace an existing game | | `DELETE` | `/api/games/{id}` | Delete a game | -| `GET` | `/healthz` | Health check endpoint | +| `GET` | `/health` | Health check endpoint | ## Project Structure @@ -143,19 +144,22 @@ dotnet build ## Troubleshooting 1. API starts but requests fail with MongoDB connection errors: -- Ensure MongoDB is running and reachable at `GamesDatabaseSettings__ConnectionString`. +- Ensure MongoDB is running and reachable at `GamesDatabaseSettings:ConnectionString` in app settings. 2. Port binding errors on `5000`: - Run with a different port: `dotnet run --urls http://localhost:5050`. 3. Empty response from `/api/games` after first run: -- Confirm `SEED_ON_STARTUP` is not set to `false`. +- Confirm `StartupBehaviorSettings:SeedOnStartup` is `true`. 4. Dev container builds but API cannot reach MongoDB: - In shared network mode, use `mongodb://localhost:27017` (not `mongodb://mongodb:27017`). 5. Swagger does not load: -- Check `/healthz` first; if healthy, verify `http://localhost:5000/swagger` and inspect server logs. +- Check `/health` first; if healthy, verify `http://localhost:5000/swagger` and inspect server logs. + +6. Receiving 502 error when visiting /health or /swagger from running in GitHub Codespaces +- Ensure that port 5000 is set to public visibility in the Ports tab inside Codespaces. ## Additional Resources diff --git a/Startup.cs b/Startup.cs index be237d6..b08bee8 100644 --- a/Startup.cs +++ b/Startup.cs @@ -30,6 +30,7 @@ public Startup(IConfiguration configuration) public void ConfigureServices(IServiceCollection services) { services.Configure(Configuration.GetSection(nameof(GamesDatabaseSettings))); + services.Configure(Configuration.GetSection(nameof(StartupBehaviorSettings))); services.AddSingleton(sp => sp.GetRequiredService>().Value); @@ -44,13 +45,13 @@ public void ConfigureServices(IServiceCollection services) } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IOptions startupBehaviorOptions) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); - - + + } app.UseSwagger(); @@ -65,8 +66,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseAuthorization(); - var seedOnStartup = Environment.GetEnvironmentVariable("SEED_ON_STARTUP"); - if (!string.Equals(seedOnStartup, "false", StringComparison.OrdinalIgnoreCase)) + if (startupBehaviorOptions.Value.SeedOnStartup) { var gamesService = app.ApplicationServices.GetRequiredService(); gamesService.SeedIfEmpty(GameSeedData.DefaultGames); @@ -74,8 +74,8 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseEndpoints(endpoints => { - endpoints.MapControllers(); - endpoints.MapHealthChecks("/healthz"); + endpoints.MapControllers(); + endpoints.MapHealthChecks("/health"); }); } } diff --git a/appsettings.Development.json b/appsettings.Development.json index 39542bf..eafc681 100644 --- a/appsettings.Development.json +++ b/appsettings.Development.json @@ -4,6 +4,9 @@ "ConnectionString": "mongodb://localhost:27017", "DatabaseName": "GamesDB" }, + "StartupBehaviorSettings": { + "SeedOnStartup": true + }, "Logging": { "LogLevel": { "Default": "Information", diff --git a/appsettings.json b/appsettings.json index 1bb6c35..5bf0f8e 100644 --- a/appsettings.json +++ b/appsettings.json @@ -4,6 +4,9 @@ "ConnectionString": "mongodb://localhost:27017", "DatabaseName": "GamesDB" }, + "StartupBehaviorSettings": { + "SeedOnStartup": true + }, "Logging": { "LogLevel": { "Default": "Information", diff --git a/mongodb-dotnet-example.sln b/mongodb-dotnet-example.sln index e6085ca..f1152cb 100644 --- a/mongodb-dotnet-example.sln +++ b/mongodb-dotnet-example.sln @@ -1,34 +1,53 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.30114.105 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "mongodb-dotnet-example", "mongodb-dotnet-example.csproj", "{647E6B76-A5E1-4512-AD1F-81714328BADC}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Debug|x64 = Debug|x64 - Debug|x86 = Debug|x86 - Release|Any CPU = Release|Any CPU - Release|x64 = Release|x64 - Release|x86 = Release|x86 - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {647E6B76-A5E1-4512-AD1F-81714328BADC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {647E6B76-A5E1-4512-AD1F-81714328BADC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {647E6B76-A5E1-4512-AD1F-81714328BADC}.Debug|x64.ActiveCfg = Debug|Any CPU - {647E6B76-A5E1-4512-AD1F-81714328BADC}.Debug|x64.Build.0 = Debug|Any CPU - {647E6B76-A5E1-4512-AD1F-81714328BADC}.Debug|x86.ActiveCfg = Debug|Any CPU - {647E6B76-A5E1-4512-AD1F-81714328BADC}.Debug|x86.Build.0 = Debug|Any CPU - {647E6B76-A5E1-4512-AD1F-81714328BADC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {647E6B76-A5E1-4512-AD1F-81714328BADC}.Release|Any CPU.Build.0 = Release|Any CPU - {647E6B76-A5E1-4512-AD1F-81714328BADC}.Release|x64.ActiveCfg = Release|Any CPU - {647E6B76-A5E1-4512-AD1F-81714328BADC}.Release|x64.Build.0 = Release|Any CPU - {647E6B76-A5E1-4512-AD1F-81714328BADC}.Release|x86.ActiveCfg = Release|Any CPU - {647E6B76-A5E1-4512-AD1F-81714328BADC}.Release|x86.Build.0 = Release|Any CPU - EndGlobalSection -EndGlobal + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30114.105 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "mongodb-dotnet-example", "mongodb-dotnet-example.csproj", "{647E6B76-A5E1-4512-AD1F-81714328BADC}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05-4346-4AA6-1389-037BE0695223}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MongodbDotnetExample.Tests", "tests\MongodbDotnetExample.Tests\MongodbDotnetExample.Tests.csproj", "{6157DA5A-9844-42B4-A835-E37A2FF91C8F}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {647E6B76-A5E1-4512-AD1F-81714328BADC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {647E6B76-A5E1-4512-AD1F-81714328BADC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {647E6B76-A5E1-4512-AD1F-81714328BADC}.Debug|x64.ActiveCfg = Debug|Any CPU + {647E6B76-A5E1-4512-AD1F-81714328BADC}.Debug|x64.Build.0 = Debug|Any CPU + {647E6B76-A5E1-4512-AD1F-81714328BADC}.Debug|x86.ActiveCfg = Debug|Any CPU + {647E6B76-A5E1-4512-AD1F-81714328BADC}.Debug|x86.Build.0 = Debug|Any CPU + {647E6B76-A5E1-4512-AD1F-81714328BADC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {647E6B76-A5E1-4512-AD1F-81714328BADC}.Release|Any CPU.Build.0 = Release|Any CPU + {647E6B76-A5E1-4512-AD1F-81714328BADC}.Release|x64.ActiveCfg = Release|Any CPU + {647E6B76-A5E1-4512-AD1F-81714328BADC}.Release|x64.Build.0 = Release|Any CPU + {647E6B76-A5E1-4512-AD1F-81714328BADC}.Release|x86.ActiveCfg = Release|Any CPU + {647E6B76-A5E1-4512-AD1F-81714328BADC}.Release|x86.Build.0 = Release|Any CPU + {6157DA5A-9844-42B4-A835-E37A2FF91C8F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6157DA5A-9844-42B4-A835-E37A2FF91C8F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6157DA5A-9844-42B4-A835-E37A2FF91C8F}.Debug|x64.ActiveCfg = Debug|Any CPU + {6157DA5A-9844-42B4-A835-E37A2FF91C8F}.Debug|x64.Build.0 = Debug|Any CPU + {6157DA5A-9844-42B4-A835-E37A2FF91C8F}.Debug|x86.ActiveCfg = Debug|Any CPU + {6157DA5A-9844-42B4-A835-E37A2FF91C8F}.Debug|x86.Build.0 = Debug|Any CPU + {6157DA5A-9844-42B4-A835-E37A2FF91C8F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6157DA5A-9844-42B4-A835-E37A2FF91C8F}.Release|Any CPU.Build.0 = Release|Any CPU + {6157DA5A-9844-42B4-A835-E37A2FF91C8F}.Release|x64.ActiveCfg = Release|Any CPU + {6157DA5A-9844-42B4-A835-E37A2FF91C8F}.Release|x64.Build.0 = Release|Any CPU + {6157DA5A-9844-42B4-A835-E37A2FF91C8F}.Release|x86.ActiveCfg = Release|Any CPU + {6157DA5A-9844-42B4-A835-E37A2FF91C8F}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {6157DA5A-9844-42B4-A835-E37A2FF91C8F} = {0AB3BF05-4346-4AA6-1389-037BE0695223} + EndGlobalSection +EndGlobal