From ce67945015c9acd0a65ed3f4ec0162a5b5dd0b85 Mon Sep 17 00:00:00 2001 From: Anastasia Lysionok Date: Mon, 22 May 2023 13:38:54 +0200 Subject: [PATCH 1/4] Swagger was added. --- Startup.cs | 15 +++++++++++++++ TodoApiDTO.csproj | 1 + 2 files changed, 16 insertions(+) diff --git a/Startup.cs b/Startup.cs index bbfbc83d..60b58fbd 100644 --- a/Startup.cs +++ b/Startup.cs @@ -11,6 +11,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using Microsoft.OpenApi.Models; using TodoApi.Models; namespace TodoApi @@ -27,6 +28,13 @@ public Startup(IConfiguration configuration) // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { + services.AddMvc(); + + services.AddSwaggerGen(c => + { + c.SwaggerDoc("v1", new OpenApiInfo { Title = "Todo API", Version = "v1" }); + }); + services.AddDbContext(opt => opt.UseInMemoryDatabase("TodoList")); services.AddControllers(); @@ -50,6 +58,13 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { endpoints.MapControllers(); }); + + app.UseSwagger(); + + app.UseSwaggerUI(c => + { + c.SwaggerEndpoint("v1/swagger.json", "Todo API V1"); + }); } } } diff --git a/TodoApiDTO.csproj b/TodoApiDTO.csproj index bba6f6af..6c186056 100644 --- a/TodoApiDTO.csproj +++ b/TodoApiDTO.csproj @@ -12,6 +12,7 @@ + From 65be77a2d3eaa095da22ffe060af7fdcacf5d1ab Mon Sep 17 00:00:00 2001 From: Anastasia Lysionok Date: Mon, 22 May 2023 16:18:12 +0200 Subject: [PATCH 2/4] Added MS SQL Server connection and migrations. --- ...0230522140042_InitialMigration.Designer.cs | 46 +++++++++++++++++++ Migrations/20230522140042_InitialMigration.cs | 31 +++++++++++++ Migrations/TodoContextModelSnapshot.cs | 44 ++++++++++++++++++ Startup.cs | 3 +- appsettings.json | 5 +- 5 files changed, 127 insertions(+), 2 deletions(-) create mode 100644 Migrations/20230522140042_InitialMigration.Designer.cs create mode 100644 Migrations/20230522140042_InitialMigration.cs create mode 100644 Migrations/TodoContextModelSnapshot.cs diff --git a/Migrations/20230522140042_InitialMigration.Designer.cs b/Migrations/20230522140042_InitialMigration.Designer.cs new file mode 100644 index 00000000..02cbceeb --- /dev/null +++ b/Migrations/20230522140042_InitialMigration.Designer.cs @@ -0,0 +1,46 @@ +// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using TodoApi.Models; + +namespace TodoApiDTO.Migrations +{ + [DbContext(typeof(TodoContext))] + [Migration("20230522140042_InitialMigration")] + partial class InitialMigration + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.0") + .HasAnnotation("Relational:MaxIdentifierLength", 128) + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("TodoApi.Models.TodoItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("IsComplete") + .HasColumnType("bit"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Secret") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("TodoItems"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Migrations/20230522140042_InitialMigration.cs b/Migrations/20230522140042_InitialMigration.cs new file mode 100644 index 00000000..f908b4f1 --- /dev/null +++ b/Migrations/20230522140042_InitialMigration.cs @@ -0,0 +1,31 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace TodoApiDTO.Migrations +{ + public partial class InitialMigration : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "TodoItems", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Name = table.Column(nullable: true), + IsComplete = table.Column(nullable: false), + Secret = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_TodoItems", x => x.Id); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "TodoItems"); + } + } +} diff --git a/Migrations/TodoContextModelSnapshot.cs b/Migrations/TodoContextModelSnapshot.cs new file mode 100644 index 00000000..9d5b3c3e --- /dev/null +++ b/Migrations/TodoContextModelSnapshot.cs @@ -0,0 +1,44 @@ +// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using TodoApi.Models; + +namespace TodoApiDTO.Migrations +{ + [DbContext(typeof(TodoContext))] + partial class TodoContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.0") + .HasAnnotation("Relational:MaxIdentifierLength", 128) + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("TodoApi.Models.TodoItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("IsComplete") + .HasColumnType("bit"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Secret") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("TodoItems"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Startup.cs b/Startup.cs index 60b58fbd..bfb06511 100644 --- a/Startup.cs +++ b/Startup.cs @@ -36,7 +36,8 @@ public void ConfigureServices(IServiceCollection services) }); services.AddDbContext(opt => - opt.UseInMemoryDatabase("TodoList")); + opt.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))); + services.AddControllers(); } diff --git a/appsettings.json b/appsettings.json index d9d9a9bf..57fd6d6a 100644 --- a/appsettings.json +++ b/appsettings.json @@ -6,5 +6,8 @@ "Microsoft.Hosting.Lifetime": "Information" } }, - "AllowedHosts": "*" + "AllowedHosts": "*", + "ConnectionStrings": { + "DefaultConnection": "Server=localhost\\SQLEXPRESS;Database=TodoList;Trusted_Connection=True;" + } } From a814e51e15b6084263135ebe6e262737ed1d9564 Mon Sep 17 00:00:00 2001 From: Anastasia Lysionok Date: Mon, 22 May 2023 18:43:41 +0200 Subject: [PATCH 3/4] Added Serilog for logging to file. --- Program.cs | 5 +++++ TodoApiDTO.csproj | 3 +++ appsettings.json | 19 +++++++++++++++++++ 3 files changed, 27 insertions(+) diff --git a/Program.cs b/Program.cs index b27ac16a..ece7b238 100644 --- a/Program.cs +++ b/Program.cs @@ -6,6 +6,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using Serilog; namespace TodoApi { @@ -21,6 +22,10 @@ public static IHostBuilder CreateHostBuilder(string[] args) => .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); + }) + .UseSerilog((hostingContext, loggerConfiguration) => + { + loggerConfiguration.ReadFrom.Configuration(hostingContext.Configuration); }); } } diff --git a/TodoApiDTO.csproj b/TodoApiDTO.csproj index 6c186056..34d2af69 100644 --- a/TodoApiDTO.csproj +++ b/TodoApiDTO.csproj @@ -12,6 +12,9 @@ + + + diff --git a/appsettings.json b/appsettings.json index 57fd6d6a..ea03ff9b 100644 --- a/appsettings.json +++ b/appsettings.json @@ -9,5 +9,24 @@ "AllowedHosts": "*", "ConnectionStrings": { "DefaultConnection": "Server=localhost\\SQLEXPRESS;Database=TodoList;Trusted_Connection=True;" + }, + "Serilog": { + "MinimumLevel": { + "Default": "Error", + "Overrides": { + "Microsoft": "Error", + "Microsoft.Hosting.Lifetime": "Error" + } + }, + "Using": ["Serilog.Sinks.File"], + "WriteTo": [ + { + "Name": "File", + "Args": { + "path": "./bin/log.txt", + "rollingInterval": "Day" + } + } + ] } } From 1ac129926db640e1a0f0deaf3c1e4a91234ce129 Mon Sep 17 00:00:00 2001 From: Anastasia Lysionok Date: Mon, 22 May 2023 20:43:27 +0200 Subject: [PATCH 4/4] Refactoring, dividing into Data Access Layer, Business Layer. --- {Models => BLL/DTO}/TodoItemDTO.cs | 4 +- BLL/Interfaces/ITodoItemService.cs | 15 +++ BLL/Services/TodoItemService.cs | 111 ++++++++++++++++++ Controllers/TodoItemsController.cs | 83 ++++--------- {Models => DAL/Models}/TodoContext.cs | 2 +- {Models => DAL/Models}/TodoItem.cs | 2 +- ...0230522140042_InitialMigration.Designer.cs | 2 +- Migrations/TodoContextModelSnapshot.cs | 2 +- Program.cs | 8 +- Startup.cs | 15 +-- TodoApiDTO.csproj => TodoApi.csproj | 5 + TodoApiDTO.sln => TodoApi.sln | 2 +- 12 files changed, 167 insertions(+), 84 deletions(-) rename {Models => BLL/DTO}/TodoItemDTO.cs (74%) create mode 100644 BLL/Interfaces/ITodoItemService.cs create mode 100644 BLL/Services/TodoItemService.cs rename {Models => DAL/Models}/TodoContext.cs (89%) rename {Models => DAL/Models}/TodoItem.cs (87%) rename TodoApiDTO.csproj => TodoApi.csproj (90%) rename TodoApiDTO.sln => TodoApi.sln (88%) diff --git a/Models/TodoItemDTO.cs b/BLL/DTO/TodoItemDTO.cs similarity index 74% rename from Models/TodoItemDTO.cs rename to BLL/DTO/TodoItemDTO.cs index e66a500a..6276620e 100644 --- a/Models/TodoItemDTO.cs +++ b/BLL/DTO/TodoItemDTO.cs @@ -1,7 +1,7 @@ -namespace TodoApi.Models +namespace TodoApiDTO.BLL.DTO { #region snippet - public class TodoItemDTO + public class TodoItemDto { public long Id { get; set; } public string Name { get; set; } diff --git a/BLL/Interfaces/ITodoItemService.cs b/BLL/Interfaces/ITodoItemService.cs new file mode 100644 index 00000000..2819afb7 --- /dev/null +++ b/BLL/Interfaces/ITodoItemService.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using TodoApiDTO.BLL.DTO; + +namespace TodoApiDTO.BLL.Interfaces +{ + public interface ITodoItemService + { + Task> GetTodoItems(); + Task GetTodoItem(long id); + Task UpdateTodoItem(long id, TodoItemDto todoItemDto); + Task CreateTodoItem(TodoItemDto todoItemDto); + Task DeleteTodoItem(long id); + } +} \ No newline at end of file diff --git a/BLL/Services/TodoItemService.cs b/BLL/Services/TodoItemService.cs new file mode 100644 index 00000000..fe414ad6 --- /dev/null +++ b/BLL/Services/TodoItemService.cs @@ -0,0 +1,111 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using TodoApiDTO.BLL.DTO; +using TodoApiDTO.BLL.Interfaces; +using TodoApiDTO.DAL.Models; + +namespace TodoApiDTO.BLL.Services +{ + public class TodoItemService : ITodoItemService + { + private readonly TodoContext _context; + + public TodoItemService(TodoContext context) + { + _context = context; + } + + public async Task> GetTodoItems() + { + return await _context.TodoItems + .Select(x => ItemToDto(x)) + .ToListAsync(); + } + + public async Task GetTodoItem(long id) + { + var todoItem = await _context.TodoItems.FindAsync(id); + + if (todoItem == null) + { + return null; + } + + return ItemToDto(todoItem); + } + + public async Task UpdateTodoItem(long id, TodoItemDto todoItemDto) + { + if (id != todoItemDto.Id) + { + return false; + } + + var todoItem = await _context.TodoItems.FindAsync(id); + if (todoItem == null) + { + return false; + } + + todoItem.Name = todoItemDto.Name; + todoItem.IsComplete = todoItemDto.IsComplete; + + try + { + await _context.SaveChangesAsync(); + } + catch (DbUpdateConcurrencyException) + { + if (!TodoItemExists(id)) + { + return false; + } + throw; + } + + return true; + } + + public async Task CreateTodoItem(TodoItemDto todoItemDto) + { + var todoItem = new TodoItem + { + IsComplete = todoItemDto.IsComplete, + Name = todoItemDto.Name + }; + + _context.TodoItems.Add(todoItem); + await _context.SaveChangesAsync(); + + return ItemToDto(todoItem); + } + + public async Task DeleteTodoItem(long id) + { + var todoItem = await _context.TodoItems.FindAsync(id); + + if (todoItem == null) + { + return false; + } + + _context.TodoItems.Remove(todoItem); + await _context.SaveChangesAsync(); + + return true; + } + + private bool TodoItemExists(long id) => + _context.TodoItems.Any(e => e.Id == id); + + private static TodoItemDto ItemToDto(TodoItem todoItem) => + new TodoItemDto + { + Id = todoItem.Id, + Name = todoItem.Name, + IsComplete = todoItem.IsComplete + }; + } +} \ No newline at end of file diff --git a/Controllers/TodoItemsController.cs b/Controllers/TodoItemsController.cs index 0ef138e7..044e20f4 100644 --- a/Controllers/TodoItemsController.cs +++ b/Controllers/TodoItemsController.cs @@ -1,66 +1,48 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; -using TodoApi.Models; +using Microsoft.AspNetCore.Mvc; +using TodoApiDTO.BLL.DTO; +using TodoApiDTO.BLL.Interfaces; -namespace TodoApi.Controllers +namespace TodoApiDTO.Controllers { [Route("api/[controller]")] [ApiController] public class TodoItemsController : ControllerBase { - private readonly TodoContext _context; + private readonly ITodoItemService _todoItemService; - public TodoItemsController(TodoContext context) + public TodoItemsController(ITodoItemService todoItemService) { - _context = context; + _todoItemService = todoItemService; } [HttpGet] - public async Task>> GetTodoItems() + public async Task>> GetTodoItems() { - return await _context.TodoItems - .Select(x => ItemToDTO(x)) - .ToListAsync(); + var todoItems = await _todoItemService.GetTodoItems(); + return Ok(todoItems); } [HttpGet("{id}")] - public async Task> GetTodoItem(long id) + public async Task> GetTodoItem(long id) { - var todoItem = await _context.TodoItems.FindAsync(id); + var todoItem = await _todoItemService.GetTodoItem(id); if (todoItem == null) { return NotFound(); } - return ItemToDTO(todoItem); + return Ok(todoItem); } [HttpPut("{id}")] - public async Task UpdateTodoItem(long id, TodoItemDTO todoItemDTO) + public async Task UpdateTodoItem(long id, TodoItemDto todoItemDto) { - if (id != todoItemDTO.Id) - { - return BadRequest(); - } + var result = await _todoItemService.UpdateTodoItem(id, todoItemDto); - var todoItem = await _context.TodoItems.FindAsync(id); - if (todoItem == null) - { - return NotFound(); - } - - todoItem.Name = todoItemDTO.Name; - todoItem.IsComplete = todoItemDTO.IsComplete; - - try - { - await _context.SaveChangesAsync(); - } - catch (DbUpdateConcurrencyException) when (!TodoItemExists(id)) + if (!result) { return NotFound(); } @@ -69,48 +51,27 @@ public async Task UpdateTodoItem(long id, TodoItemDTO todoItemDTO } [HttpPost] - public async Task> CreateTodoItem(TodoItemDTO todoItemDTO) + public async Task> CreateTodoItem(TodoItemDto todoItemDto) { - var todoItem = new TodoItem - { - IsComplete = todoItemDTO.IsComplete, - Name = todoItemDTO.Name - }; - - _context.TodoItems.Add(todoItem); - await _context.SaveChangesAsync(); + var createdTodoItem = await _todoItemService.CreateTodoItem(todoItemDto); return CreatedAtAction( nameof(GetTodoItem), - new { id = todoItem.Id }, - ItemToDTO(todoItem)); + new { id = createdTodoItem.Id }, + createdTodoItem); } [HttpDelete("{id}")] public async Task DeleteTodoItem(long id) { - var todoItem = await _context.TodoItems.FindAsync(id); + var result = await _todoItemService.DeleteTodoItem(id); - if (todoItem == null) + if (!result) { return NotFound(); } - _context.TodoItems.Remove(todoItem); - await _context.SaveChangesAsync(); - return NoContent(); } - - private bool TodoItemExists(long id) => - _context.TodoItems.Any(e => e.Id == id); - - private static TodoItemDTO ItemToDTO(TodoItem todoItem) => - new TodoItemDTO - { - Id = todoItem.Id, - Name = todoItem.Name, - IsComplete = todoItem.IsComplete - }; } } diff --git a/Models/TodoContext.cs b/DAL/Models/TodoContext.cs similarity index 89% rename from Models/TodoContext.cs rename to DAL/Models/TodoContext.cs index 6e59e363..4237351f 100644 --- a/Models/TodoContext.cs +++ b/DAL/Models/TodoContext.cs @@ -1,6 +1,6 @@ using Microsoft.EntityFrameworkCore; -namespace TodoApi.Models +namespace TodoApiDTO.DAL.Models { public class TodoContext : DbContext { diff --git a/Models/TodoItem.cs b/DAL/Models/TodoItem.cs similarity index 87% rename from Models/TodoItem.cs rename to DAL/Models/TodoItem.cs index 1f6e5465..4ecd9745 100644 --- a/Models/TodoItem.cs +++ b/DAL/Models/TodoItem.cs @@ -1,4 +1,4 @@ -namespace TodoApi.Models +namespace TodoApiDTO.DAL.Models { #region snippet public class TodoItem diff --git a/Migrations/20230522140042_InitialMigration.Designer.cs b/Migrations/20230522140042_InitialMigration.Designer.cs index 02cbceeb..c1fc6f03 100644 --- a/Migrations/20230522140042_InitialMigration.Designer.cs +++ b/Migrations/20230522140042_InitialMigration.Designer.cs @@ -4,7 +4,7 @@ using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using TodoApi.Models; +using TodoApiDTO.DAL.Models; namespace TodoApiDTO.Migrations { diff --git a/Migrations/TodoContextModelSnapshot.cs b/Migrations/TodoContextModelSnapshot.cs index 9d5b3c3e..9d815851 100644 --- a/Migrations/TodoContextModelSnapshot.cs +++ b/Migrations/TodoContextModelSnapshot.cs @@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using TodoApi.Models; +using TodoApiDTO.DAL.Models; namespace TodoApiDTO.Migrations { diff --git a/Program.cs b/Program.cs index ece7b238..a8db26be 100644 --- a/Program.cs +++ b/Program.cs @@ -1,14 +1,8 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; using Serilog; -namespace TodoApi +namespace TodoApiDTO { public class Program { diff --git a/Startup.cs b/Startup.cs index bfb06511..a80a73c0 100644 --- a/Startup.cs +++ b/Startup.cs @@ -1,20 +1,15 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.HttpsPolicy; -using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; using Microsoft.OpenApi.Models; -using TodoApi.Models; +using TodoApiDTO.BLL.Interfaces; +using TodoApiDTO.BLL.Services; +using TodoApiDTO.DAL.Models; -namespace TodoApi +namespace TodoApiDTO { public class Startup { @@ -39,6 +34,8 @@ public void ConfigureServices(IServiceCollection services) opt.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))); services.AddControllers(); + + services.AddScoped(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. diff --git a/TodoApiDTO.csproj b/TodoApi.csproj similarity index 90% rename from TodoApiDTO.csproj rename to TodoApi.csproj index 34d2af69..db258e58 100644 --- a/TodoApiDTO.csproj +++ b/TodoApi.csproj @@ -2,6 +2,7 @@ netcoreapp3.1 + TodoApiDTO @@ -18,5 +19,9 @@ + + + + diff --git a/TodoApiDTO.sln b/TodoApi.sln similarity index 88% rename from TodoApiDTO.sln rename to TodoApi.sln index e49c182b..74d488a4 100644 --- a/TodoApiDTO.sln +++ b/TodoApi.sln @@ -3,7 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.30002.166 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TodoApiDTO", "TodoApiDTO.csproj", "{623124F9-F5BA-42DD-BC26-A1720774229C}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TodoApi", "TodoApi.csproj", "{623124F9-F5BA-42DD-BC26-A1720774229C}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution