From c0f41b591947637b6cdd144d1701ba43191da54c Mon Sep 17 00:00:00 2001 From: eugenegritsina Date: Fri, 16 Jun 2023 20:47:18 +0400 Subject: [PATCH 1/9] feat: add swagger + add TodoService + TodoRepository + connect to DB --- Controllers/TodoItemsController.cs | 75 +++++------------- Mappers/TodoProfile.cs | 15 ++++ Models/TodoItem.cs | 2 - Models/TodoItemDTO.cs | 2 - Program.cs | 6 -- Repositories/Interfaces/ITodoRepository.cs | 15 ++++ Repositories/TodoRepository.cs | 57 ++++++++++++++ Services/Interfaces/ITodoService.cs | 15 ++++ Services/TodoService.cs | 88 ++++++++++++++++++++++ Startup.cs | 43 ++++++++--- TodoApiDTO.csproj | 4 + 11 files changed, 245 insertions(+), 77 deletions(-) create mode 100644 Mappers/TodoProfile.cs create mode 100644 Repositories/Interfaces/ITodoRepository.cs create mode 100644 Repositories/TodoRepository.cs create mode 100644 Services/Interfaces/ITodoService.cs create mode 100644 Services/TodoService.cs diff --git a/Controllers/TodoItemsController.cs b/Controllers/TodoItemsController.cs index 0ef138e7..f64b6f9c 100644 --- a/Controllers/TodoItemsController.cs +++ b/Controllers/TodoItemsController.cs @@ -1,9 +1,8 @@ using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; using TodoApi.Models; +using TodoApiDTO.Services.Interfaces; namespace TodoApi.Controllers { @@ -11,56 +10,40 @@ 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)) - .ToListAsync(); - } + public async Task>> GetAll() => Ok(await _todoService.GetAll()); [HttpGet("{id}")] - public async Task> GetTodoItem(long id) + public async Task> Get(long id) { - var todoItem = await _context.TodoItems.FindAsync(id); + var todoItem = await _todoService.Get(id); if (todoItem == null) { return NotFound(); } - return ItemToDTO(todoItem); + return todoItem; } [HttpPut("{id}")] - public async Task UpdateTodoItem(long id, TodoItemDTO todoItemDTO) + public async Task> Update(long id, TodoItemDTO todoItemDTO) { if (id != todoItemDTO.Id) { return BadRequest(); } - var todoItem = await _context.TodoItems.FindAsync(id); - if (todoItem == null) - { - return NotFound(); - } - - todoItem.Name = todoItemDTO.Name; - todoItem.IsComplete = todoItemDTO.IsComplete; + var isFound = await _todoService.Update(id, todoItemDTO); - try - { - await _context.SaveChangesAsync(); - } - catch (DbUpdateConcurrencyException) when (!TodoItemExists(id)) + if (isFound == false) { return NotFound(); } @@ -69,48 +52,24 @@ public async Task UpdateTodoItem(long id, TodoItemDTO todoItemDTO } [HttpPost] - public async Task> CreateTodoItem(TodoItemDTO todoItemDTO) + public async Task> Create(TodoItemDTO todoItemDTO) { - var todoItem = new TodoItem - { - IsComplete = todoItemDTO.IsComplete, - Name = todoItemDTO.Name - }; + var todo = await _todoService.Create(todoItemDTO); - _context.TodoItems.Add(todoItem); - await _context.SaveChangesAsync(); - - return CreatedAtAction( - nameof(GetTodoItem), - new { id = todoItem.Id }, - ItemToDTO(todoItem)); + return CreatedAtAction(nameof(Get), new { id = todo.Id }, todo); } [HttpDelete("{id}")] - public async Task DeleteTodoItem(long id) + public async Task Delete(long id) { - var todoItem = await _context.TodoItems.FindAsync(id); + var isFound = await _todoService.Delete(id); - if (todoItem == null) + if (isFound == false) { 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/Mappers/TodoProfile.cs b/Mappers/TodoProfile.cs new file mode 100644 index 00000000..a7ac29cb --- /dev/null +++ b/Mappers/TodoProfile.cs @@ -0,0 +1,15 @@ +using AutoMapper; +using TodoApi.Models; + +namespace GeekStore.API.Core.Configurations +{ + public class TodoProfile : Profile + { + public TodoProfile() + { + CreateMap() + .ForMember(x => x.Secret, opt => opt.Ignore()) + .ReverseMap(); + } + } +} \ No newline at end of file diff --git a/Models/TodoItem.cs b/Models/TodoItem.cs index 1f6e5465..8b76633b 100644 --- a/Models/TodoItem.cs +++ b/Models/TodoItem.cs @@ -1,6 +1,5 @@ namespace TodoApi.Models { - #region snippet public class TodoItem { public long Id { get; set; } @@ -8,5 +7,4 @@ public class TodoItem public bool IsComplete { get; set; } public string Secret { get; set; } } - #endregion } \ No newline at end of file diff --git a/Models/TodoItemDTO.cs b/Models/TodoItemDTO.cs index e66a500a..0083606f 100644 --- a/Models/TodoItemDTO.cs +++ b/Models/TodoItemDTO.cs @@ -1,11 +1,9 @@ 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/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/Repositories/Interfaces/ITodoRepository.cs b/Repositories/Interfaces/ITodoRepository.cs new file mode 100644 index 00000000..c5930a7a --- /dev/null +++ b/Repositories/Interfaces/ITodoRepository.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using TodoApi.Models; + +namespace TodoApiDTO.Repositories.Interfaces +{ + public interface ITodoRepository + { + public Task> GetAll(); + public Task Get(long id); + public Task Update(long id, TodoItem todoItem); + public Task Create(TodoItem todoItem); + public Task Delete(TodoItem todoItem); + } +} diff --git a/Repositories/TodoRepository.cs b/Repositories/TodoRepository.cs new file mode 100644 index 00000000..baa3ccaf --- /dev/null +++ b/Repositories/TodoRepository.cs @@ -0,0 +1,57 @@ +using Microsoft.EntityFrameworkCore; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using TodoApi.Models; +using TodoApiDTO.Repositories.Interfaces; + +namespace TodoApiDTO.Repositories +{ + public class TodoRepository : ITodoRepository + { + private TodoContext _context; + + public TodoRepository(TodoContext context) + { + _context = context; + } + + public async Task> GetAll() + { + return await _context.TodoItems.ToListAsync(); + } + + public async Task Get(long id) + { + return await _context.TodoItems.FindAsync(id); + } + + public async Task Update(long id, TodoItem todoItem) + { + try + { + await _context.SaveChangesAsync(); + } + catch (DbUpdateConcurrencyException) when (!TodoItemExists(id)) + { + return false; + } + return true; + } + public async Task Create(TodoItem todoItem) + { + _context.TodoItems.Add(todoItem); + await _context.SaveChangesAsync(); + + return todoItem; + } + + public async Task Delete(TodoItem todoItem) + { + _context.TodoItems.Remove(todoItem); + await _context.SaveChangesAsync(); + } + + private bool TodoItemExists(long id) => _context.TodoItems.Any(e => e.Id == id); + } +} diff --git a/Services/Interfaces/ITodoService.cs b/Services/Interfaces/ITodoService.cs new file mode 100644 index 00000000..dccae3f5 --- /dev/null +++ b/Services/Interfaces/ITodoService.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using TodoApi.Models; + +namespace TodoApiDTO.Services.Interfaces +{ + public interface ITodoService + { + public Task> GetAll(); + public Task Get(long id); + public Task Update(long id, TodoItemDTO todoItemDTO); + public Task Create(TodoItemDTO todoItemDTO); + public Task Delete(long id); + } +} diff --git a/Services/TodoService.cs b/Services/TodoService.cs new file mode 100644 index 00000000..c15ad0b8 --- /dev/null +++ b/Services/TodoService.cs @@ -0,0 +1,88 @@ +using AutoMapper; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using TodoApi.Models; +using TodoApiDTO.Repositories.Interfaces; +using TodoApiDTO.Services.Interfaces; + +namespace TodoApiDTO.Services +{ + public class TodoService : ITodoService + { + private readonly ITodoRepository _repository; + private readonly IMapper _mapper; + + + public TodoService(ITodoRepository repository, IMapper mapper) + { + _repository = repository; + _mapper = mapper; + } + + public async Task> GetAll() + { + var todos = await _repository.GetAll(); + // TODO: mapper + //var dtos = todos.Select(todo => _mapper.Map(todo)); + var dtos = todos.Select(todo => ItemToDTO(todo)); + return dtos; + } + + public async Task Get(long id) + { + var todo = await _repository.Get(id); + // TODO: mapper + //return _mapper.Map(todo); + return ItemToDTO(todo); + } + + public async Task Update(long id, TodoItemDTO todoItemDTO) + { + var todoItem = await _repository.Get(id); + if (todoItem == null) + return false; + // TODO: mapper + todoItem.Name = todoItemDTO.Name; + todoItem.IsComplete = todoItemDTO.IsComplete; + return await _repository.Update(id, todoItem); + } + + public async Task Create(TodoItemDTO todoItemDTO) + { + // TODO: mapper + var todoItem = new TodoItem + { + IsComplete = todoItemDTO.IsComplete, + Name = todoItemDTO.Name + }; + + // TODO: mapper + //return await _repository.Create(todoItem); + var todo = await _repository.Create(todoItem); + return ItemToDTO(todo); + } + + public async Task Delete(long id) + { + var todoItem = await _repository.Get(id); + + if (todoItem == null) + { + return false; + } + await _repository.Delete(todoItem); + + return true; + } + + // TODO: remove + private static TodoItemDTO ItemToDTO(TodoItem todoItem) => + new TodoItemDTO + { + Id = todoItem.Id, + Name = todoItem.Name, + IsComplete = todoItem.IsComplete + }; + } +} diff --git a/Startup.cs b/Startup.cs index bbfbc83d..37a3afa2 100644 --- a/Startup.cs +++ b/Startup.cs @@ -1,17 +1,18 @@ -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 Microsoft.OpenApi.Models; +using TodoApiDTO.Services.Interfaces; +using TodoApiDTO.Services; +using TodoApiDTO.Repositories; +using TodoApiDTO.Repositories.Interfaces; +// TODO: mapper +//using AutoMapper; +//using GeekStore.API.Core.Configurations; namespace TodoApi { @@ -27,9 +28,26 @@ 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.UseInMemoryDatabase("TodoList")); services.AddControllers(); + services.AddSwaggerGen(c => + { + c.SwaggerDoc("v1", new OpenApiInfo { Title = "Todo API", Version = "v1" }); + }); + + services.AddDbContext(options => + options.UseSqlServer(Configuration.GetConnectionString("TodosDatabase"))); + + // TODO: mapper + //var mapperConfig = new MapperConfiguration(mc => + //{ + // mc.AddProfile(new TodoProfile()); + //}); + + //IMapper mapper = mapperConfig.CreateMapper(); + //services.AddSingleton(mapper); + + services.AddScoped(); + services.AddScoped(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. @@ -50,6 +68,13 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { endpoints.MapControllers(); }); + + app.UseSwagger(); + + app.UseSwaggerUI(c => + { + c.SwaggerEndpoint("/swagger/v1/swagger.json", "TodoAPI"); + }); } } } diff --git a/TodoApiDTO.csproj b/TodoApiDTO.csproj index bba6f6af..2641ffa3 100644 --- a/TodoApiDTO.csproj +++ b/TodoApiDTO.csproj @@ -5,6 +5,8 @@ + + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -12,6 +14,8 @@ + + From c32f194bb397a5b1d6de1cd63fcaa0e84dc8e65c Mon Sep 17 00:00:00 2001 From: eugenegritsina Date: Sat, 17 Jun 2023 13:15:32 +0400 Subject: [PATCH 2/9] feat: add automapper --- Services/TodoService.cs | 32 +++++--------------------------- Startup.cs | 18 ++++++++---------- 2 files changed, 13 insertions(+), 37 deletions(-) diff --git a/Services/TodoService.cs b/Services/TodoService.cs index c15ad0b8..2901be01 100644 --- a/Services/TodoService.cs +++ b/Services/TodoService.cs @@ -23,18 +23,14 @@ public TodoService(ITodoRepository repository, IMapper mapper) public async Task> GetAll() { var todos = await _repository.GetAll(); - // TODO: mapper - //var dtos = todos.Select(todo => _mapper.Map(todo)); - var dtos = todos.Select(todo => ItemToDTO(todo)); + var dtos = todos.Select(todo => _mapper.Map(todo)); return dtos; } public async Task Get(long id) { var todo = await _repository.Get(id); - // TODO: mapper - //return _mapper.Map(todo); - return ItemToDTO(todo); + return _mapper.Map(todo); } public async Task Update(long id, TodoItemDTO todoItemDTO) @@ -42,25 +38,16 @@ public async Task Update(long id, TodoItemDTO todoItemDTO) var todoItem = await _repository.Get(id); if (todoItem == null) return false; - // TODO: mapper - todoItem.Name = todoItemDTO.Name; - todoItem.IsComplete = todoItemDTO.IsComplete; + todoItem = _mapper.Map(todoItemDTO); return await _repository.Update(id, todoItem); } public async Task Create(TodoItemDTO todoItemDTO) { - // TODO: mapper - var todoItem = new TodoItem - { - IsComplete = todoItemDTO.IsComplete, - Name = todoItemDTO.Name - }; + var todoItem = _mapper.Map(todoItemDTO); - // TODO: mapper - //return await _repository.Create(todoItem); var todo = await _repository.Create(todoItem); - return ItemToDTO(todo); + return _mapper.Map(todo); } public async Task Delete(long id) @@ -75,14 +62,5 @@ public async Task Delete(long id) return true; } - - // TODO: remove - private static TodoItemDTO ItemToDTO(TodoItem todoItem) => - new TodoItemDTO - { - Id = todoItem.Id, - Name = todoItem.Name, - IsComplete = todoItem.IsComplete - }; } } diff --git a/Startup.cs b/Startup.cs index 37a3afa2..b2b5b5cc 100644 --- a/Startup.cs +++ b/Startup.cs @@ -10,9 +10,8 @@ using TodoApiDTO.Services; using TodoApiDTO.Repositories; using TodoApiDTO.Repositories.Interfaces; -// TODO: mapper -//using AutoMapper; -//using GeekStore.API.Core.Configurations; +using AutoMapper; +using GeekStore.API.Core.Configurations; namespace TodoApi { @@ -37,14 +36,13 @@ public void ConfigureServices(IServiceCollection services) services.AddDbContext(options => options.UseSqlServer(Configuration.GetConnectionString("TodosDatabase"))); - // TODO: mapper - //var mapperConfig = new MapperConfiguration(mc => - //{ - // mc.AddProfile(new TodoProfile()); - //}); + var mapperConfig = new MapperConfiguration(mc => + { + mc.AddProfile(new TodoProfile()); + }); - //IMapper mapper = mapperConfig.CreateMapper(); - //services.AddSingleton(mapper); + IMapper mapper = mapperConfig.CreateMapper(); + services.AddSingleton(mapper); services.AddScoped(); services.AddScoped(); From 31f12b59ae05edd556f16a7ccc21fef7d52c7b7f Mon Sep 17 00:00:00 2001 From: eugenegritsina Date: Sat, 17 Jun 2023 14:14:38 +0400 Subject: [PATCH 3/9] feat: add Tests project + Mapper tests --- Tests/Tests.csproj | 25 ++++++++ Tests/TodoMapperTests.cs | 62 +++++++++++++++++++ Tests/TodoRepositoryTests.cs | 13 ++++ Tests/Usings.cs | 1 + .../Controllers}/TodoItemsController.cs | 0 .../Mappers}/TodoProfile.cs | 0 {Models => TodoApiDTO/Models}/TodoContext.cs | 0 {Models => TodoApiDTO/Models}/TodoItem.cs | 0 {Models => TodoApiDTO/Models}/TodoItemDTO.cs | 0 Program.cs => TodoApiDTO/Program.cs | 0 .../Properties}/launchSettings.json | 0 README.md => TodoApiDTO/README.md | 0 .../Interfaces/ITodoRepository.cs | 0 .../Repositories}/TodoRepository.cs | 0 .../Services}/Interfaces/ITodoService.cs | 0 .../Services}/TodoService.cs | 1 - Startup.cs => TodoApiDTO/Startup.cs | 0 .../TodoApiDTO.csproj | 7 +++ TodoApiDTO.sln => TodoApiDTO/TodoApiDTO.sln | 10 ++- .../appsettings.Development.json | 5 +- .../appsettings.json | 0 21 files changed, 120 insertions(+), 4 deletions(-) create mode 100644 Tests/Tests.csproj create mode 100644 Tests/TodoMapperTests.cs create mode 100644 Tests/TodoRepositoryTests.cs create mode 100644 Tests/Usings.cs rename {Controllers => TodoApiDTO/Controllers}/TodoItemsController.cs (100%) rename {Mappers => TodoApiDTO/Mappers}/TodoProfile.cs (100%) rename {Models => TodoApiDTO/Models}/TodoContext.cs (100%) rename {Models => TodoApiDTO/Models}/TodoItem.cs (100%) rename {Models => TodoApiDTO/Models}/TodoItemDTO.cs (100%) rename Program.cs => TodoApiDTO/Program.cs (100%) rename {Properties => TodoApiDTO/Properties}/launchSettings.json (100%) rename README.md => TodoApiDTO/README.md (100%) rename {Repositories => TodoApiDTO/Repositories}/Interfaces/ITodoRepository.cs (100%) rename {Repositories => TodoApiDTO/Repositories}/TodoRepository.cs (100%) rename {Services => TodoApiDTO/Services}/Interfaces/ITodoService.cs (100%) rename {Services => TodoApiDTO/Services}/TodoService.cs (99%) rename Startup.cs => TodoApiDTO/Startup.cs (100%) rename TodoApiDTO.csproj => TodoApiDTO/TodoApiDTO.csproj (84%) rename TodoApiDTO.sln => TodoApiDTO/TodoApiDTO.sln (66%) rename appsettings.Development.json => TodoApiDTO/appsettings.Development.json (54%) rename appsettings.json => TodoApiDTO/appsettings.json (100%) diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj new file mode 100644 index 00000000..099c728c --- /dev/null +++ b/Tests/Tests.csproj @@ -0,0 +1,25 @@ + + + + net6.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + diff --git a/Tests/TodoMapperTests.cs b/Tests/TodoMapperTests.cs new file mode 100644 index 00000000..1c6d1105 --- /dev/null +++ b/Tests/TodoMapperTests.cs @@ -0,0 +1,62 @@ +using AutoMapper; +using GeekStore.API.Core.Configurations; +using TodoApi.Models; +using TodoApiDTO.DTOs; + +namespace Tests +{ + [TestFixture] + public class TodoMapperTests + { + private readonly IMapper _mapper; + + public TodoMapperTests() + { + var mapperConfig = new MapperConfiguration(mc => + { + mc.AddProfile(new TodoProfile()); + }); + + _mapper = mapperConfig.CreateMapper(); + } + + [Test] + public void ModelToDtoTest() + { + var todo = new TodoItem { Id = 1, IsComplete = true, Name = "model", Secret = "secret" }; + var dto = _mapper.Map(todo); + + Assert.NotNull(dto); + Assert.IsTrue(dto.IsComplete); + Assert.AreEqual(todo.Id, dto.Id); + Assert.AreEqual(todo.Name, dto.Name); + } + + [Test] + public void DtoToModelTest() + { + var dto = new TodoItemDTO { Id = 1, IsComplete = true, Name = "model" }; + var todo = _mapper.Map(dto); + + Assert.NotNull(todo); + Assert.IsTrue(todo.IsComplete); + Assert.AreEqual(dto.Id, todo.Id); + Assert.AreEqual(todo.Name, dto.Name); + Assert.Null(todo.Secret); + } + + [Test] + public void CreateUpdateDtoToModelTest() + { + var dto = new CreateUpdateItemTodoDTO { Name = "postput", IsComplete = false }; + var todo = new TodoItem { Id = 1 }; + + _mapper.Map(dto, todo); + + Assert.IsFalse(todo.IsComplete); + Assert.AreEqual(dto.Name, todo.Name); + Assert.AreEqual(1, todo.Id); + + } + } +} \ No newline at end of file diff --git a/Tests/TodoRepositoryTests.cs b/Tests/TodoRepositoryTests.cs new file mode 100644 index 00000000..2bbc14e7 --- /dev/null +++ b/Tests/TodoRepositoryTests.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Tests +{ + [TestFixture] + public class TodoRepositoryTests + { + } +} diff --git a/Tests/Usings.cs b/Tests/Usings.cs new file mode 100644 index 00000000..cefced49 --- /dev/null +++ b/Tests/Usings.cs @@ -0,0 +1 @@ +global using NUnit.Framework; \ No newline at end of file diff --git a/Controllers/TodoItemsController.cs b/TodoApiDTO/Controllers/TodoItemsController.cs similarity index 100% rename from Controllers/TodoItemsController.cs rename to TodoApiDTO/Controllers/TodoItemsController.cs diff --git a/Mappers/TodoProfile.cs b/TodoApiDTO/Mappers/TodoProfile.cs similarity index 100% rename from Mappers/TodoProfile.cs rename to TodoApiDTO/Mappers/TodoProfile.cs diff --git a/Models/TodoContext.cs b/TodoApiDTO/Models/TodoContext.cs similarity index 100% rename from Models/TodoContext.cs rename to TodoApiDTO/Models/TodoContext.cs diff --git a/Models/TodoItem.cs b/TodoApiDTO/Models/TodoItem.cs similarity index 100% rename from Models/TodoItem.cs rename to TodoApiDTO/Models/TodoItem.cs diff --git a/Models/TodoItemDTO.cs b/TodoApiDTO/Models/TodoItemDTO.cs similarity index 100% rename from Models/TodoItemDTO.cs rename to TodoApiDTO/Models/TodoItemDTO.cs diff --git a/Program.cs b/TodoApiDTO/Program.cs similarity index 100% rename from Program.cs rename to TodoApiDTO/Program.cs diff --git a/Properties/launchSettings.json b/TodoApiDTO/Properties/launchSettings.json similarity index 100% rename from Properties/launchSettings.json rename to TodoApiDTO/Properties/launchSettings.json diff --git a/README.md b/TodoApiDTO/README.md similarity index 100% rename from README.md rename to TodoApiDTO/README.md diff --git a/Repositories/Interfaces/ITodoRepository.cs b/TodoApiDTO/Repositories/Interfaces/ITodoRepository.cs similarity index 100% rename from Repositories/Interfaces/ITodoRepository.cs rename to TodoApiDTO/Repositories/Interfaces/ITodoRepository.cs diff --git a/Repositories/TodoRepository.cs b/TodoApiDTO/Repositories/TodoRepository.cs similarity index 100% rename from Repositories/TodoRepository.cs rename to TodoApiDTO/Repositories/TodoRepository.cs diff --git a/Services/Interfaces/ITodoService.cs b/TodoApiDTO/Services/Interfaces/ITodoService.cs similarity index 100% rename from Services/Interfaces/ITodoService.cs rename to TodoApiDTO/Services/Interfaces/ITodoService.cs diff --git a/Services/TodoService.cs b/TodoApiDTO/Services/TodoService.cs similarity index 99% rename from Services/TodoService.cs rename to TodoApiDTO/Services/TodoService.cs index 2901be01..d05d7413 100644 --- a/Services/TodoService.cs +++ b/TodoApiDTO/Services/TodoService.cs @@ -13,7 +13,6 @@ public class TodoService : ITodoService private readonly ITodoRepository _repository; private readonly IMapper _mapper; - public TodoService(ITodoRepository repository, IMapper mapper) { _repository = repository; diff --git a/Startup.cs b/TodoApiDTO/Startup.cs similarity index 100% rename from Startup.cs rename to TodoApiDTO/Startup.cs diff --git a/TodoApiDTO.csproj b/TodoApiDTO/TodoApiDTO.csproj similarity index 84% rename from TodoApiDTO.csproj rename to TodoApiDTO/TodoApiDTO.csproj index 2641ffa3..02edcef4 100644 --- a/TodoApiDTO.csproj +++ b/TodoApiDTO/TodoApiDTO.csproj @@ -4,6 +4,13 @@ netcoreapp3.1 + + + + + + + diff --git a/TodoApiDTO.sln b/TodoApiDTO/TodoApiDTO.sln similarity index 66% rename from TodoApiDTO.sln rename to TodoApiDTO/TodoApiDTO.sln index e49c182b..a786fd7b 100644 --- a/TodoApiDTO.sln +++ b/TodoApiDTO/TodoApiDTO.sln @@ -1,10 +1,12 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.30002.166 +# Visual Studio Version 17 +VisualStudioVersion = 17.6.33801.468 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TodoApiDTO", "TodoApiDTO.csproj", "{623124F9-F5BA-42DD-BC26-A1720774229C}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "..\Tests\Tests.csproj", "{FBA88543-0422-4044-B1C3-FC7ECFC14AA9}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -15,6 +17,10 @@ Global {623124F9-F5BA-42DD-BC26-A1720774229C}.Debug|Any CPU.Build.0 = Debug|Any CPU {623124F9-F5BA-42DD-BC26-A1720774229C}.Release|Any CPU.ActiveCfg = Release|Any CPU {623124F9-F5BA-42DD-BC26-A1720774229C}.Release|Any CPU.Build.0 = Release|Any CPU + {FBA88543-0422-4044-B1C3-FC7ECFC14AA9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FBA88543-0422-4044-B1C3-FC7ECFC14AA9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FBA88543-0422-4044-B1C3-FC7ECFC14AA9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FBA88543-0422-4044-B1C3-FC7ECFC14AA9}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/appsettings.Development.json b/TodoApiDTO/appsettings.Development.json similarity index 54% rename from appsettings.Development.json rename to TodoApiDTO/appsettings.Development.json index 8983e0fc..11b4c79f 100644 --- a/appsettings.Development.json +++ b/TodoApiDTO/appsettings.Development.json @@ -5,5 +5,8 @@ "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } + }, + "ConnectionStrings": { + "TodosDatabase": "Server=DESKTOP-0ROQ806\\SQLEXPRESS;Database=ToDos;Trusted_Connection=True;" } -} +} \ No newline at end of file diff --git a/appsettings.json b/TodoApiDTO/appsettings.json similarity index 100% rename from appsettings.json rename to TodoApiDTO/appsettings.json From 0461e2ede1393e76c6cceaf29e9a2cb32eb0f8cb Mon Sep 17 00:00:00 2001 From: eugenegritsina Date: Sat, 17 Jun 2023 19:53:03 +0400 Subject: [PATCH 4/9] refactor: move mapping logic to repo + restrict setting Id in body when PUT/POST to avoid confusing situations --- TodoApiDTO/Controllers/TodoItemsController.cs | 15 ++---- TodoApiDTO/DTOs/CreateUpdateItemTodoDTO.cs | 8 ++++ TodoApiDTO/{Models => DTOs}/TodoItemDTO.cs | 2 +- TodoApiDTO/Mappers/TodoProfile.cs | 3 ++ .../Interfaces/ITodoRepository.cs | 12 ++--- TodoApiDTO/Repositories/TodoRepository.cs | 46 ++++++++++++++----- .../Services/Interfaces/ITodoService.cs | 6 +-- TodoApiDTO/Services/TodoService.cs | 39 ++++------------ 8 files changed, 69 insertions(+), 62 deletions(-) create mode 100644 TodoApiDTO/DTOs/CreateUpdateItemTodoDTO.cs rename TodoApiDTO/{Models => DTOs}/TodoItemDTO.cs (85%) diff --git a/TodoApiDTO/Controllers/TodoItemsController.cs b/TodoApiDTO/Controllers/TodoItemsController.cs index f64b6f9c..3874e76f 100644 --- a/TodoApiDTO/Controllers/TodoItemsController.cs +++ b/TodoApiDTO/Controllers/TodoItemsController.cs @@ -1,7 +1,7 @@ using Microsoft.AspNetCore.Mvc; using System.Collections.Generic; using System.Threading.Tasks; -using TodoApi.Models; +using TodoApiDTO.DTOs; using TodoApiDTO.Services.Interfaces; namespace TodoApi.Controllers @@ -34,14 +34,9 @@ public async Task> Get(long id) } [HttpPut("{id}")] - public async Task> Update(long id, TodoItemDTO todoItemDTO) + public async Task> Update(long id, CreateUpdateItemTodoDTO createUpdateDTO) { - if (id != todoItemDTO.Id) - { - return BadRequest(); - } - - var isFound = await _todoService.Update(id, todoItemDTO); + var isFound = await _todoService.Update(id, createUpdateDTO); if (isFound == false) { @@ -52,9 +47,9 @@ public async Task> Update(long id, TodoItemDTO todoItemDTO) } [HttpPost] - public async Task> Create(TodoItemDTO todoItemDTO) + public async Task> Create(CreateUpdateItemTodoDTO createUpdateDTO) { - var todo = await _todoService.Create(todoItemDTO); + var todo = await _todoService.Create(createUpdateDTO); return CreatedAtAction(nameof(Get), new { id = todo.Id }, todo); } diff --git a/TodoApiDTO/DTOs/CreateUpdateItemTodoDTO.cs b/TodoApiDTO/DTOs/CreateUpdateItemTodoDTO.cs new file mode 100644 index 00000000..b3b6d683 --- /dev/null +++ b/TodoApiDTO/DTOs/CreateUpdateItemTodoDTO.cs @@ -0,0 +1,8 @@ +namespace TodoApiDTO.DTOs +{ + public class CreateUpdateItemTodoDTO + { + public string Name { get; set; } + public bool IsComplete { get; set; } + } +} diff --git a/TodoApiDTO/Models/TodoItemDTO.cs b/TodoApiDTO/DTOs/TodoItemDTO.cs similarity index 85% rename from TodoApiDTO/Models/TodoItemDTO.cs rename to TodoApiDTO/DTOs/TodoItemDTO.cs index 0083606f..5ce0e5ce 100644 --- a/TodoApiDTO/Models/TodoItemDTO.cs +++ b/TodoApiDTO/DTOs/TodoItemDTO.cs @@ -1,4 +1,4 @@ -namespace TodoApi.Models +namespace TodoApiDTO.DTOs { public class TodoItemDTO { diff --git a/TodoApiDTO/Mappers/TodoProfile.cs b/TodoApiDTO/Mappers/TodoProfile.cs index a7ac29cb..200d016e 100644 --- a/TodoApiDTO/Mappers/TodoProfile.cs +++ b/TodoApiDTO/Mappers/TodoProfile.cs @@ -1,5 +1,6 @@ using AutoMapper; using TodoApi.Models; +using TodoApiDTO.DTOs; namespace GeekStore.API.Core.Configurations { @@ -10,6 +11,8 @@ public TodoProfile() CreateMap() .ForMember(x => x.Secret, opt => opt.Ignore()) .ReverseMap(); + + CreateMap(); } } } \ No newline at end of file diff --git a/TodoApiDTO/Repositories/Interfaces/ITodoRepository.cs b/TodoApiDTO/Repositories/Interfaces/ITodoRepository.cs index c5930a7a..d27a3757 100644 --- a/TodoApiDTO/Repositories/Interfaces/ITodoRepository.cs +++ b/TodoApiDTO/Repositories/Interfaces/ITodoRepository.cs @@ -1,15 +1,15 @@ using System.Collections.Generic; using System.Threading.Tasks; -using TodoApi.Models; +using TodoApiDTO.DTOs; namespace TodoApiDTO.Repositories.Interfaces { public interface ITodoRepository { - public Task> GetAll(); - public Task Get(long id); - public Task Update(long id, TodoItem todoItem); - public Task Create(TodoItem todoItem); - public Task Delete(TodoItem todoItem); + public Task> GetAll(); + public Task Get(long id); + public Task Update(long id, CreateUpdateItemTodoDTO todoItem); + public Task Create(CreateUpdateItemTodoDTO todoItem); + public Task Delete(long id); } } diff --git a/TodoApiDTO/Repositories/TodoRepository.cs b/TodoApiDTO/Repositories/TodoRepository.cs index baa3ccaf..d761b17c 100644 --- a/TodoApiDTO/Repositories/TodoRepository.cs +++ b/TodoApiDTO/Repositories/TodoRepository.cs @@ -1,35 +1,45 @@ -using Microsoft.EntityFrameworkCore; +using AutoMapper; +using Microsoft.EntityFrameworkCore; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using TodoApi.Models; +using TodoApiDTO.DTOs; using TodoApiDTO.Repositories.Interfaces; namespace TodoApiDTO.Repositories { public class TodoRepository : ITodoRepository { - private TodoContext _context; + private readonly TodoContext _context; + private readonly IMapper _mapper; - public TodoRepository(TodoContext context) + public TodoRepository(TodoContext context, IMapper mapper) { _context = context; + _mapper = mapper; } - public async Task> GetAll() + public async Task> GetAll() { - return await _context.TodoItems.ToListAsync(); + var todos = await _context.TodoItems.ToListAsync(); + var dtos = todos.Select(todo => _mapper.Map(todo)); + return dtos; } - public async Task Get(long id) + public async Task Get(long id) { - return await _context.TodoItems.FindAsync(id); + return _mapper.Map(await GetById(id)); } - public async Task Update(long id, TodoItem todoItem) + public async Task Update(long id, CreateUpdateItemTodoDTO createUpdateDTO) { try { + var todoItem = await GetById(id); + if (todoItem == null) + return false; + _mapper.Map(createUpdateDTO, todoItem); await _context.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) when (!TodoItemExists(id)) @@ -38,20 +48,32 @@ public async Task Update(long id, TodoItem todoItem) } return true; } - public async Task Create(TodoItem todoItem) + public async Task Create(CreateUpdateItemTodoDTO createUpdateDTO) { + var todoItem = _mapper.Map(createUpdateDTO); + _context.TodoItems.Add(todoItem); await _context.SaveChangesAsync(); - return todoItem; + return _mapper.Map(todoItem); } - public async Task Delete(TodoItem todoItem) + public async Task Delete(long id) { + var todoItem = await GetById(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 async Task GetById(long id) => await _context.TodoItems.FindAsync(id); + private bool TodoItemExists(long id) => _context.TodoItems.Any(todo => todo.Id == id); } } diff --git a/TodoApiDTO/Services/Interfaces/ITodoService.cs b/TodoApiDTO/Services/Interfaces/ITodoService.cs index dccae3f5..ca25195b 100644 --- a/TodoApiDTO/Services/Interfaces/ITodoService.cs +++ b/TodoApiDTO/Services/Interfaces/ITodoService.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; using System.Threading.Tasks; -using TodoApi.Models; +using TodoApiDTO.DTOs; namespace TodoApiDTO.Services.Interfaces { @@ -8,8 +8,8 @@ public interface ITodoService { public Task> GetAll(); public Task Get(long id); - public Task Update(long id, TodoItemDTO todoItemDTO); - public Task Create(TodoItemDTO todoItemDTO); + public Task Update(long id, CreateUpdateItemTodoDTO createUpdateItemTodo); + public Task Create(CreateUpdateItemTodoDTO createUpdateItemTodo); public Task Delete(long id); } } diff --git a/TodoApiDTO/Services/TodoService.cs b/TodoApiDTO/Services/TodoService.cs index d05d7413..5f49d764 100644 --- a/TodoApiDTO/Services/TodoService.cs +++ b/TodoApiDTO/Services/TodoService.cs @@ -1,8 +1,7 @@ using AutoMapper; using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; -using TodoApi.Models; +using TodoApiDTO.DTOs; using TodoApiDTO.Repositories.Interfaces; using TodoApiDTO.Services.Interfaces; @@ -11,55 +10,35 @@ namespace TodoApiDTO.Services public class TodoService : ITodoService { private readonly ITodoRepository _repository; - private readonly IMapper _mapper; - public TodoService(ITodoRepository repository, IMapper mapper) + public TodoService(ITodoRepository repository) { _repository = repository; - _mapper = mapper; } public async Task> GetAll() { - var todos = await _repository.GetAll(); - var dtos = todos.Select(todo => _mapper.Map(todo)); - return dtos; + return await _repository.GetAll(); } public async Task Get(long id) { - var todo = await _repository.Get(id); - return _mapper.Map(todo); + return await _repository.Get(id); } - public async Task Update(long id, TodoItemDTO todoItemDTO) + public async Task Update(long id, CreateUpdateItemTodoDTO createUpdateDTO) { - var todoItem = await _repository.Get(id); - if (todoItem == null) - return false; - todoItem = _mapper.Map(todoItemDTO); - return await _repository.Update(id, todoItem); + return await _repository.Update(id, createUpdateDTO); } - public async Task Create(TodoItemDTO todoItemDTO) + public async Task Create(CreateUpdateItemTodoDTO createUpdateDTO) { - var todoItem = _mapper.Map(todoItemDTO); - - var todo = await _repository.Create(todoItem); - return _mapper.Map(todo); + return await _repository.Create(createUpdateDTO); } public async Task Delete(long id) { - var todoItem = await _repository.Get(id); - - if (todoItem == null) - { - return false; - } - await _repository.Delete(todoItem); - - return true; + return await _repository.Delete(id); } } } From 350c9ce2b827df394073656fea834fc2d99b58f4 Mon Sep 17 00:00:00 2001 From: eugenegritsina Date: Mon, 19 Jun 2023 02:33:16 +0400 Subject: [PATCH 5/9] feat: add TodoService tests --- Tests/Tests.csproj | 2 +- Tests/TodoRepositoryTests.cs | 13 ------ Tests/TodoServiceTests.cs | 79 ++++++++++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+), 14 deletions(-) delete mode 100644 Tests/TodoRepositoryTests.cs create mode 100644 Tests/TodoServiceTests.cs diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj index 099c728c..8fc3f147 100644 --- a/Tests/Tests.csproj +++ b/Tests/Tests.csproj @@ -10,8 +10,8 @@ - + diff --git a/Tests/TodoRepositoryTests.cs b/Tests/TodoRepositoryTests.cs deleted file mode 100644 index 2bbc14e7..00000000 --- a/Tests/TodoRepositoryTests.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Tests -{ - [TestFixture] - public class TodoRepositoryTests - { - } -} diff --git a/Tests/TodoServiceTests.cs b/Tests/TodoServiceTests.cs new file mode 100644 index 00000000..c18585c4 --- /dev/null +++ b/Tests/TodoServiceTests.cs @@ -0,0 +1,79 @@ +using AutoMapper; +using GeekStore.API.Core.Configurations; +using Moq; +using TodoApi.Models; +using TodoApiDTO.ApiConstans; +using TodoApiDTO.DTOs; +using TodoApiDTO.Repositories.Interfaces; +using TodoApiDTO.Services; + +namespace Tests +{ + [TestFixture] + public class TodoServiceTests + { + private readonly IMapper _mapper; + private readonly TodoService _service; + private readonly Mock _repository; + + public TodoServiceTests() + { + _repository = new Mock(); + + var todoProfile = new TodoProfile(); + var configuration = new MapperConfiguration(cfg => cfg.AddProfile(todoProfile)); + _mapper = new Mapper(configuration); + + _service = new TodoService(_repository.Object, _mapper); + } + + [Test] + public void UpdateSuccessTest() + { + // Arrange + var dto = new CreateUpdateItemTodoDTO { Name = "update", IsComplete = false }; + _repository.Setup(r => r.Update(It.IsAny(), It.IsAny())) + .ReturnsAsync(ApiRequestStatus.Success); + + // Act + var result = _service.Update(123, dto); + + // Assert + Assert.AreEqual(ApiRequestStatus.Success, result.Result); + } + + [Test] + public void UpdateIncorrectIdTest() + { + // Arrange + var dto = new CreateUpdateItemTodoDTO { Name = "update", IsComplete = false }; + _repository.Setup(r => r.Update(It.IsAny(), It.IsAny())) + .ReturnsAsync(ApiRequestStatus.ItemDoesntExist); + + // Act + var result = _service.Update(123, null); + + // Assert + Assert.AreEqual(ApiRequestStatus.ItemDoesntExist, result.Result); + } + + [Test] + public void CreateSuccessTest() + { + // Arrange + var dto = new CreateUpdateItemTodoDTO { Name = "create", IsComplete = false }; + var expectedTodo = new TodoItem { Name = dto.Name, IsComplete = dto.IsComplete }; + _repository.Setup(r => r.Create(It.IsAny())) + .ReturnsAsync(expectedTodo); + + // Act + var result = _service.Create(dto); + + // Assert + var modelAfterUpdate = result.Result; + Assert.NotNull(modelAfterUpdate); + Assert.AreEqual(dto.IsComplete, modelAfterUpdate.IsComplete); + Assert.AreEqual(dto.Name, modelAfterUpdate.Name); + } + } +} From 5541bde40299c22570b178edb495bafd1605db05 Mon Sep 17 00:00:00 2001 From: eugenegritsina Date: Mon, 19 Jun 2023 02:37:27 +0400 Subject: [PATCH 6/9] refactor: move mapping logic back to service + add request status enums + remove --- Tests/TodoServiceTests.cs | 8 +-- TodoApiDTO/ApiConstans/ApiResponseStatus.cs | 12 +++++ TodoApiDTO/Controllers/TodoItemsController.cs | 27 +++++----- .../Extentions/EnumDescriptionExtention.cs | 18 +++++++ .../Interfaces/ITodoRepository.cs | 13 ++--- TodoApiDTO/Repositories/TodoRepository.cs | 52 +++++++++---------- .../Services/Interfaces/ITodoService.cs | 5 +- TodoApiDTO/Services/TodoService.cs | 28 ++++++---- 8 files changed, 101 insertions(+), 62 deletions(-) create mode 100644 TodoApiDTO/ApiConstans/ApiResponseStatus.cs create mode 100644 TodoApiDTO/Extentions/EnumDescriptionExtention.cs diff --git a/Tests/TodoServiceTests.cs b/Tests/TodoServiceTests.cs index c18585c4..6d4685f8 100644 --- a/Tests/TodoServiceTests.cs +++ b/Tests/TodoServiceTests.cs @@ -33,13 +33,13 @@ public void UpdateSuccessTest() // Arrange var dto = new CreateUpdateItemTodoDTO { Name = "update", IsComplete = false }; _repository.Setup(r => r.Update(It.IsAny(), It.IsAny())) - .ReturnsAsync(ApiRequestStatus.Success); + .ReturnsAsync(ApiResponseStatus.Success); // Act var result = _service.Update(123, dto); // Assert - Assert.AreEqual(ApiRequestStatus.Success, result.Result); + Assert.AreEqual(ApiResponseStatus.Success, result.Result); } [Test] @@ -48,13 +48,13 @@ public void UpdateIncorrectIdTest() // Arrange var dto = new CreateUpdateItemTodoDTO { Name = "update", IsComplete = false }; _repository.Setup(r => r.Update(It.IsAny(), It.IsAny())) - .ReturnsAsync(ApiRequestStatus.ItemDoesntExist); + .ReturnsAsync(ApiResponseStatus.ItemDoesntExist); // Act var result = _service.Update(123, null); // Assert - Assert.AreEqual(ApiRequestStatus.ItemDoesntExist, result.Result); + Assert.AreEqual(ApiResponseStatus.ItemDoesntExist, result.Result); } [Test] diff --git a/TodoApiDTO/ApiConstans/ApiResponseStatus.cs b/TodoApiDTO/ApiConstans/ApiResponseStatus.cs new file mode 100644 index 00000000..9153ed4d --- /dev/null +++ b/TodoApiDTO/ApiConstans/ApiResponseStatus.cs @@ -0,0 +1,12 @@ +using System.ComponentModel; + +namespace TodoApiDTO.ApiConstans +{ + public enum ApiResponseStatus + { + [Description("Success")] + Success, + [Description("Item does not exist")] + ItemDoesntExist, + } +} diff --git a/TodoApiDTO/Controllers/TodoItemsController.cs b/TodoApiDTO/Controllers/TodoItemsController.cs index 3874e76f..45acba81 100644 --- a/TodoApiDTO/Controllers/TodoItemsController.cs +++ b/TodoApiDTO/Controllers/TodoItemsController.cs @@ -1,7 +1,8 @@ using Microsoft.AspNetCore.Mvc; -using System.Collections.Generic; using System.Threading.Tasks; +using TodoApiDTO.ApiConstans; using TodoApiDTO.DTOs; +using TodoApiDTO.Extentions; using TodoApiDTO.Services.Interfaces; namespace TodoApi.Controllers @@ -18,36 +19,36 @@ public TodoItemsController(ITodoService todoService) } [HttpGet] - public async Task>> GetAll() => Ok(await _todoService.GetAll()); + public async Task GetAll() => Ok(await _todoService.GetAll()); [HttpGet("{id}")] - public async Task> Get(long id) + public async Task Get(long id) { var todoItem = await _todoService.Get(id); if (todoItem == null) { - return NotFound(); + return NotFound(ApiResponseStatus.ItemDoesntExist.GetEnumDescription()); } - return todoItem; + return Ok(todoItem); } [HttpPut("{id}")] - public async Task> Update(long id, CreateUpdateItemTodoDTO createUpdateDTO) + public async Task Update(long id, CreateUpdateItemTodoDTO createUpdateDTO) { - var isFound = await _todoService.Update(id, createUpdateDTO); + var response = await _todoService.Update(id, createUpdateDTO); - if (isFound == false) + if (response == ApiResponseStatus.ItemDoesntExist) { - return NotFound(); + return NotFound(response.GetEnumDescription()); } return NoContent(); } [HttpPost] - public async Task> Create(CreateUpdateItemTodoDTO createUpdateDTO) + public async Task Create(CreateUpdateItemTodoDTO createUpdateDTO) { var todo = await _todoService.Create(createUpdateDTO); @@ -57,11 +58,11 @@ public async Task> Create(CreateUpdateItemTodoDTO crea [HttpDelete("{id}")] public async Task Delete(long id) { - var isFound = await _todoService.Delete(id); + var response = await _todoService.Delete(id); - if (isFound == false) + if (response == ApiResponseStatus.ItemDoesntExist) { - return NotFound(); + return NotFound(response.GetEnumDescription()); } return NoContent(); diff --git a/TodoApiDTO/Extentions/EnumDescriptionExtention.cs b/TodoApiDTO/Extentions/EnumDescriptionExtention.cs new file mode 100644 index 00000000..a1ffacde --- /dev/null +++ b/TodoApiDTO/Extentions/EnumDescriptionExtention.cs @@ -0,0 +1,18 @@ +using System; +using System.ComponentModel; + +namespace TodoApiDTO.Extentions +{ + public static class EnumDescriptionExtention + { + public static string GetEnumDescription(this Enum enumValue) + { + var field = enumValue.GetType().GetField(enumValue.ToString()); + if (Attribute.GetCustomAttribute(field, typeof(DescriptionAttribute)) is DescriptionAttribute attribute) + { + return attribute.Description; + } + throw new ArgumentException("Item not found.", nameof(enumValue)); + } + } +} diff --git a/TodoApiDTO/Repositories/Interfaces/ITodoRepository.cs b/TodoApiDTO/Repositories/Interfaces/ITodoRepository.cs index d27a3757..ecfb1100 100644 --- a/TodoApiDTO/Repositories/Interfaces/ITodoRepository.cs +++ b/TodoApiDTO/Repositories/Interfaces/ITodoRepository.cs @@ -1,15 +1,16 @@ using System.Collections.Generic; using System.Threading.Tasks; -using TodoApiDTO.DTOs; +using TodoApi.Models; +using TodoApiDTO.ApiConstans; namespace TodoApiDTO.Repositories.Interfaces { public interface ITodoRepository { - public Task> GetAll(); - public Task Get(long id); - public Task Update(long id, CreateUpdateItemTodoDTO todoItem); - public Task Create(CreateUpdateItemTodoDTO todoItem); - public Task Delete(long id); + public Task> GetAll(); + public Task Get(long id); + public Task Update(long id, TodoItem todoItem); + public Task Create(TodoItem todoItem); + public Task Delete(long id); } } diff --git a/TodoApiDTO/Repositories/TodoRepository.cs b/TodoApiDTO/Repositories/TodoRepository.cs index d761b17c..bdd116fa 100644 --- a/TodoApiDTO/Repositories/TodoRepository.cs +++ b/TodoApiDTO/Repositories/TodoRepository.cs @@ -1,10 +1,9 @@ -using AutoMapper; -using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using TodoApi.Models; -using TodoApiDTO.DTOs; +using TodoApiDTO.ApiConstans; using TodoApiDTO.Repositories.Interfaces; namespace TodoApiDTO.Repositories @@ -12,68 +11,65 @@ namespace TodoApiDTO.Repositories public class TodoRepository : ITodoRepository { private readonly TodoContext _context; - private readonly IMapper _mapper; - public TodoRepository(TodoContext context, IMapper mapper) + public TodoRepository(TodoContext context) { _context = context; - _mapper = mapper; } - public async Task> GetAll() + public async Task> GetAll() { - var todos = await _context.TodoItems.ToListAsync(); - var dtos = todos.Select(todo => _mapper.Map(todo)); - return dtos; + return await _context.TodoItems.ToListAsync(); } - public async Task Get(long id) + public async Task Get(long id) { - return _mapper.Map(await GetById(id)); + return await _context.TodoItems.FindAsync(id); } - public async Task Update(long id, CreateUpdateItemTodoDTO createUpdateDTO) + public async Task Update(long id, TodoItem model) { try { - var todoItem = await GetById(id); + var todoItem = await Get(id); if (todoItem == null) - return false; - _mapper.Map(createUpdateDTO, todoItem); + { + return ApiResponseStatus.ItemDoesntExist; + } + todoItem.Name = model.Name; + todoItem.IsComplete = model.IsComplete; + await _context.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) when (!TodoItemExists(id)) { - return false; + return ApiResponseStatus.ItemDoesntExist; } - return true; + return ApiResponseStatus.Success; } - public async Task Create(CreateUpdateItemTodoDTO createUpdateDTO) + public async Task Create(TodoItem model) { - var todoItem = _mapper.Map(createUpdateDTO); - - _context.TodoItems.Add(todoItem); + _context.TodoItems.Add(model); await _context.SaveChangesAsync(); - return _mapper.Map(todoItem); + return model; } - public async Task Delete(long id) + public async Task Delete(long id) { - var todoItem = await GetById(id); + var todoItem = await Get(id); if (todoItem == null) { - return false; + return ApiResponseStatus.ItemDoesntExist; } _context.TodoItems.Remove(todoItem); await _context.SaveChangesAsync(); - return true; + return ApiResponseStatus.Success; } - private async Task GetById(long id) => await _context.TodoItems.FindAsync(id); private bool TodoItemExists(long id) => _context.TodoItems.Any(todo => todo.Id == id); } } diff --git a/TodoApiDTO/Services/Interfaces/ITodoService.cs b/TodoApiDTO/Services/Interfaces/ITodoService.cs index ca25195b..7b8dc08d 100644 --- a/TodoApiDTO/Services/Interfaces/ITodoService.cs +++ b/TodoApiDTO/Services/Interfaces/ITodoService.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Threading.Tasks; +using TodoApiDTO.ApiConstans; using TodoApiDTO.DTOs; namespace TodoApiDTO.Services.Interfaces @@ -8,8 +9,8 @@ public interface ITodoService { public Task> GetAll(); public Task Get(long id); - public Task Update(long id, CreateUpdateItemTodoDTO createUpdateItemTodo); + public Task Update(long id, CreateUpdateItemTodoDTO createUpdateItemTodo); public Task Create(CreateUpdateItemTodoDTO createUpdateItemTodo); - public Task Delete(long id); + public Task Delete(long id); } } diff --git a/TodoApiDTO/Services/TodoService.cs b/TodoApiDTO/Services/TodoService.cs index 5f49d764..73804406 100644 --- a/TodoApiDTO/Services/TodoService.cs +++ b/TodoApiDTO/Services/TodoService.cs @@ -1,6 +1,9 @@ using AutoMapper; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; +using TodoApi.Models; +using TodoApiDTO.ApiConstans; using TodoApiDTO.DTOs; using TodoApiDTO.Repositories.Interfaces; using TodoApiDTO.Services.Interfaces; @@ -10,33 +13,40 @@ namespace TodoApiDTO.Services public class TodoService : ITodoService { private readonly ITodoRepository _repository; + private readonly IMapper _mapper; - public TodoService(ITodoRepository repository) + public TodoService(ITodoRepository repository, IMapper mapper) { - _repository = repository; + _repository = repository; + _mapper = mapper; } public async Task> GetAll() { - return await _repository.GetAll(); + var todos = await _repository.GetAll(); + return todos.Select(todo => _mapper.Map(todo)); } public async Task Get(long id) { - return await _repository.Get(id); + return _mapper.Map(await _repository.Get(id)); } - public async Task Update(long id, CreateUpdateItemTodoDTO createUpdateDTO) - { - return await _repository.Update(id, createUpdateDTO); + public async Task Update(long id, CreateUpdateItemTodoDTO createUpdateDTO) + { + var todoItem = _mapper.Map(createUpdateDTO); + + return await _repository.Update(id, todoItem); } public async Task Create(CreateUpdateItemTodoDTO createUpdateDTO) { - return await _repository.Create(createUpdateDTO); + var todoItem = _mapper.Map(createUpdateDTO); + + return _mapper.Map(await _repository.Create(todoItem)); } - public async Task Delete(long id) + public async Task Delete(long id) { return await _repository.Delete(id); } From c7b77d59b149d935c9009a95e68b94384fd89a5f Mon Sep 17 00:00:00 2001 From: eugenegritsina Date: Mon, 19 Jun 2023 02:37:44 +0400 Subject: [PATCH 7/9] refactor: remove unused packages --- TodoApiDTO/TodoApiDTO.csproj | 6 ------ 1 file changed, 6 deletions(-) diff --git a/TodoApiDTO/TodoApiDTO.csproj b/TodoApiDTO/TodoApiDTO.csproj index 02edcef4..544c810e 100644 --- a/TodoApiDTO/TodoApiDTO.csproj +++ b/TodoApiDTO/TodoApiDTO.csproj @@ -14,13 +14,7 @@ - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - From 67265782ee14ccf2b2d042be4d0e0f0c6adaf2c4 Mon Sep 17 00:00:00 2001 From: eugenegritsina Date: Mon, 19 Jun 2023 14:03:58 +0400 Subject: [PATCH 8/9] doc: readme (+1 squashed commits) Squashed commits: [b74e83e] doc: readme --- TodoApiDTO/README.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/TodoApiDTO/README.md b/TodoApiDTO/README.md index 466e41fd..a7ee5083 100644 --- a/TodoApiDTO/README.md +++ b/TodoApiDTO/README.md @@ -1 +1,15 @@ -# TodoService \ No newline at end of file +# TodoService + +Why I added CreateUpdateItemTodoDTO? +- To avoid the scenario when user types different ids in body and route parameter, simply avoiding that + +Why didn't I test all the methods in the service? +- methods I didn't test are too simple, thay only use mapper (which is tested) and call repo which calls EF, so it's like testing EF + +Possible scaling changes: +1) in case if TodoService will need to talk to more than one repo, it is good to add database service to store all of them +2) if project grows up it's good to separate DAL and BLL to separate projects, like I did with Tests + +P.S. +I've passed a real mapper instead of mocked one to service tests, because mapper is already tested and +mocking Automapper makes to sence since its aim is to reduce dumb mapping code From 9169e19c3764323406ae0e84598c7735fd55a7b4 Mon Sep 17 00:00:00 2001 From: eugenegritsina Date: Sun, 25 Jun 2023 15:12:59 +0400 Subject: [PATCH 9/9] refactor: add task description to readme --- TodoApiDTO/README.md => README.md | 6 ++++++ 1 file changed, 6 insertions(+) rename TodoApiDTO/README.md => README.md (79%) diff --git a/TodoApiDTO/README.md b/README.md similarity index 79% rename from TodoApiDTO/README.md rename to README.md index a7ee5083..caacadd9 100644 --- a/TodoApiDTO/README.md +++ b/README.md @@ -1,5 +1,11 @@ # TodoService +TASK DESCRIPTION: +1) Add swagger +2) Store list of tasks in SQL Server (initially InMemoryDB is used) +3) Do refactoring: extract Data Access Layer, Business Layer +4) Write unit tests for business operations + Why I added CreateUpdateItemTodoDTO? - To avoid the scenario when user types different ids in body and route parameter, simply avoiding that