From 775aa29550531f5d67adbb9890548a7e7f426448 Mon Sep 17 00:00:00 2001 From: edwardgnt Date: Wed, 5 Aug 2026 14:00:03 -0700 Subject: [PATCH 1/6] Upgrade to .NET 10 and add SQL Server Docker setup --- .env.example | 1 + .gitignore | 3 +- .../BooksAPIDapper.Tests.csproj | 2 +- BooksAPIDapper/BooksAPIDapper.csproj | 2 +- compose.yaml | 15 ++++++++ sql/init/01-create-books-db.sql | 35 +++++++++++++++++++ 6 files changed, 55 insertions(+), 3 deletions(-) create mode 100644 .env.example create mode 100644 compose.yaml create mode 100644 sql/init/01-create-books-db.sql diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..ad057e5 --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +SA_PASSWORD=YourStrongPassword123! \ No newline at end of file diff --git a/.gitignore b/.gitignore index 9491a2f..231c9dc 100644 --- a/.gitignore +++ b/.gitignore @@ -360,4 +360,5 @@ MigrationBackup/ .ionide/ # Fody - auto-generated XML schema -FodyWeavers.xsd \ No newline at end of file +FodyWeavers.xsd +.env diff --git a/BooksAPIDapper.Tests/BooksAPIDapper.Tests.csproj b/BooksAPIDapper.Tests/BooksAPIDapper.Tests.csproj index ece7e92..3299adc 100644 --- a/BooksAPIDapper.Tests/BooksAPIDapper.Tests.csproj +++ b/BooksAPIDapper.Tests/BooksAPIDapper.Tests.csproj @@ -1,7 +1,7 @@ ๏ปฟ - net9.0 + net10.0 enable enable false diff --git a/BooksAPIDapper/BooksAPIDapper.csproj b/BooksAPIDapper/BooksAPIDapper.csproj index afe4b20..3db81ed 100644 --- a/BooksAPIDapper/BooksAPIDapper.csproj +++ b/BooksAPIDapper/BooksAPIDapper.csproj @@ -1,7 +1,7 @@ - net9.0 + net10.0 enable enable diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..62af7a8 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,15 @@ +services: + sqlserver: + image: mcr.microsoft.com/mssql/server:2022-latest + container_name: booksapi-sqlserver + environment: + ACCEPT_EULA: "Y" + MSSQL_SA_PASSWORD: "${SA_PASSWORD}" + MSSQL_PID: "Developer" + ports: + - "14333:1433" + volumes: + - booksapi_sql_data:/var/opt/mssql + +volumes: + booksapi_sql_data: \ No newline at end of file diff --git a/sql/init/01-create-books-db.sql b/sql/init/01-create-books-db.sql new file mode 100644 index 0000000..03ae098 --- /dev/null +++ b/sql/init/01-create-books-db.sql @@ -0,0 +1,35 @@ +IF DB_ID(N'BooksDb') IS NULL +BEGIN + CREATE DATABASE BooksDb; +END +GO + +USE BooksDb; +GO + +IF OBJECT_ID(N'dbo.Books', N'U') IS NULL +BEGIN + CREATE TABLE dbo.Books + ( + Id INT IDENTITY(1,1) NOT NULL PRIMARY KEY, + Title NVARCHAR(200) NOT NULL, + Author NVARCHAR(200) NOT NULL, + YearPublished INT NOT NULL, + CreatedAt DATETIME2 NOT NULL CONSTRAINT DF_Books_CreatedAt DEFAULT SYSUTCDATETIME(), + IsArchived BIT NOT NULL CONSTRAINT DF_Books_IsArchived DEFAULT 0, + Price DECIMAL(10,2) NOT NULL CONSTRAINT DF_Books_Price DEFAULT 0 + ); +END +GO + +IF NOT EXISTS (SELECT 1 FROM dbo.Books) +BEGIN + INSERT INTO dbo.Books (Title, Author, YearPublished, CreatedAt, IsArchived, Price) + VALUES + ('Clean Code', 'Robert C. Martin', 2008, SYSUTCDATETIME(), 0, 39.99), + ('The Pragmatic Programmer', 'Andrew Hunt and David Thomas', 1999, SYSUTCDATETIME(), 0, 42.50), + ('Design Patterns', 'Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides', 1994, SYSUTCDATETIME(), 0, 54.99), + ('Refactoring', 'Martin Fowler', 1999, SYSUTCDATETIME(), 0, 47.25), + ('Domain-Driven Design', 'Eric Evans', 2003, SYSUTCDATETIME(), 0, 59.99); +END +GO \ No newline at end of file From 1f6cce4989539b93c42bc2ea231a5e9edd7d9ebe Mon Sep 17 00:00:00 2001 From: edwardgnt Date: Wed, 5 Aug 2026 14:09:29 -0700 Subject: [PATCH 2/6] Containerize BooksAPI with Docker Compose --- .dockerignore | 7 +++++++ Dockerfile | 24 ++++++++++++++++++++++++ compose.yaml | 14 ++++++++++++++ 3 files changed, 45 insertions(+) create mode 100644 .dockerignore create mode 100644 Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..5eed764 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +**/bin/ +**/obj/ +.vscode/ +.git/ +.gitignore +.env +README.md \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..85919af --- /dev/null +++ b/Dockerfile @@ -0,0 +1,24 @@ +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +WORKDIR /src + +COPY BooksAPIDapper.sln ./ +COPY BooksAPIDapper/BooksAPIDapper.csproj BooksAPIDapper/ +COPY BooksAPIDapper.Tests/BooksAPIDapper.Tests.csproj BooksAPIDapper.Tests/ + +RUN dotnet restore + +COPY . . + +RUN dotnet publish BooksAPIDapper/BooksAPIDapper.csproj \ + -c Release \ + -o /app/publish \ + --no-restore + +FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final +WORKDIR /app + +COPY --from=build /app/publish . + +EXPOSE 8080 + +ENTRYPOINT ["dotnet", "BooksAPIDapper.dll"] \ No newline at end of file diff --git a/compose.yaml b/compose.yaml index 62af7a8..6fef01c 100644 --- a/compose.yaml +++ b/compose.yaml @@ -1,4 +1,18 @@ services: + api: + build: + context: . + dockerfile: Dockerfile + container_name: booksapi-api + depends_on: + - sqlserver + environment: + ASPNETCORE_ENVIRONMENT: Development + ASPNETCORE_URLS: http://+:8080 + ConnectionStrings__DefaultConnection: "Server=sqlserver,1433;Database=BooksDb;User Id=sa;Password=${SA_PASSWORD};Encrypt=False;TrustServerCertificate=True" + ports: + - "8080:8080" + sqlserver: image: mcr.microsoft.com/mssql/server:2022-latest container_name: booksapi-sqlserver From 07b788f820acb412bd6fd27257e9f65112992bd1 Mon Sep 17 00:00:00 2001 From: edwardgnt Date: Wed, 5 Aug 2026 14:30:53 -0700 Subject: [PATCH 3/6] Update README file --- README.md | 205 ++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 183 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index a664845..37d103e 100644 --- a/README.md +++ b/README.md @@ -1,49 +1,210 @@ -# ๐Ÿ“š Books API (Dapper, .NET 9) +# ๐Ÿ“š Books API (Dapper, .NET 10) -![.NET](https://img.shields.io/badge/.NET-9.0-blueviolet) +![.NET](https://img.shields.io/badge/.NET-10.0-blueviolet) ![License](https://img.shields.io/badge/License-MIT-green) ![Dapper](https://img.shields.io/badge/Dapper-ORM-orange) +![Docker](https://img.shields.io/badge/Docker-Compose-blue) ![Status](https://img.shields.io/badge/Status-Stable-brightgreen) ![Build](https://github.com/edwardgnt/BooksAPI/actions/workflows/dotnet-ci.yml/badge.svg) -Production-style REST API with **DTOs**, **Repository Pattern**, and **Filtering + Sorting + Pagination**. -- Data access: **Dapper** (`Microsoft.Data.SqlClient`) -- Error shape: **ProblemDetails** (RFC 7807) -- Extras: **Soft delete**, **Search**, **Date-range guardrails** +Production-style REST API built with **.NET 10**, **Dapper**, **DTOs**, **Repository Pattern**, and **Filtering + Sorting + Pagination**. + +This project also includes **xUnit integration tests**, **GitHub Actions CI**, and **Docker Compose support** for running the API with a containerized SQL Server database. ## ๐Ÿงฐ Tech Stack -- **.NET 9 Web API** โ€” Backend framework -- **Dapper** โ€” Lightweight data access -- **SQL Server** โ€” Database -- **Repository Pattern** โ€” Clean architecture & separation of concerns -- **DTOs** โ€” Safe data transfer between layers -- **Dependency Injection** โ€” For maintainable, testable code -- **OpenAPI** โ€” For API exploration and testing +- **.NET 10 Web API** โ€” Backend framework +- **Dapper** โ€” Lightweight data access +- **SQL Server** โ€” Relational database +- **Docker Compose** โ€” Local API + SQL Server orchestration +- **Repository Pattern** โ€” Clean architecture and separation of concerns +- **DTOs** โ€” Safe data transfer between layers +- **Dependency Injection** โ€” Maintainable and testable services +- **ProblemDetails** โ€” Standards-based API error responses +- **xUnit** โ€” Automated integration testing +- **GitHub Actions** โ€” CI pipeline for restore, build, and test ## ๐Ÿš€ Features -- Thin controllers, repository behind `IBookRepository` + +- Thin controllers with repository abstraction behind `IBookRepository` - DTOs: `BookCreateDto`, `BookUpdateDto`, `BookReadDto`, `BookFilterDto` - Query params: `search`, `minPrice`, `maxPrice`, `sort`, `start`, `end`, `page`, `pageSize` -- Sorting: `price_asc|price_desc|title_asc|title_desc|year_asc|year_desc|created_asc|created_desc` +- Sorting: `price_asc`, `price_desc`, `title_asc`, `title_desc`, `year_asc`, `year_desc`, `created_asc`, `created_desc` - Pagination wrapper: `PagedResult` โ†’ `{ items, totalCount, page, pageSize }` - Soft delete via `IsArchived` +- SQL Server container with persistent Docker volume +- Repeatable database setup script with seed data +- Integration tests using `WebApplicationFactory` ## ๐Ÿงญ Example Endpoints + ```http GET /api/books -GET /api/books?search=king&sort=price_desc +GET /api/books?search=clean&sort=price_desc GET /api/books?minPrice=10&maxPrice=50 -GET /api/books?startDate=2024-01-01&endDate=2024-12-31&page=1&pageSize=10 +GET /api/books?start=2024-01-01&end=2024-12-31&page=1&pageSize=10 POST /api/books PUT /api/books/{id} DELETE /api/books/{id} +``` + +## ๐Ÿ Getting Started + +Clone the repository: + +```bash +git clone https://github.com/edwardgnt/BooksAPI.git +cd BooksAPI +``` + +Restore and build: -๐Ÿ Getting Started -git clone https://github.com//BooksAPIDapper.git -cd BooksAPIDapper +```bash dotnet restore -dotnet run -# App will print e.g. https://localhost:7205 +dotnet build +``` + +Run tests: + +```bash +dotnet test +``` + +## ๐Ÿณ Running with Docker Compose + +This project includes a Docker Compose setup for running the API with SQL Server. + +### Services + +- `api` โ€” .NET 10 Web API container +- `sqlserver` โ€” SQL Server 2022 Developer container +- `booksapi_sql_data` โ€” Persistent Docker volume for SQL Server data + +### 1. Create a `.env` file + +Create a `.env` file in the solution root: + +```env +SA_PASSWORD=YourStrongPassword123! +``` + +> `.env` is ignored by Git and should not be committed. + +### 2. Start SQL Server + +```bash +docker compose up -d sqlserver +``` + +Wait for SQL Server to finish starting. You can check the logs with: + +```bash +docker logs booksapi-sqlserver +``` + +Look for a message indicating SQL Server is ready for client connections. + +### 3. Load the `.env` value into your shell + +```bash +set -a +source .env +set +a +``` + +### 4. Initialize the database + +Run the database setup script: + +```bash +docker exec -i booksapi-sqlserver /opt/mssql-tools18/bin/sqlcmd \ + -S localhost \ + -U sa \ + -P "$SA_PASSWORD" \ + -C \ + -i /dev/stdin < sql/init/01-create-books-db.sql +``` + +This creates: + +- `BooksDb` +- `dbo.Books` +- Seed book records + +### 5. Start the API + +```bash +docker compose up --build +``` + +The API will be available at: + +```text +http://localhost:8080 +``` + +Example request: + +```bash +curl http://localhost:8080/api/books +``` + +## ๐Ÿ—„๏ธ Connecting with SQL Server Management Studio + +To view the Docker SQL Server database from SSMS: + +```text +Server: localhost,14333 +Authentication: SQL Server Authentication +Login: sa +Password: your .env password +Database: BooksDb +``` + +Then run: + +```sql +SELECT Id, Title, Author, YearPublished, CreatedAt, IsArchived, Price +FROM dbo.Books; +``` + +## ๐Ÿงช Testing + +Run the test suite: + +```bash +dotnet test +``` + +The project includes integration tests that validate API behavior through the ASP.NET Core test host. + +## ๐Ÿ—๏ธ Architecture + +The project follows a clean layered structure: + +```text +Controllers + โ†“ +DTOs + โ†“ +IBookRepository + โ†“ +BookRepository + โ†“ +Dapper + โ†“ +SQL Server +``` + +This keeps API contracts, business flow, and data access responsibilities separated and easier to maintain. + +## ๐Ÿ“Œ Notes + +- The app uses Dapper instead of Entity Framework Core for explicit SQL and lightweight data access. +- SQL Server runs in Docker for a repeatable local development environment. +- The API container connects to SQL Server through the Docker Compose service name: `sqlserver`. +- Local tools such as SSMS can connect through the mapped host port: `localhost,14333`. +## ๐Ÿ“„ License +MIT From 461d0d9fcb48ee32d02a28bfb012a2f1e5780f1b Mon Sep 17 00:00:00 2001 From: edwardgnt Date: Wed, 5 Aug 2026 14:55:09 -0700 Subject: [PATCH 4/6] Update CI workflow --- .github/workflows/dotnet-ci.yml | 58 ++++++++++++++++++++++++++------- 1 file changed, 46 insertions(+), 12 deletions(-) diff --git a/.github/workflows/dotnet-ci.yml b/.github/workflows/dotnet-ci.yml index eee0c8a..bc82fb6 100644 --- a/.github/workflows/dotnet-ci.yml +++ b/.github/workflows/dotnet-ci.yml @@ -6,28 +6,62 @@ on: pull_request: branches: [main] +env: + DOTNET_VERSION: "10.0.x" + SA_PASSWORD: ${{ secrets.BOOKSAPI_SA_PASSWORD }} + ConnectionStrings__DefaultConnection: "Server=localhost,14333;Database=BooksDb;User Id=sa;Password=${{ secrets.BOOKSAPI_SA_PASSWORD }};Encrypt=False;TrustServerCertificate=True" + jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - name: Checkout repository + uses: actions/checkout@v4 - name: Setup .NET uses: actions/setup-dotnet@v4 with: - dotnet-version: "9.0.x" + dotnet-version: ${{ env.DOTNET_VERSION }} + + - name: Start SQL Server container + run: docker compose up -d sqlserver + + - name: Wait for SQL Server + run: | + for i in {1..30}; do + if docker exec booksapi-sqlserver /opt/mssql-tools18/bin/sqlcmd \ + -S localhost \ + -U sa \ + -P "$SA_PASSWORD" \ + -C \ + -Q "SELECT 1" > /dev/null 2>&1; then + echo "SQL Server is ready." + exit 0 + fi + + echo "Waiting for SQL Server..." + sleep 5 + done + + echo "SQL Server did not become ready in time." + docker logs booksapi-sqlserver + exit 1 + + - name: Initialize database + run: | + docker exec -i booksapi-sqlserver /opt/mssql-tools18/bin/sqlcmd \ + -S localhost \ + -U sa \ + -P "$SA_PASSWORD" \ + -C \ + -i /dev/stdin < sql/init/01-create-books-db.sql - name: Restore - run: dotnet restore - working-directory: ./BooksAPIDapper + run: dotnet restore BooksAPIDapper.sln - name: Build - run: dotnet build --configuration Release --no-restore - working-directory: ./BooksAPIDapper - - # CI TODO - # Remove this step if you don't have tests yet - # - name: Test - # run: dotnet test --no-build --verbosity normal - # working-directory: ./BooksAPIDapper + run: dotnet build BooksAPIDapper.sln --configuration Release --no-restore + + - name: Test + run: dotnet test BooksAPIDapper.sln --configuration Release --no-build --verbosity normal From f2f6db7722e335e9a3b76e281f81b08ae60e781b Mon Sep 17 00:00:00 2001 From: edwardgnt Date: Wed, 5 Aug 2026 15:12:56 -0700 Subject: [PATCH 5/6] Use placeholder value in env example --- .env.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env.example b/.env.example index ad057e5..1b16168 100644 --- a/.env.example +++ b/.env.example @@ -1 +1 @@ -SA_PASSWORD=YourStrongPassword123! \ No newline at end of file +SA_PASSWORD=your-strong-password-here \ No newline at end of file From b9ba383c74147c39fe1fd83c236e19b5f08202a6 Mon Sep 17 00:00:00 2001 From: edwardgnt Date: Wed, 5 Aug 2026 15:57:21 -0700 Subject: [PATCH 6/6] Clean up local configuration for Docker SQL Server --- BooksAPIDapper/appsettings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/BooksAPIDapper/appsettings.json b/BooksAPIDapper/appsettings.json index 5057346..c093f65 100644 --- a/BooksAPIDapper/appsettings.json +++ b/BooksAPIDapper/appsettings.json @@ -7,6 +7,6 @@ }, "AllowedHosts": "*", "ConnectionStrings": { - "DefaultConnection": "Data Source=localhost\\SQLEXPRESS;Initial Catalog=BooksDb3;Integrated Security=True;Pooling=False;Encrypt=False;Trust Server Certificate=True" + "DefaultConnection": "" } -} +} \ No newline at end of file