diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..76e0161 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,5 @@ +{ + "enabledPlugins": { + "mongodb@claude-plugins-official": true + } +} diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..928d35b --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,31 @@ +{ + "name": "mongodb-dotnet-example", + "dockerComposeFile": "docker-compose.yml", + "service": "app", + "workspaceFolder": "/workspaces/mongodb-dotnet-example", + "customizations": { + "vscode": { + "extensions": [ + "ms-dotnettools.csharp", + "mongodb.mongodb-vscode" + ] + } + }, + "forwardPorts": [5000, 27017], + "portsAttributes": { + "5000": { + "label": "ASP.NET API" + }, + "27017": { + "label": "MongoDB Atlas Local" + } + }, + "remoteEnv": { + "GamesDatabaseSettings__ConnectionString": "mongodb://localhost:27017", + "GamesDatabaseSettings__DatabaseName": "GamesDB", + "GamesDatabaseSettings__GamesCollectionName": "Games", + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "postCreateCommand": "dotnet restore", + "postStartCommand": "echo 'Startup seed runs when the API starts (SEED_ON_STARTUP=true by default).'" +} diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml new file mode 100644 index 0000000..bd16077 --- /dev/null +++ b/.devcontainer/docker-compose.yml @@ -0,0 +1,27 @@ +services: + app: + image: mcr.microsoft.com/devcontainers/dotnet:1-10.0-bookworm + volumes: + - ..:/workspaces/mongodb-dotnet-example:cached + command: sleep infinity + network_mode: service:mongodb + depends_on: + - mongodb + + mongodb: + image: mongodb/mongodb-atlas-local:8.0.3-20250506T093411Z + restart: unless-stopped + volumes: + - mongodb-data:/data/db + - mongodb-config:/data/configdb + ports: + - "27017:27017" + healthcheck: + test: ["CMD-SHELL", "mongosh --eval \"db.adminCommand({ ping: 1 })\" || exit 1"] + interval: 10s + timeout: 5s + retries: 12 + +volumes: + mongodb-data: + mongodb-config: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..542956f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,70 @@ +name: ci + +on: + push: + branches: [ main ] + pull_request: + +jobs: + build-and-smoke: + runs-on: ubuntu-latest + strategy: + max-parallel: 1 + matrix: + dotnet-version: ["10.0.x"] + + services: + mongodb: + image: mongo:latest + ports: + - 27017:27017 + options: >- + --health-cmd "mongosh --eval 'db.adminCommand({ ping: 1 })'" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + env: + GamesDatabaseSettings__ConnectionString: mongodb://localhost:27017 + GamesDatabaseSettings__DatabaseName: GamesDB + GamesDatabaseSettings__GamesCollectionName: Games + ASPNETCORE_ENVIRONMENT: Development + ASPNETCORE_URLS: http://localhost:5050 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: ${{ matrix.dotnet-version }} + + - name: Restore + run: dotnet restore + + - name: Build + run: dotnet build --configuration Release --no-restore + + - name: Test + run: dotnet test tests/MongodbDotnetExample.Tests/MongodbDotnetExample.Tests.csproj --configuration Release --no-restore + + - name: Start API + run: dotnet run --configuration Release --no-build --no-launch-profile > /tmp/server.log 2>&1 & + + - name: Wait for health + run: | + for i in {1..45}; do + if curl -fsS http://localhost:5050/healthz >/dev/null; then + exit 0 + fi + sleep 2 + done + cat /tmp/server.log + exit 1 + + - name: Verify seeded data endpoint + run: | + body=$(curl -fsS http://localhost:5050/api/games) + echo "$body" + echo "$body" | grep -q "Celeste" diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..dd472eb --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,71 @@ +# AGENTS.md + +This file guides coding agents working in this repository. + +## Build And Test Commands + +```bash +dotnet restore +dotnet build +dotnet test tests/MongodbDotnetExample.Tests/MongodbDotnetExample.Tests.csproj +dotnet run --urls http://localhost:5000 +``` + +This repository has an xUnit test suite plus the smoke checks below. + +If port 5000 is already in use, stop the conflicting process or pass a different port via `--urls`; do not change the default port in source code. + +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/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. + +## Style + +Run `dotnet format` before completing any C# code change. + +## Project Structure + +- `Program.cs`, `Startup.cs`: ASP.NET Core host and middleware pipeline +- `Controllers/GamesController.cs`: REST routes for `Game` CRUD +- `Services/GamesService.cs`: MongoDB access layer and seed helper +- `Models/Game.cs`: MongoDB document model +- `Models/GamesDatabaseSettings.cs`: bound configuration schema +- `Models/GameSeedData.cs`: default dataset inserted when collection is empty +- `.devcontainer/`: local dev environment with Atlas Local +- `.github/workflows/ci.yml`: build and integration smoke checks +- `EDD.md`: MongoDB data model contract + +## Environment Variables And Configuration + +Required configuration (via environment variables or appsettings): + +- `GamesDatabaseSettings__ConnectionString` (example: `mongodb://localhost:27017`) +- `GamesDatabaseSettings__DatabaseName` (example: `GamesDB`) +- `GamesDatabaseSettings__GamesCollectionName` (example: `Games`) + +Optional: + +- `SEED_ON_STARTUP` (`true` by default; set `false` to disable startup seeding) +- `ASPNETCORE_ENVIRONMENT` (`Development`, `Production`, etc.) + +## MongoDB Skills + +Before performing MongoDB schema, query, aggregation, or index work, fetch the skill index from https://github.com/mongodb/agent-skills and use any skill whose description matches the task. If no matching skill is found, proceed without one and note this in your response. + +## When To Use EDD.md + +Use [EDD.md](./EDD.md) as the source of truth for the MongoDB data model in this repository. + +Consult [EDD.md](./EDD.md) before making changes that touch: + +- MongoDB collections, document structure, or field names +- ASP.NET Core controllers and service methods that read or write database records +- Validation, form fields, API payloads, or UI that depend on persisted data +- Schema documentation, Mermaid diagrams, or entity modeling discussions + +If `EDD.md` and the C# model code disagree, treat `EDD.md` as authoritative and update the C# model to match, then call out the discrepancy in your response. diff --git a/Controllers/GamesController.cs b/Controllers/GamesController.cs index fc9822f..6637845 100644 --- a/Controllers/GamesController.cs +++ b/Controllers/GamesController.cs @@ -9,9 +9,9 @@ namespace mongodb_dotnet_example.Controllers [ApiController] public class GamesController : ControllerBase { - private readonly GamesService _gameService; + private readonly IGamesService _gameService; - public GamesController(GamesService gamesService) + public GamesController(IGamesService gamesService) { _gameService = gamesService; } diff --git a/EDD.md b/EDD.md new file mode 100644 index 0000000..f9856be --- /dev/null +++ b/EDD.md @@ -0,0 +1,55 @@ +# EDD.md + +Entity Document Diagram for `mongodb-dotnet-example`. + +## Metadata + +- Database: `GamesDB` +- Primary Collection: `Games` +- Source of truth in code: `Models/Game.cs`, `Services/GamesService.cs` + +## Entity: Game + +Collection: `Games` + +### Fields + +| Field | BSON Type | C# Type | Required | Notes | +|---|---|---|---|---| +| `_id` | `ObjectId` | `string` (`[BsonRepresentation(ObjectId)]`) | Yes | MongoDB primary key | +| `Name` | `String` | `string` | Yes | Serialized with `[BsonElement("Name")]` | +| `Price` | `Decimal128` or numeric-compatible | `decimal` | Yes | Monetary value | +| `Category` | `String` | `string` | Yes | Game genre/category | + +### Indexes + +- Default unique index on `_id` +- No additional secondary indexes are defined by application code + +### Validation And Constraints + +- Route constraints require `id` values to have length 24 for get/update/delete endpoints +- No MongoDB schema validator is currently configured in code + +### Seed Behavior + +- On API startup, if `SEED_ON_STARTUP` is not `false`, the app checks collection emptiness +- If empty, inserts default records from `Models/GameSeedData.cs` +- Seed operation is idempotent by emptiness guard + +## Relationships + +- `Game` has no document references in current data model +- Single-collection design + +## Mermaid Diagram + +```mermaid +erDiagram + GAMES { + ObjectId _id PK + string Name + decimal Price + string Category + } +``` diff --git a/Models/GameSeedData.cs b/Models/GameSeedData.cs new file mode 100644 index 0000000..f637728 --- /dev/null +++ b/Models/GameSeedData.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; + +namespace mongodb_dotnet_example.Models +{ + public static class GameSeedData + { + public static readonly IReadOnlyList DefaultGames = new List + { + new Game { Name = "Celeste", Price = 19.99m, Category = "Platformer" }, + new Game { Name = "Hades", Price = 24.99m, Category = "Roguelike" }, + new Game { Name = "Stardew Valley", Price = 14.99m, Category = "Simulation" }, + new Game { Name = "Forza Horizon 5", Price = 59.99m, Category = "Racing" }, + new Game { Name = "Minecraft", Price = 29.99m, Category = "Sandbox" } + }; + } +} \ No newline at end of file diff --git a/Properties/launchSettings.json b/Properties/launchSettings.json index 6fdf0aa..6e8ebcd 100644 --- a/Properties/launchSettings.json +++ b/Properties/launchSettings.json @@ -19,7 +19,7 @@ }, "mongodb_dotnet_example": { "commandName": "Project", - "dotnetRunMessages": "true", + "dotnetRunMessages": true, "launchBrowser": true, "launchUrl": "swagger", "applicationUrl": "https://localhost:5001;http://localhost:5000", diff --git a/README.md b/README.md index 5264677..f2db3de 100644 --- a/README.md +++ b/README.md @@ -1,55 +1,167 @@ -![MongoDB and C# logo](./images/banner.png) +# MongoDB Games API with ASP.NET Core -## Introduction -Welcome to this MongoDB and ASP.Net Core Web API sample project. -The aim of this project is to give you a working example of how you can use the power of MongoDB Atlas and .NET to create modern applications. -This project is intended to be a companion project to the article [How to use MongoDB Atlas with .NET/.NET Core](https://www.mongodb.com/languages/how-to-use-mongodb-with-dotnet) from the MongoDB website. +This repository is a minimal .NET Web API that demonstrates CRUD operations against MongoDB for a `games` domain model. +It is designed for developers learning how to connect ASP.NET Core services to MongoDB Atlas or a local MongoDB instance. -## Getting Started +[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/mongodb-developer/mongodb-dotnet-example) -1. Clone this repo to your local machine -2. Open the project in your IDE of choice -3. Edit appsettings.json and appsettings.Development.json and update the ConnectionString field with your connection string from the 'Connect' button for your cluster in the [Atlas UI](https://cloud.mongodb.com) -4. Run the project to allow you access to the endpoints for all CRUD operations. +## Features -## Getting to know the code +- ASP.NET Core Web API with Swagger UI +- MongoDB .NET Driver integration through a dedicated service layer +- CRUD endpoints for a `Game` entity +- Startup seed behavior that inserts sample games when the collection is empty +- Dev Container support with MongoDB Atlas Local for fast onboarding +- GitHub Actions CI with a local MongoDB service container -The below diagram shows the overall architecture of the application and the following sections will explain the code more. +## Architecture Overview + +The API receives HTTP requests in `GamesController`, delegates data operations to `GamesService`, and persists documents in MongoDB. ![Architecture diagram for the Web API with MongoDB](./images/architecture.jpeg) -### Controllers +## Tech Stack + +- .NET 10 Web API +- MongoDB .NET Driver (`MongoDB.Driver`) +- Swagger / OpenAPI (`Swashbuckle.AspNetCore`) +- MongoDB Atlas Local (dev container), `mongo:latest` (CI) + +## Prerequisites + +- .NET SDK 10+ +- Docker 24+ (for local MongoDB or dev container workflows) +- Optional: MongoDB Atlas cluster connection string + +## Quick Start (Local) + +1. Start MongoDB locally (Docker example): + +```bash +docker run --rm -d --name mongo-local -p 27017:27017 mongo:latest +``` + +2. Restore and run the API: + +```bash +dotnet restore +dotnet run --urls http://localhost:5000 +``` + +3. Verify service health and seeded data: + +```bash +curl http://localhost:5000/healthz +curl http://localhost:5000/api/games +``` + +4. Open the API documentation UI: + +```text +http://localhost:5000/swagger/index.html +``` + +Expected outcomes: +- Health endpoint returns `Healthy` +- `GET /api/games` returns a JSON array containing sample records (for example `Celeste`, `Hades`) + +## Run in Codespaces / Dev Container + +1. Open the repository in GitHub Codespaces (badge above) or VS Code Dev Containers. +2. Wait for container setup to complete (`dotnet restore` runs automatically). +3. Run the API: + +```bash +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` + +## Environment Variables + +The app reads nested configuration via ASP.NET Core environment binding. + +| Name | Required | Example | 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 | + +## MongoDB Features Demonstrated + +- MongoDB document mapping with BSON attributes (`Game` model) +- Collection-level CRUD operations (`Find`, `InsertOne`, `ReplaceOne`, `DeleteOne`) +- Configuration-driven database and collection selection +- Standardized MongoDB client `appName` for telemetry/observability + +Relevant docs: +- [MongoDB .NET/C# Driver](https://www.mongodb.com/docs/drivers/csharp/) +- [MongoDB Atlas](https://www.mongodb.com/docs/atlas/) + +## API Overview -The GamesController.cs class is where the routes/endpoints for the api are defined. +| Method | Path | Purpose | +|---|---|---| +| `GET` | `/api/games` | List all games | +| `GET` | `/api/games/{id}` | Get one game by ObjectId | +| `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 | -Each endpoint calls to a method in the GamesService.cs class. +## Project Structure -### Services +- `Controllers/GamesController.cs`: API routes and HTTP responses +- `Services/GamesService.cs`: MongoDB data access and seed helper +- `Models/Game.cs`: Document model +- `Models/GamesDatabaseSettings.cs`: Configuration contracts +- `Models/GameSeedData.cs`: Default seed dataset +- `.devcontainer/`: Dev Container and Atlas Local configuration +- `.github/workflows/ci.yml`: CI build and smoke/integration checks +- `EDD.md`: MongoDB entity and index contract -The GamesService.cs class contains the code that uses the MongoDB.Driver NuGet package to carry out CRUD operations against your Cluster. +## Testing and CI -### Models +CI runs on GitHub Actions and performs: +- `dotnet test tests/MongodbDotnetExample.Tests/MongodbDotnetExample.Tests.csproj` +- `dotnet restore` +- `dotnet build` +- API startup + health check +- CRUD smoke check against a local MongoDB service container -The Game.cs class acts as a model you can use throughout the project and the properties in it, map to the fields in the MongoDB document. +Run locally: -GamesDatabaseSettings.cs contains an interface and implementation that maps to the GamesDatabaseSettings section in appsettings.json and appsettings.Development.json. In this application, the connection string to your cluster is stored here, but normally in production, you would combine this with [user secrets](https://docs.microsoft.com/en-us/aspnet/core/security/app-secrets?view=aspnetcore-5.0&tabs=windows). +```bash +dotnet test tests/MongodbDotnetExample.Tests/MongodbDotnetExample.Tests.csproj +dotnet build +``` -### Project Root +## Troubleshooting -At the root of the project is the usual files that come out of the box with an ASP.NET Core Web API project. +1. API starts but requests fail with MongoDB connection errors: +- Ensure MongoDB is running and reachable at `GamesDatabaseSettings__ConnectionString`. -The only file with changes made here is the Startup.cs class. Inside the ConfigureServices method, the appsettings code is pulled in and the Database settings and Games Service are added to dependency injection for use in other classes. +2. Port binding errors on `5000`: +- Run with a different port: `dotnet run --urls http://localhost:5050`. -## Running the code +3. Empty response from `/api/games` after first run: +- Confirm `SEED_ON_STARTUP` is not set to `false`. -If your IDE supports it, you can go ahead and run the application from inside the IDE. +4. Dev container builds but API cannot reach MongoDB: +- In shared network mode, use `mongodb://localhost:27017` (not `mongodb://mongodb:27017`). -If you prefer to run it from your terminal/command-line, you can use ``` dotnet run ```. +5. Swagger does not load: +- Check `/healthz` first; if healthy, verify `http://localhost:5000/swagger` and inspect server logs. -## More information +## Additional Resources -If you want more information about MongoDB and Atlas, the powerful cloud-based database solution, you can view [the documentation](https://docs.atlas.mongodb.com/). +- [How to use MongoDB Atlas with .NET/.NET Core](https://www.mongodb.com/languages/how-to-use-mongodb-with-dotnet) +- [MongoDB C# Driver CRUD Quick Reference](https://www.mongodb.com/docs/drivers/csharp/current/fundamentals/crud/) -## Disclaimer +## License -Use at your own risk; not a supported MongoDB product +This project is licensed under the terms in [LICENSE](./LICENSE). diff --git a/Services/GamesService.cs b/Services/GamesService.cs index 3366b4f..20c457f 100644 --- a/Services/GamesService.cs +++ b/Services/GamesService.cs @@ -5,13 +5,33 @@ namespace mongodb_dotnet_example.Services { - public class GamesService + public interface IGamesService + { + List Get(); + + Game Get(string id); + + Game Create(Game game); + + void Update(string id, Game updatedGame); + + void Delete(Game gameForDeletion); + + void Delete(string id); + + void SeedIfEmpty(IEnumerable seedGames); + } + + public class GamesService : IGamesService { private readonly IMongoCollection _games; public GamesService(IGamesDatabaseSettings settings) { - var client = new MongoClient(settings.ConnectionString); + var clientSettings = MongoClientSettings.FromConnectionString(settings.ConnectionString); + clientSettings.ApplicationName = "mongodb-dotnet-example-api"; + + var client = new MongoClient(clientSettings); var database = client.GetDatabase(settings.DatabaseName); _games = database.GetCollection(settings.GamesCollectionName); @@ -32,5 +52,15 @@ public Game Create(Game game) public void Delete(Game gameForDeletion) => _games.DeleteOne(game => game.Id == gameForDeletion.Id); public void Delete(string id) => _games.DeleteOne(game => game.Id == id); + + public void SeedIfEmpty(IEnumerable seedGames) + { + if (_games.CountDocuments(_ => true) > 0) + { + return; + } + + _games.InsertMany(seedGames); + } } } \ No newline at end of file diff --git a/Startup.cs b/Startup.cs index f1b43e5..be237d6 100644 --- a/Startup.cs +++ b/Startup.cs @@ -33,9 +33,10 @@ public void ConfigureServices(IServiceCollection services) services.AddSingleton(sp => sp.GetRequiredService>().Value); - services.AddSingleton(); + services.AddSingleton(); services.AddControllers(); + services.AddHealthChecks(); services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "mongodb_dotnet_example", Version = "v1" }); @@ -55,15 +56,26 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseSwagger(); app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "mongodb_dotnet_example v1")); - app.UseHttpsRedirection(); + if (!env.IsDevelopment()) + { + app.UseHttpsRedirection(); + } app.UseRouting(); app.UseAuthorization(); + var seedOnStartup = Environment.GetEnvironmentVariable("SEED_ON_STARTUP"); + if (!string.Equals(seedOnStartup, "false", StringComparison.OrdinalIgnoreCase)) + { + var gamesService = app.ApplicationServices.GetRequiredService(); + gamesService.SeedIfEmpty(GameSeedData.DefaultGames); + } + app.UseEndpoints(endpoints => { endpoints.MapControllers(); + endpoints.MapHealthChecks("/healthz"); }); } } diff --git a/appsettings.Development.json b/appsettings.Development.json index a64dc4b..39542bf 100644 --- a/appsettings.Development.json +++ b/appsettings.Development.json @@ -1,7 +1,7 @@ { "GamesDatabaseSettings": { "GamesCollectionName": "Games", - "ConnectionString": "", + "ConnectionString": "mongodb://localhost:27017", "DatabaseName": "GamesDB" }, "Logging": { diff --git a/images/banner.png b/images/banner.png deleted file mode 100644 index 898ef9f..0000000 Binary files a/images/banner.png and /dev/null differ diff --git a/mongodb-dotnet-example.csproj b/mongodb-dotnet-example.csproj index 723e877..df8d88d 100644 --- a/mongodb-dotnet-example.csproj +++ b/mongodb-dotnet-example.csproj @@ -1,13 +1,17 @@ - net7.0 + net10.0 mongodb_dotnet_example - + + + + + diff --git a/tests/MongodbDotnetExample.Tests/GamesControllerTests.cs b/tests/MongodbDotnetExample.Tests/GamesControllerTests.cs new file mode 100644 index 0000000..d1d73ac --- /dev/null +++ b/tests/MongodbDotnetExample.Tests/GamesControllerTests.cs @@ -0,0 +1,129 @@ +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc; +using mongodb_dotnet_example.Controllers; +using mongodb_dotnet_example.Models; +using mongodb_dotnet_example.Services; +using Xunit; + +namespace mongodb_dotnet_example.Tests +{ + public class GamesControllerTests + { + [Fact] + public void Get_ReturnsAllGames() + { + var expectedGames = new List + { + new Game { Id = "0123456789abcdef01234567", Name = "Celeste", Price = 19.99m, Category = "Platformer" }, + new Game { Id = "fedcba987654321001234567", Name = "Hades", Price = 24.99m, Category = "Roguelike" } + }; + var controller = new GamesController(new FakeGamesService(expectedGames)); + + var result = controller.Get(); + + Assert.Equal(expectedGames, result.Value); + } + + [Fact] + public void Get_WhenGameExists_ReturnsGame() + { + var game = new Game { Id = "0123456789abcdef01234567", Name = "Celeste", Price = 19.99m, Category = "Platformer" }; + var controller = new GamesController(new FakeGamesService(new List { game })); + + var result = controller.Get(game.Id); + + Assert.Equal(game, result.Value); + } + + [Fact] + public void Get_WhenGameIsMissing_ReturnsNotFound() + { + var controller = new GamesController(new FakeGamesService()); + + var result = controller.Get("0123456789abcdef01234567"); + + Assert.IsType(result.Result); + } + + [Fact] + public void Create_ReturnsCreatedAtRouteResult() + { + var controller = new GamesController(new FakeGamesService()); + var newGame = new Game { Name = "Stardew Valley", Price = 14.99m, Category = "Simulation" }; + + var result = controller.Create(newGame); + + var createdAtRouteResult = Assert.IsType(result.Result); + Assert.Equal("GetGame", createdAtRouteResult.RouteName); + Assert.Equal(newGame, createdAtRouteResult.Value); + Assert.Equal(newGame.Id, createdAtRouteResult.RouteValues["id"]); + } + + [Fact] + public void Update_WhenGameExists_ReturnsNoContent() + { + var existingGame = new Game { Id = "0123456789abcdef01234567", Name = "Celeste", Price = 19.99m, Category = "Platformer" }; + var controller = new GamesController(new FakeGamesService(new List { existingGame })); + var updatedGame = new Game { Id = existingGame.Id, Name = "Celeste", Price = 17.99m, Category = "Platformer" }; + + var result = controller.Update(existingGame.Id, updatedGame); + + Assert.IsType(result); + } + + [Fact] + public void Delete_WhenGameExists_ReturnsNoContent() + { + var existingGame = new Game { Id = "0123456789abcdef01234567", Name = "Celeste", Price = 19.99m, Category = "Platformer" }; + var fakeService = new FakeGamesService(new List { existingGame }); + var controller = new GamesController(fakeService); + + var result = controller.Delete(existingGame.Id); + + Assert.IsType(result); + Assert.Null(fakeService.Get(existingGame.Id)); + } + + private sealed class FakeGamesService : IGamesService + { + private readonly List games; + + public FakeGamesService(List seedGames = null) + { + games = seedGames ?? new List(); + } + + public List Get() => new List(games); + + public Game Get(string id) => games.Find(game => game.Id == id); + + public Game Create(Game game) + { + game.Id ??= "0123456789abcdef01234567"; + games.Add(game); + return game; + } + + public void Update(string id, Game updatedGame) + { + var index = games.FindIndex(game => game.Id == id); + if (index >= 0) + { + games[index] = updatedGame; + } + } + + public void Delete(Game gameForDeletion) => games.RemoveAll(game => game.Id == gameForDeletion.Id); + + public void Delete(string id) => games.RemoveAll(game => game.Id == id); + + public void SeedIfEmpty(IEnumerable seedGames) + { + if (games.Count == 0) + { + games.AddRange(seedGames); + } + } + } + } +} diff --git a/tests/MongodbDotnetExample.Tests/MongodbDotnetExample.Tests.csproj b/tests/MongodbDotnetExample.Tests/MongodbDotnetExample.Tests.csproj new file mode 100644 index 0000000..5784068 --- /dev/null +++ b/tests/MongodbDotnetExample.Tests/MongodbDotnetExample.Tests.csproj @@ -0,0 +1,21 @@ + + + + net10.0 + false + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + +