From b1cd941638361706206d0f9712dd7bffee50e82b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Fern=C3=A1ndez?= Date: Tue, 26 Nov 2024 14:03:54 +0000 Subject: [PATCH] feat: Add SQLServer database --- .config/dotnet-tools.json | 13 +++++ .devcontainer/devcontainer.json | 14 +++-- .devcontainer/docker-compose.yml | 15 ++++++ .github/workflows/ci.yaml | 2 +- README.md | 7 ++- .../Controllers/TodoListsControllerTests.cs | 29 +++-------- TodoApi.Tests/TodoApi.Tests.csproj | 2 +- TodoApi/Controllers/TodoListsController.cs | 52 +++++-------------- TodoApi/Dtos/CreateTodoList.cs | 6 +++ TodoApi/Dtos/UpdateTodoList.cs | 6 +++ ...20241126134649_CreateTodoLists.Designer.cs | 45 ++++++++++++++++ .../20241126134649_CreateTodoLists.cs | 34 ++++++++++++ .../Migrations/TodoContextModelSnapshot.cs | 42 +++++++++++++++ TodoApi/Models/TodoList.cs | 2 +- TodoApi/Program.cs | 4 +- TodoApi/TodoApi.csproj | 8 +-- TodoApi/appsettings.Development.json | 3 ++ TodoApi/appsettings.json | 5 +- 18 files changed, 207 insertions(+), 82 deletions(-) create mode 100644 .config/dotnet-tools.json create mode 100644 .devcontainer/docker-compose.yml create mode 100644 TodoApi/Dtos/CreateTodoList.cs create mode 100644 TodoApi/Dtos/UpdateTodoList.cs create mode 100644 TodoApi/Migrations/20241126134649_CreateTodoLists.Designer.cs create mode 100644 TodoApi/Migrations/20241126134649_CreateTodoLists.cs create mode 100644 TodoApi/Migrations/TodoContextModelSnapshot.cs diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 0000000..4f48799 --- /dev/null +++ b/.config/dotnet-tools.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "dotnet-ef": { + "version": "9.0.0", + "commands": [ + "dotnet-ef" + ], + "rollForward": false + } + } +} \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index e798d3e..5c8ac64 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,11 +1,17 @@ { "name": "crunchloop-dotnet-interview", - // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile - "image": "mcr.microsoft.com/devcontainers/dotnet:0-7.0", + // Update the 'dockerComposeFile' list if you have more compose files or use different names. + // The .devcontainer/docker-compose.yml file contains any overrides you need/want to make. + "dockerComposeFile": ["docker-compose.yml"], - // Set the workspace folder - "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", + // The 'service' property is the name of the service for the container that VS Code should + // use. Update this value and .devcontainer/docker-compose.yml to the real service name. + "service": "app", + + // The optional 'workspaceFolder' property is the path VS Code should open by default when + // connected. This is typically a file mount in .devcontainer/docker-compose.yml + "workspaceFolder": "/app", // Features to add to the dev container. More info: https://containers.dev/features. "features": {}, diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml new file mode 100644 index 0000000..f0e5add --- /dev/null +++ b/.devcontainer/docker-compose.yml @@ -0,0 +1,15 @@ +version: '3.8' + +services: + app: + image: mcr.microsoft.com/devcontainers/dotnet:8.0 + command: sleep infinity + depends_on: + - sqlserver + volumes: + - ..:/app + sqlserver: + image: mcr.microsoft.com/mssql/server:2022-latest + environment: + - ACCEPT_EULA=Y + - MSSQL_SA_PASSWORD=Password123 diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 14d1a5c..b4995df 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -10,7 +10,7 @@ jobs: - name: Setup .NET Core SDK uses: actions/setup-dotnet@v3 with: - dotnet-version: "7.0.100" + dotnet-version: "8.0" - name: Install dependencies run: dotnet restore - name: Build diff --git a/README.md b/README.md index 48a87af..a91ed37 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,12 @@ [![Open in Coder](https://dev.crunchloop.io/open-in-coder.svg)](https://dev.crunchloop.io/templates/fly-containers/workspace?param.Git%20Repository=git@github.com:crunchloop/dotnet-interview.git) -This is a simple Todo List API built in .NET 7. This project is currently being used for .NET full-stack candidates. +This is a simple Todo List API built in .NET 8. This project is currently being used for .NET full-stack candidates. + +## Database + +The project comes with a devcontainer that provisions a SQL Server database. If you are not going to use the devcontainer, make sure to provision a SQL Server database and +update the connection string. ## Build diff --git a/TodoApi.Tests/Controllers/TodoListsControllerTests.cs b/TodoApi.Tests/Controllers/TodoListsControllerTests.cs index 169ea94..f409f55 100644 --- a/TodoApi.Tests/Controllers/TodoListsControllerTests.cs +++ b/TodoApi.Tests/Controllers/TodoListsControllerTests.cs @@ -17,8 +17,8 @@ private DbContextOptions DatabaseContextOptions() private void PopulateDatabaseContext(TodoContext context) { - context.TodoList.Add(new Models.TodoList { Id = 1, Name = "Task 1" }); - context.TodoList.Add(new Models.TodoList { Id = 2, Name = "Task 2" }); + context.TodoList.Add(new TodoList { Id = 1, Name = "Task 1" }); + context.TodoList.Add(new TodoList { Id = 2, Name = "Task 2" }); context.SaveChanges(); } @@ -60,22 +60,6 @@ public async Task GetTodoList_WhenCalled_ReturnsTodoListById() } } - [Fact] - public async Task PutTodoList_WhenTodoListIdDoesntMatch_ReturnsBadRequest() - { - using (var context = new TodoContext(DatabaseContextOptions())) - { - PopulateDatabaseContext(context); - - var controller = new TodoListsController(context); - - var todoList = await context.TodoList.Where(x => x.Id == 2).FirstAsync(); - var result = await controller.PutTodoList(1, todoList); - - Assert.IsType(result); - } - } - [Fact] public async Task PutTodoList_WhenTodoListDoesntExist_ReturnsBadRequest() { @@ -85,7 +69,7 @@ public async Task PutTodoList_WhenTodoListDoesntExist_ReturnsBadRequest() var controller = new TodoListsController(context); - var result = await controller.PutTodoList(3, new TodoList { Id = 3}); + var result = await controller.PutTodoList(3, new Dtos.UpdateTodoList { Name = "Task 3" }); Assert.IsType(result); } @@ -101,9 +85,9 @@ public async Task PutTodoList_WhenCalled_UpdatesTheTodoList() var controller = new TodoListsController(context); var todoList = await context.TodoList.Where(x => x.Id == 2).FirstAsync(); - var result = await controller.PutTodoList(todoList.Id, todoList); + var result = await controller.PutTodoList(todoList.Id, new Dtos.UpdateTodoList { Name = "Changed Task 2" }); - Assert.IsType(result); + Assert.IsType(result); } } @@ -116,8 +100,7 @@ public async Task PostTodoList_WhenCalled_CreatesTodoList() var controller = new TodoListsController(context); - var todoList = new TodoList { Name = "Task 3" }; - var result = await controller.PostTodoList(todoList); + var result = await controller.PostTodoList(new Dtos.CreateTodoList { Name = "Task 3" }); Assert.IsType(result.Result); Assert.Equal( diff --git a/TodoApi.Tests/TodoApi.Tests.csproj b/TodoApi.Tests/TodoApi.Tests.csproj index 3cf00d1..1bb1bfa 100644 --- a/TodoApi.Tests/TodoApi.Tests.csproj +++ b/TodoApi.Tests/TodoApi.Tests.csproj @@ -1,7 +1,7 @@ - net7.0 + net8.0 enable enable diff --git a/TodoApi/Controllers/TodoListsController.cs b/TodoApi/Controllers/TodoListsController.cs index 744574c..5803251 100644 --- a/TodoApi/Controllers/TodoListsController.cs +++ b/TodoApi/Controllers/TodoListsController.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; +using TodoApi.Dtos; using TodoApi.Models; namespace TodoApi.Controllers @@ -19,11 +20,6 @@ public TodoListsController(TodoContext context) [HttpGet] public async Task>> GetTodoLists() { - if (_context.TodoList == null) - { - return NotFound(); - } - return Ok(await _context.TodoList.ToListAsync()); } @@ -31,11 +27,6 @@ public async Task>> GetTodoLists() [HttpGet("{id}")] public async Task> GetTodoList(long id) { - if (_context.TodoList == null) - { - return NotFound(); - } - var todoList = await _context.TodoList.FindAsync(id); if (todoList == null) @@ -49,43 +40,28 @@ public async Task> GetTodoList(long id) // PUT: api/todolists/5 // To protect from over-posting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754 [HttpPut("{id}")] - public async Task PutTodoList(long id, TodoList todoList) + public async Task PutTodoList(long id, UpdateTodoList payload) { - if (id != todoList.Id) - { - return BadRequest(); - } - - _context.Entry(todoList).State = EntityState.Modified; + var todoList = await _context.TodoList.FindAsync(id); - try - { - await _context.SaveChangesAsync(); - } - catch (DbUpdateConcurrencyException) + if (todoList == null) { - if (!TodoListExists(id)) - { - return NotFound(); - } - else - { - throw; - } + return NotFound(); } - return NoContent(); + todoList.Name = payload.Name; + await _context.SaveChangesAsync(); + + return Ok(todoList); } // POST: api/todolists // To protect from over-posting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754 [HttpPost] - public async Task> PostTodoList(TodoList todoList) + public async Task> PostTodoList(CreateTodoList payload) { - if (_context.TodoList == null) - { - return Problem("Entity set 'TodoContext.TodoList' is null."); - } + var todoList = new TodoList { Name = payload.Name }; + _context.TodoList.Add(todoList); await _context.SaveChangesAsync(); @@ -96,10 +72,6 @@ public async Task> PostTodoList(TodoList todoList) [HttpDelete("{id}")] public async Task DeleteTodoList(long id) { - if (_context.TodoList == null) - { - return NotFound(); - } var todoList = await _context.TodoList.FindAsync(id); if (todoList == null) { diff --git a/TodoApi/Dtos/CreateTodoList.cs b/TodoApi/Dtos/CreateTodoList.cs new file mode 100644 index 0000000..68ff8c0 --- /dev/null +++ b/TodoApi/Dtos/CreateTodoList.cs @@ -0,0 +1,6 @@ +namespace TodoApi.Dtos; + +public class CreateTodoList +{ + public required string Name { get; set; } +} diff --git a/TodoApi/Dtos/UpdateTodoList.cs b/TodoApi/Dtos/UpdateTodoList.cs new file mode 100644 index 0000000..10dabce --- /dev/null +++ b/TodoApi/Dtos/UpdateTodoList.cs @@ -0,0 +1,6 @@ +namespace TodoApi.Dtos; + +public class UpdateTodoList +{ + public required string Name { get; set; } +} diff --git a/TodoApi/Migrations/20241126134649_CreateTodoLists.Designer.cs b/TodoApi/Migrations/20241126134649_CreateTodoLists.Designer.cs new file mode 100644 index 0000000..cbf41a7 --- /dev/null +++ b/TodoApi/Migrations/20241126134649_CreateTodoLists.Designer.cs @@ -0,0 +1,45 @@ +// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace TodoApi.Migrations +{ + [DbContext(typeof(TodoContext))] + [Migration("20241126134649_CreateTodoLists")] + partial class CreateTodoLists + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("TodoApi.Models.TodoList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("TodoList"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/TodoApi/Migrations/20241126134649_CreateTodoLists.cs b/TodoApi/Migrations/20241126134649_CreateTodoLists.cs new file mode 100644 index 0000000..1e6a347 --- /dev/null +++ b/TodoApi/Migrations/20241126134649_CreateTodoLists.cs @@ -0,0 +1,34 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TodoApi.Migrations +{ + /// + public partial class CreateTodoLists : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "TodoList", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Name = table.Column(type: "nvarchar(max)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_TodoList", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "TodoList"); + } + } +} diff --git a/TodoApi/Migrations/TodoContextModelSnapshot.cs b/TodoApi/Migrations/TodoContextModelSnapshot.cs new file mode 100644 index 0000000..2fe8661 --- /dev/null +++ b/TodoApi/Migrations/TodoContextModelSnapshot.cs @@ -0,0 +1,42 @@ +// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace TodoApi.Migrations +{ + [DbContext(typeof(TodoContext))] + partial class TodoContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("TodoApi.Models.TodoList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("TodoList"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/TodoApi/Models/TodoList.cs b/TodoApi/Models/TodoList.cs index dca7012..f0b99e9 100644 --- a/TodoApi/Models/TodoList.cs +++ b/TodoApi/Models/TodoList.cs @@ -3,5 +3,5 @@ namespace TodoApi.Models; public class TodoList { public long Id { get; set; } - public string? Name { get; set; } + public required string Name { get; set; } } diff --git a/TodoApi/Program.cs b/TodoApi/Program.cs index 93dc210..c384259 100644 --- a/TodoApi/Program.cs +++ b/TodoApi/Program.cs @@ -3,9 +3,7 @@ var builder = WebApplication.CreateBuilder(args); builder.Services .AddDbContext( - // Use SQL Server - // opt.UseSqlServer(builder.Configuration.GetConnectionString("TodoContext")); - opt => opt.UseInMemoryDatabase("TodoList") + opt => opt.UseSqlServer(builder.Configuration.GetConnectionString("TodoContext")) ) .AddEndpointsApiExplorer() .AddControllers(); diff --git a/TodoApi/TodoApi.csproj b/TodoApi/TodoApi.csproj index d67da55..82f3df7 100644 --- a/TodoApi/TodoApi.csproj +++ b/TodoApi/TodoApi.csproj @@ -1,15 +1,15 @@ - net7.0 + net8.0 enable enable - - runtime; build; native; contentfiles; analyzers; buildtransitive - all + + runtime; build; native; contentfiles; analyzers; buildtransitive + all diff --git a/TodoApi/appsettings.Development.json b/TodoApi/appsettings.Development.json index f042c67..f99890a 100644 --- a/TodoApi/appsettings.Development.json +++ b/TodoApi/appsettings.Development.json @@ -5,5 +5,8 @@ "Default": "Information", "Microsoft.AspNetCore": "Warning" } + }, + "ConnectionStrings": { + "TodoContext": "Server=sqlserver;Database=Todos;User Id=sa;Password=Password123;TrustServerCertificate=True;" } } diff --git a/TodoApi/appsettings.json b/TodoApi/appsettings.json index 1a86733..ec04bc1 100644 --- a/TodoApi/appsettings.json +++ b/TodoApi/appsettings.json @@ -5,8 +5,5 @@ "Microsoft.AspNetCore": "Warning" } }, - "AllowedHosts": "*", - "ConnectionStrings": { - "TodoContext": "Server=(localdb)\\mssqllocaldb;Database=;Trusted_Connection=True;MultipleActiveResultSets=true" - } + "AllowedHosts": "*" } \ No newline at end of file