From 0d545fd58be7587660e8e0ec7eccf934897696d1 Mon Sep 17 00:00:00 2001 From: Roman Sharikov Date: Sat, 11 Mar 2023 17:53:13 +0600 Subject: [PATCH 1/4] =?UTF-8?q?=D0=9F=D0=B5=D1=80=D0=B2=D0=BE=D0=BD=D0=B0?= =?UTF-8?q?=D1=87=D0=B0=D0=BB=D1=8C=D0=BD=D0=B0=D1=8F=20=D1=80=D0=B0=D0=B7?= =?UTF-8?q?=D0=B1=D0=B8=D0=B2=D0=BA=D0=B0=20=D0=BD=D0=B0=20BusinessLayer?= =?UTF-8?q?=20=D0=B8=20DataAccessLayer=20+=20=D0=BF=D1=80=D0=BE=D1=87?= =?UTF-8?q?=D0=B8=D0=B5=20=D0=BD=D0=B5=D0=B1=D0=BE=D0=BB=D1=8C=D1=88=D0=B8?= =?UTF-8?q?=D0=B5=20=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Controllers/TodoItemsController.cs | 79 ++++++++----------------- Data/TodoRepository.cs | 75 +++++++++++++++++++++++ Models/Repositories/ITodoRepository.cs | 16 +++++ Models/Services/ITodoService.cs | 16 +++++ Models/{ => TodoItem}/TodoItem.cs | 0 Models/TodoItem/TodoItemActionResult.cs | 14 +++++ Models/TodoItem/TodoItemDTO.cs | 21 +++++++ Models/TodoItemDTO.cs | 11 ---- Services/TodoService.cs | 46 ++++++++++++++ 9 files changed, 212 insertions(+), 66 deletions(-) create mode 100644 Data/TodoRepository.cs create mode 100644 Models/Repositories/ITodoRepository.cs create mode 100644 Models/Services/ITodoService.cs rename Models/{ => TodoItem}/TodoItem.cs (100%) create mode 100644 Models/TodoItem/TodoItemActionResult.cs create mode 100644 Models/TodoItem/TodoItemDTO.cs delete mode 100644 Models/TodoItemDTO.cs create mode 100644 Services/TodoService.cs diff --git a/Controllers/TodoItemsController.cs b/Controllers/TodoItemsController.cs index 0ef138e7..aa0db2eb 100644 --- a/Controllers/TodoItemsController.cs +++ b/Controllers/TodoItemsController.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Threading.Tasks; using TodoApi.Models; +using TodoApiDTO.Models; namespace TodoApi.Controllers { @@ -11,32 +12,31 @@ namespace TodoApi.Controllers [ApiController] public class TodoItemsController : ControllerBase { - private readonly TodoContext _context; + private readonly ITodoService _todoService; - public TodoItemsController(TodoContext context) + public TodoItemsController(ITodoService todoService) { - _context = context; + _todoService = todoService; } [HttpGet] public async Task>> GetTodoItems() { - return await _context.TodoItems - .Select(x => ItemToDTO(x)) + return await _todoService.GetList() + .Select(x => TodoItemDTO.ItemToDTO(x)) .ToListAsync(); } [HttpGet("{id}")] public async Task> GetTodoItem(long id) { - var todoItem = await _context.TodoItems.FindAsync(id); - + var todoItem = await _todoService.GetById(id); if (todoItem == null) { return NotFound(); } - return ItemToDTO(todoItem); + return TodoItemDTO.ItemToDTO(todoItem); } [HttpPut("{id}")] @@ -47,70 +47,39 @@ public async Task UpdateTodoItem(long id, TodoItemDTO todoItemDTO return BadRequest(); } - var todoItem = await _context.TodoItems.FindAsync(id); - if (todoItem == null) - { - return NotFound(); - } - - todoItem.Name = todoItemDTO.Name; - todoItem.IsComplete = todoItemDTO.IsComplete; + var result = await _todoService.Update(todoItemDTO); - try - { - await _context.SaveChangesAsync(); - } - catch (DbUpdateConcurrencyException) when (!TodoItemExists(id)) - { - return NotFound(); - } - - return NoContent(); + return GetTodoItemActionResult(result); } [HttpPost] public async Task> CreateTodoItem(TodoItemDTO todoItemDTO) { - var todoItem = new TodoItem - { - IsComplete = todoItemDTO.IsComplete, - Name = todoItemDTO.Name - }; - - _context.TodoItems.Add(todoItem); - await _context.SaveChangesAsync(); + var todoItem = await _todoService.Create(todoItemDTO); return CreatedAtAction( nameof(GetTodoItem), new { id = todoItem.Id }, - ItemToDTO(todoItem)); + TodoItemDTO.ItemToDTO(todoItem)); } [HttpDelete("{id}")] public async Task DeleteTodoItem(long id) { - var todoItem = await _context.TodoItems.FindAsync(id); - - if (todoItem == null) - { - return NotFound(); - } + var result = await _todoService.Delete(id); - _context.TodoItems.Remove(todoItem); - await _context.SaveChangesAsync(); - - return NoContent(); + return GetTodoItemActionResult(result); } - - private bool TodoItemExists(long id) => - _context.TodoItems.Any(e => e.Id == id); - - private static TodoItemDTO ItemToDTO(TodoItem todoItem) => - new TodoItemDTO + + private IActionResult GetTodoItemActionResult(TodoItemActionResult result) + { + return result switch { - Id = todoItem.Id, - Name = todoItem.Name, - IsComplete = todoItem.IsComplete - }; + TodoItemActionResult.Success => NoContent(), + TodoItemActionResult.NotFound => NotFound(), + TodoItemActionResult.Failed => BadRequest(), + _ => Accepted(), + }; + } } } diff --git a/Data/TodoRepository.cs b/Data/TodoRepository.cs new file mode 100644 index 00000000..0fc793d6 --- /dev/null +++ b/Data/TodoRepository.cs @@ -0,0 +1,75 @@ +using Microsoft.EntityFrameworkCore; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using TodoApi.Models; +using TodoApiDTO.Models; + +namespace TodoApiDTO.Data +{ + public class TodoRepository : ITodoRepository + { + private readonly TodoContext _context; + + public TodoRepository(TodoContext context) + { + _context = context; + } + + public async Task Create(TodoItemDTO input) + { + var createdTodoItem = await _context.TodoItems.AddAsync(new TodoItem { IsComplete = input.IsComplete, Name = input.Name }); + await _context.SaveChangesAsync(); + + return createdTodoItem?.Entity; + } + + public async Task Delete(long itemToDeleteId) + { + var itemToDelete = await _context.TodoItems.FindAsync(itemToDeleteId); + + if (itemToDelete == null) + { + return TodoItemActionResult.NotFound; + } + + _context.TodoItems.Remove(itemToDelete); + await _context.SaveChangesAsync(); + + return TodoItemActionResult.Success; + } + + public async Task GetById(long itemId) + { + return await _context.TodoItems.FindAsync(itemId); + } + + public DbSet GetList() + { + return _context.TodoItems; + } + + public async Task Update(TodoItemDTO input) + { + var todoItemToUpdate = await _context.TodoItems.FindAsync(input.Id); + if (todoItemToUpdate == null) + { + return TodoItemActionResult.NotFound; + } + + todoItemToUpdate.Name = input.Name; + todoItemToUpdate.IsComplete = input.IsComplete; + + try + { + await _context.SaveChangesAsync(); + } + catch (DbUpdateConcurrencyException) when (!_context.TodoItems.Any(e => e.Id == input.Id)) + { + return TodoItemActionResult.NotFound; + } + + return TodoItemActionResult.Success; + } + } +} diff --git a/Models/Repositories/ITodoRepository.cs b/Models/Repositories/ITodoRepository.cs new file mode 100644 index 00000000..f61ee512 --- /dev/null +++ b/Models/Repositories/ITodoRepository.cs @@ -0,0 +1,16 @@ +using Microsoft.EntityFrameworkCore; +using System.Collections.Generic; +using System.Threading.Tasks; +using TodoApi.Models; + +namespace TodoApiDTO.Models +{ + public interface ITodoRepository + { + Task GetById(long itemId); + Task Create(TodoItemDTO input); + Task Update(TodoItemDTO input); + Task Delete(long itemToDeleteId); + DbSet GetList(); + } +} diff --git a/Models/Services/ITodoService.cs b/Models/Services/ITodoService.cs new file mode 100644 index 00000000..6a2d887c --- /dev/null +++ b/Models/Services/ITodoService.cs @@ -0,0 +1,16 @@ +using Microsoft.EntityFrameworkCore; +using System.Collections.Generic; +using System.Threading.Tasks; +using TodoApiDTO.Models; + +namespace TodoApi.Models +{ + public interface ITodoService + { + Task GetById(long itemId); + Task Create(TodoItemDTO createInput); + Task Update(TodoItemDTO updateInput); + Task Delete(long itemToDeleteId); + DbSet GetList(); + } +} diff --git a/Models/TodoItem.cs b/Models/TodoItem/TodoItem.cs similarity index 100% rename from Models/TodoItem.cs rename to Models/TodoItem/TodoItem.cs diff --git a/Models/TodoItem/TodoItemActionResult.cs b/Models/TodoItem/TodoItemActionResult.cs new file mode 100644 index 00000000..a597b481 --- /dev/null +++ b/Models/TodoItem/TodoItemActionResult.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace TodoApiDTO.Models +{ + public enum TodoItemActionResult: byte + { + Success = 0, + Failed = 1, + NotFound = 2 + } +} diff --git a/Models/TodoItem/TodoItemDTO.cs b/Models/TodoItem/TodoItemDTO.cs new file mode 100644 index 00000000..20bf1709 --- /dev/null +++ b/Models/TodoItem/TodoItemDTO.cs @@ -0,0 +1,21 @@ +namespace TodoApi.Models +{ + #region snippet + public class TodoItemDTO + { + public long Id { get; set; } + public string Name { get; set; } + public bool IsComplete { get; set; } + + private TodoItemDTO() { } + + public static TodoItemDTO ItemToDTO(TodoItem todoItem) => + new TodoItemDTO + { + Id = todoItem.Id, + Name = todoItem.Name, + IsComplete = todoItem.IsComplete + }; + } + #endregion +} diff --git a/Models/TodoItemDTO.cs b/Models/TodoItemDTO.cs deleted file mode 100644 index e66a500a..00000000 --- a/Models/TodoItemDTO.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace TodoApi.Models -{ - #region snippet - public class TodoItemDTO - { - public long Id { get; set; } - public string Name { get; set; } - public bool IsComplete { get; set; } - } - #endregion -} diff --git a/Services/TodoService.cs b/Services/TodoService.cs new file mode 100644 index 00000000..810ee8a0 --- /dev/null +++ b/Services/TodoService.cs @@ -0,0 +1,46 @@ +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using TodoApi.Models; +using TodoApiDTO.Models; + +namespace TodoApiDTO.Services +{ + public class TodoService : ITodoService + { + private readonly ITodoRepository _todoRepository; + + public TodoService(ITodoRepository todoRepository) + { + _todoRepository = todoRepository; + } + + public async Task Create(TodoItemDTO createInput) + { + if (createInput == null) throw new ArgumentNullException(nameof(createInput)); + return await _todoRepository.Create(createInput); + } + + public async Task Delete(long itemToDeleteId) + { + return await _todoRepository.Delete(itemToDeleteId); + } + + public async Task GetById(long itemId) + { + return await _todoRepository.GetById(itemId); + } + + public async Task Update(TodoItemDTO updateInput) + { + if (updateInput == null) throw new ArgumentNullException(nameof(updateInput)); + return await _todoRepository.Update(updateInput); + } + + public DbSet GetList() + { + return _todoRepository.GetList(); + } + } +} From 62002078d349da63ef1346da29dd9a0a3b6d5813 Mon Sep 17 00:00:00 2001 From: Roman Sharikov Date: Mon, 13 Mar 2023 21:24:57 +0600 Subject: [PATCH 2/4] =?UTF-8?q?=D0=92=D0=BD=D0=B5=D0=B4=D1=80=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20SQL=20Server?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Data/TodoRepository.cs | 1 - Models/TodoItem/TodoItemActionResult.cs | 7 +------ Scripts/13-03-2023 sharikov add TodoItems.sql | 8 ++++++++ Services/TodoService.cs | 1 - Startup.cs | 16 ++++++++-------- appsettings.json | 6 +++++- 6 files changed, 22 insertions(+), 17 deletions(-) create mode 100644 Scripts/13-03-2023 sharikov add TodoItems.sql diff --git a/Data/TodoRepository.cs b/Data/TodoRepository.cs index 0fc793d6..79c14a13 100644 --- a/Data/TodoRepository.cs +++ b/Data/TodoRepository.cs @@ -1,5 +1,4 @@ using Microsoft.EntityFrameworkCore; -using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using TodoApi.Models; diff --git a/Models/TodoItem/TodoItemActionResult.cs b/Models/TodoItem/TodoItemActionResult.cs index a597b481..51e152dd 100644 --- a/Models/TodoItem/TodoItemActionResult.cs +++ b/Models/TodoItem/TodoItemActionResult.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace TodoApiDTO.Models +namespace TodoApiDTO.Models { public enum TodoItemActionResult: byte { diff --git a/Scripts/13-03-2023 sharikov add TodoItems.sql b/Scripts/13-03-2023 sharikov add TodoItems.sql new file mode 100644 index 00000000..ff6f6e42 --- /dev/null +++ b/Scripts/13-03-2023 sharikov add TodoItems.sql @@ -0,0 +1,8 @@ +CREATE TABLE TodoItems +( + Id int NOT NULL, + Name nvarchar(255) NOT NULL, + IsComplete bit NOT NULL, + Secret nvarchar(255), + PRIMARY KEY (Id) +); \ No newline at end of file diff --git a/Services/TodoService.cs b/Services/TodoService.cs index 810ee8a0..2d5a6e6e 100644 --- a/Services/TodoService.cs +++ b/Services/TodoService.cs @@ -1,6 +1,5 @@ using Microsoft.EntityFrameworkCore; using System; -using System.Collections.Generic; using System.Threading.Tasks; using TodoApi.Models; using TodoApiDTO.Models; diff --git a/Startup.cs b/Startup.cs index bbfbc83d..bcddb551 100644 --- a/Startup.cs +++ b/Startup.cs @@ -1,17 +1,13 @@ -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 TodoApi.Models; +using TodoApiDTO.Data; +using TodoApiDTO.Models; +using TodoApiDTO.Services; namespace TodoApi { @@ -28,7 +24,11 @@ public Startup(IConfiguration configuration) public void ConfigureServices(IServiceCollection services) { services.AddDbContext(opt => - opt.UseInMemoryDatabase("TodoList")); + opt.UseSqlServer(Configuration.GetConnectionString("MsSqlAuth"))); + + services.AddScoped(); + services.AddScoped(); + services.AddControllers(); } diff --git a/appsettings.json b/appsettings.json index d9d9a9bf..27dab2d7 100644 --- a/appsettings.json +++ b/appsettings.json @@ -6,5 +6,9 @@ "Microsoft.Hosting.Lifetime": "Information" } }, - "AllowedHosts": "*" + "AllowedHosts": "*", + "ConnectionStrings": { + "WinAuth": "Server=localhost\\SQLEXPRESS;Database=master;Trusted_Connection=True;MultipleActiveResultSets=True", + "MsSqlAuth": "Server=localhost\\SQLEXPRESS;Database=master;User_ID=sharikov;Password=1234567890;Trusted_Connection=True;MultipleActiveResultSets=True" + } } From c3ccfc91bc8c2824da3adfc0e96b50ab9ce7d553 Mon Sep 17 00:00:00 2001 From: Roman Sharikov Date: Mon, 13 Mar 2023 21:50:04 +0600 Subject: [PATCH 3/4] =?UTF-8?q?=D1=80=D0=B5=D1=84=D0=B0=D0=BA=D1=82=D0=BE?= =?UTF-8?q?=D1=80=D0=B8=D0=BD=D0=B3=20+=20=D0=B7=D0=B0=D0=B3=D0=BE=D1=82?= =?UTF-8?q?=D0=BE=D0=B2=D0=BA=D0=B0=20=D0=B4=D0=BB=D1=8F=20=D1=81=D0=B2?= =?UTF-8?q?=D0=B0=D0=B3=D0=B3=D0=B5=D1=80=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Services}/TodoService.cs | 0 Business/Utilities/ServiceHelpers.cs | 13 +++++++++ Data/{ => Repositories}/TodoRepository.cs | 1 + {Models => Data}/TodoContext.cs | 3 +- Data/Utilities/DataHelpers.cs | 21 ++++++++++++++ Program.cs | 6 ---- Startup.cs | 28 ++++++++++++++----- TodoApiDTO.csproj | 1 + 8 files changed, 59 insertions(+), 14 deletions(-) rename {Services => Business/Services}/TodoService.cs (100%) create mode 100644 Business/Utilities/ServiceHelpers.cs rename Data/{ => Repositories}/TodoRepository.cs (99%) rename {Models => Data}/TodoContext.cs (85%) create mode 100644 Data/Utilities/DataHelpers.cs diff --git a/Services/TodoService.cs b/Business/Services/TodoService.cs similarity index 100% rename from Services/TodoService.cs rename to Business/Services/TodoService.cs diff --git a/Business/Utilities/ServiceHelpers.cs b/Business/Utilities/ServiceHelpers.cs new file mode 100644 index 00000000..196dce82 --- /dev/null +++ b/Business/Utilities/ServiceHelpers.cs @@ -0,0 +1,13 @@ +using Microsoft.Extensions.DependencyInjection; +using TodoApi.Models; + +namespace TodoApiDTO.Services +{ + public static class ServiceHelpers + { + public static void Configure(IServiceCollection services) + { + services.AddScoped(); + } + } +} diff --git a/Data/TodoRepository.cs b/Data/Repositories/TodoRepository.cs similarity index 99% rename from Data/TodoRepository.cs rename to Data/Repositories/TodoRepository.cs index 79c14a13..76a2f081 100644 --- a/Data/TodoRepository.cs +++ b/Data/Repositories/TodoRepository.cs @@ -1,6 +1,7 @@ using Microsoft.EntityFrameworkCore; using System.Linq; using System.Threading.Tasks; +using TodoApi.Data; using TodoApi.Models; using TodoApiDTO.Models; diff --git a/Models/TodoContext.cs b/Data/TodoContext.cs similarity index 85% rename from Models/TodoContext.cs rename to Data/TodoContext.cs index 6e59e363..05b0c28e 100644 --- a/Models/TodoContext.cs +++ b/Data/TodoContext.cs @@ -1,6 +1,7 @@ using Microsoft.EntityFrameworkCore; +using TodoApi.Models; -namespace TodoApi.Models +namespace TodoApi.Data { public class TodoContext : DbContext { diff --git a/Data/Utilities/DataHelpers.cs b/Data/Utilities/DataHelpers.cs new file mode 100644 index 00000000..3d7c76fd --- /dev/null +++ b/Data/Utilities/DataHelpers.cs @@ -0,0 +1,21 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using TodoApi.Data; +using TodoApiDTO.Models; + +namespace TodoApiDTO.Data +{ + public static class DataHelpers + { + public static void SetDbConnection(IServiceCollection services, IConfiguration config) + { + services.AddDbContext(opt => opt.UseSqlServer(config.GetConnectionString("MsSqlAuth"))); + } + + public static void Configure(IServiceCollection services) + { + services.AddScoped(); + } + } +} diff --git a/Program.cs b/Program.cs index b27ac16a..4fe9a571 100644 --- a/Program.cs +++ b/Program.cs @@ -1,11 +1,5 @@ -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; namespace TodoApi { diff --git a/Startup.cs b/Startup.cs index bcddb551..c3d44c0e 100644 --- a/Startup.cs +++ b/Startup.cs @@ -1,12 +1,10 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using TodoApi.Models; +using Microsoft.OpenApi.Models; using TodoApiDTO.Data; -using TodoApiDTO.Models; using TodoApiDTO.Services; namespace TodoApi @@ -23,11 +21,21 @@ 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.AddDbContext(opt => - opt.UseSqlServer(Configuration.GetConnectionString("MsSqlAuth"))); + DataHelpers.SetDbConnection(services, Configuration); + DataHelpers.Configure(services); - services.AddScoped(); - services.AddScoped(); + ServiceHelpers.Configure(services); + + services.AddSwaggerGen(); + services.AddSwaggerGen(c => + { + c.SwaggerDoc("v1", new OpenApiInfo + { + Version = "v1", + Title = "TODO List - (API)", + Description = " ( 1) Velvetech by a.k.a. R0m43ss", + }); + }); services.AddControllers(); } @@ -50,6 +58,12 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { endpoints.MapControllers(); }); + + app.UseSwagger(); + + app.UseSwaggerUI(c => { + c.SwaggerEndpoint("/swagger/v1/swagger.json", "Showing 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 98b4204c94385153278136387efa7d42204f6dea Mon Sep 17 00:00:00 2001 From: Roman Sharikov Date: Tue, 14 Mar 2023 21:28:45 +0600 Subject: [PATCH 4/4] =?UTF-8?q?Swagger=20+=20Logger=20+=20=D0=B4=D0=BE?= =?UTF-8?q?=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=BA=D0=B8=20=D0=B8=20=D0=B8?= =?UTF-8?q?=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Controllers/TodoItemsController.cs | 132 ++++++++++++++---- Data/Repositories/TodoRepository.cs | 4 +- Models/TodoItem/TodoItem.cs | 5 +- Models/TodoItem/TodoItemActionResult.cs | 1 - README.md | 4 +- Scripts/13-03-2023 sharikov add TodoItems.sql | 4 +- Startup.cs | 27 ++-- TodoApiDTO.csproj | 10 +- Utilities/StartupHelpers.cs | 48 +++++++ appsettings.json | 9 +- 10 files changed, 187 insertions(+), 57 deletions(-) create mode 100644 Utilities/StartupHelpers.cs diff --git a/Controllers/TodoItemsController.cs b/Controllers/TodoItemsController.cs index aa0db2eb..36114196 100644 --- a/Controllers/TodoItemsController.cs +++ b/Controllers/TodoItemsController.cs @@ -1,5 +1,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -13,73 +15,147 @@ namespace TodoApi.Controllers public class TodoItemsController : ControllerBase { private readonly ITodoService _todoService; + private readonly ILogger _logger; - public TodoItemsController(ITodoService todoService) + public TodoItemsController(ITodoService todoService, ILogger logger) { _todoService = todoService; + _logger = logger; } [HttpGet] public async Task>> GetTodoItems() { - return await _todoService.GetList() - .Select(x => TodoItemDTO.ItemToDTO(x)) - .ToListAsync(); + try + { + return await _todoService.GetList() + .Select(x => TodoItemDTO.ItemToDTO(x)) + .ToListAsync(); + } + catch (Exception ex) + { + _logger.LogError($"GetTodoItems: {ex.Message}\n{ex.StackTrace}"); + return BadRequest(ex.Message); + } } [HttpGet("{id}")] public async Task> GetTodoItem(long id) { - var todoItem = await _todoService.GetById(id); - if (todoItem == null) + if (id < 0) { - return NotFound(); + _logger.LogError($"GetTodoItem: invalid id value ({id})"); + return BadRequest("Invalid id"); } - return TodoItemDTO.ItemToDTO(todoItem); + try + { + var todoItem = await _todoService.GetById(id); + if (todoItem == null) + { + _logger.LogError($"GetTodoItem: item not found (id:{id})"); + return NotFound("TODO item not found"); + } + + return TodoItemDTO.ItemToDTO(todoItem); + } + catch (Exception ex) + { + _logger.LogError($"GetTodoItem (id:{id}): {ex.Message}\n{ex.StackTrace}"); + return BadRequest(ex.Message); + } } - [HttpPut("{id}")] + [HttpPut("{id}/update")] public async Task UpdateTodoItem(long id, TodoItemDTO todoItemDTO) { - if (id != todoItemDTO.Id) + if (id < 0) + { + _logger.LogError($"UpdateTodoItem: invalid id value ({id})"); + return BadRequest("Invalid id"); + } + if (todoItemDTO == null) { - return BadRequest(); + _logger.LogError($"UpdateTodoItem: request is null (id:{id})"); + return BadRequest("Request is null"); } - var result = await _todoService.Update(todoItemDTO); + if (id != todoItemDTO.Id) + { + _logger.LogError($"UpdateTodoItem: ids mismatch (parameter id:{id}, request id:{todoItemDTO?.Id})"); + return BadRequest("Ids mismatch"); + } - return GetTodoItemActionResult(result); + try + { + var result = await _todoService.Update(todoItemDTO); + return HandleActionResult(result, $"UpdateTodoItem (id:{id})"); + } + catch (Exception ex) + { + _logger.LogError($"UpdateTodoItem (id:{id}): {ex.Message}\n{ex.StackTrace}"); + return BadRequest(ex.Message); + } } + [Route("create")] [HttpPost] public async Task> CreateTodoItem(TodoItemDTO todoItemDTO) { - var todoItem = await _todoService.Create(todoItemDTO); + try + { + var todoItem = await _todoService.Create(todoItemDTO); + if (todoItem == null) + { + _logger.LogError($"CreateTodoItem: cannot create item"); + return BadRequest("Couldn't create TODO item"); + } - return CreatedAtAction( - nameof(GetTodoItem), - new { id = todoItem.Id }, - TodoItemDTO.ItemToDTO(todoItem)); + return CreatedAtAction( + nameof(GetTodoItem), + new { id = todoItem.Id }, + TodoItemDTO.ItemToDTO(todoItem)); + } + catch (Exception ex) + { + _logger.LogError($"CreateTodoItem: {ex.Message}\n{ex.StackTrace}"); + return BadRequest(ex.Message); + } } - [HttpDelete("{id}")] + [HttpDelete("{id}/delete")] public async Task DeleteTodoItem(long id) { - var result = await _todoService.Delete(id); + if (id < 0) + { + _logger.LogError($"DeleteTodoItem: invalid id ({id})"); + return BadRequest("Invalid id"); + } - return GetTodoItemActionResult(result); + try + { + var result = await _todoService.Delete(id); + return HandleActionResult(result, $"DeleteTodoItem (id:{id})"); + } + catch (Exception ex) + { + _logger.LogError($"DeleteTodoItem (id:{id}): {ex.Message}\n{ex.StackTrace}"); + return BadRequest(ex.Message); + } } - private IActionResult GetTodoItemActionResult(TodoItemActionResult result) + private IActionResult HandleActionResult(TodoItemActionResult result, string actionName) { - return result switch + switch (result) { - TodoItemActionResult.Success => NoContent(), - TodoItemActionResult.NotFound => NotFound(), - TodoItemActionResult.Failed => BadRequest(), - _ => Accepted(), - }; + case TodoItemActionResult.Success: + return NoContent(); + case TodoItemActionResult.NotFound: + _logger.LogError($"{actionName}: item not found"); + return NotFound("TODO item not found"); + default: + return NoContent(); + } } } } diff --git a/Data/Repositories/TodoRepository.cs b/Data/Repositories/TodoRepository.cs index 76a2f081..53ac9c91 100644 --- a/Data/Repositories/TodoRepository.cs +++ b/Data/Repositories/TodoRepository.cs @@ -64,9 +64,9 @@ public async Task Update(TodoItemDTO input) { await _context.SaveChangesAsync(); } - catch (DbUpdateConcurrencyException) when (!_context.TodoItems.Any(e => e.Id == input.Id)) + catch (DbUpdateConcurrencyException ex) when (!_context.TodoItems.Any(e => e.Id == input.Id)) { - return TodoItemActionResult.NotFound; + throw ex; } return TodoItemActionResult.Success; diff --git a/Models/TodoItem/TodoItem.cs b/Models/TodoItem/TodoItem.cs index 1f6e5465..2b2c941f 100644 --- a/Models/TodoItem/TodoItem.cs +++ b/Models/TodoItem/TodoItem.cs @@ -1,8 +1,11 @@ -namespace TodoApi.Models +using System.ComponentModel.DataAnnotations.Schema; + +namespace TodoApi.Models { #region snippet public class TodoItem { + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public long Id { get; set; } public string Name { get; set; } public bool IsComplete { get; set; } diff --git a/Models/TodoItem/TodoItemActionResult.cs b/Models/TodoItem/TodoItemActionResult.cs index 51e152dd..7afc28fe 100644 --- a/Models/TodoItem/TodoItemActionResult.cs +++ b/Models/TodoItem/TodoItemActionResult.cs @@ -3,7 +3,6 @@ public enum TodoItemActionResult: byte { Success = 0, - Failed = 1, NotFound = 2 } } diff --git a/README.md b/README.md index 466e41fd..65dfa52e 100644 --- a/README.md +++ b/README.md @@ -1 +1,3 @@ -# TodoService \ No newline at end of file +# TodoService + + 1 \ No newline at end of file diff --git a/Scripts/13-03-2023 sharikov add TodoItems.sql b/Scripts/13-03-2023 sharikov add TodoItems.sql index ff6f6e42..0ec096de 100644 --- a/Scripts/13-03-2023 sharikov add TodoItems.sql +++ b/Scripts/13-03-2023 sharikov add TodoItems.sql @@ -1,7 +1,7 @@ CREATE TABLE TodoItems ( - Id int NOT NULL, - Name nvarchar(255) NOT NULL, + Id bigint NOT NULL IDENTITY(1,1), + Name nvarchar(255), IsComplete bit NOT NULL, Secret nvarchar(255), PRIMARY KEY (Id) diff --git a/Startup.cs b/Startup.cs index c3d44c0e..75958eda 100644 --- a/Startup.cs +++ b/Startup.cs @@ -3,7 +3,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using Microsoft.OpenApi.Models; +using TodoApiDTO; using TodoApiDTO.Data; using TodoApiDTO.Services; @@ -26,16 +26,8 @@ public void ConfigureServices(IServiceCollection services) ServiceHelpers.Configure(services); - services.AddSwaggerGen(); - services.AddSwaggerGen(c => - { - c.SwaggerDoc("v1", new OpenApiInfo - { - Version = "v1", - Title = "TODO List - (API)", - Description = " ( 1) Velvetech by a.k.a. R0m43ss", - }); - }); + StartupHelpers.InitSwagger(services); + StartupHelpers.InitLogging(services); services.AddControllers(); } @@ -48,6 +40,13 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseDeveloperExceptionPage(); } + app.UseSwagger(); + + app.UseSwaggerUI(c => { + c.SwaggerEndpoint("/swagger/v1/swagger.json", "v1.0.0"); + c.RoutePrefix = string.Empty; + }); + app.UseHttpsRedirection(); app.UseRouting(); @@ -58,12 +57,6 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { endpoints.MapControllers(); }); - - app.UseSwagger(); - - app.UseSwaggerUI(c => { - c.SwaggerEndpoint("/swagger/v1/swagger.json", "Showing API V1"); - }); } } } diff --git a/TodoApiDTO.csproj b/TodoApiDTO.csproj index 6c186056..f760bf83 100644 --- a/TodoApiDTO.csproj +++ b/TodoApiDTO.csproj @@ -1,9 +1,16 @@ - + netcoreapp3.1 + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive @@ -12,6 +19,7 @@ + diff --git a/Utilities/StartupHelpers.cs b/Utilities/StartupHelpers.cs new file mode 100644 index 00000000..96f93b2e --- /dev/null +++ b/Utilities/StartupHelpers.cs @@ -0,0 +1,48 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.OpenApi.Models; +using System; +using System.Text; + +namespace TodoApiDTO +{ + public static class StartupHelpers + { + public static void InitSwagger(IServiceCollection services) + { + services.AddSwaggerGen(); + services.AddSwaggerGen(c => + { + c.SwaggerDoc("v1", new OpenApiInfo + { + Version = "v1", + Title = "TODO List - API для управления списком задач", + Description = "Тестовое задание (вариант №1) для Velvetech by Шариков Роман a.k.a. R0m43ss", + }); + }); + + } + + public static void InitLogging(IServiceCollection services) + { + services.AddLogging(loggingBuilder => { + loggingBuilder.AddFile("!log\\{0:yyyy}-{0:MM}-{0:dd}.log", fileLoggerOptions => { + fileLoggerOptions.FormatLogFileName = fileName => { + return string.Format(fileName, DateTime.Now); + }; + fileLoggerOptions.FilterLogEntry = (logMessage) => { + return logMessage.LogLevel == LogLevel.Error; + }; + fileLoggerOptions.FormatLogEntry = (logMessage) => + { + var sb = new StringBuilder(); + sb.Append(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss ")); + sb.Append($"{Enum.GetName(typeof(LogLevel), logMessage.LogLevel).ToUpper()} "); + sb.Append(logMessage.Message); + return sb.ToString(); + }; + }); + }); + } + } +} diff --git a/appsettings.json b/appsettings.json index 27dab2d7..2ca5a612 100644 --- a/appsettings.json +++ b/appsettings.json @@ -1,14 +1,15 @@ { "Logging": { "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" + "Default": "None", + "Microsoft": "None", + "Microsoft.Hosting.Lifetime": "None", + "Microsoft.EntityFrameworkCore.Database.Command": "None" } }, "AllowedHosts": "*", "ConnectionStrings": { "WinAuth": "Server=localhost\\SQLEXPRESS;Database=master;Trusted_Connection=True;MultipleActiveResultSets=True", - "MsSqlAuth": "Server=localhost\\SQLEXPRESS;Database=master;User_ID=sharikov;Password=1234567890;Trusted_Connection=True;MultipleActiveResultSets=True" + "MsSqlAuth": "Server=localhost\\SQLEXPRESS;Database=master;User ID=sharikov;Password=1234567890;Trusted_Connection=True;MultipleActiveResultSets=True" } }