From cfbb16c9967391acaab23dfd273958d6bc099fbc Mon Sep 17 00:00:00 2001 From: mablakulova Date: Sat, 5 Aug 2023 05:03:20 +0500 Subject: [PATCH 01/10] renamed, structured folders and added DAL, BLL layers --- Controllers/TodoItemsController.cs | 116 ------------------ Models/TodoContext.cs | 14 --- Models/TodoItem.cs | 12 -- Models/TodoItemDTO.cs | 11 -- Program.cs | 26 ---- Startup.cs | 55 --------- Todo.BLL/Todo.BLL.csproj | 9 ++ Todo.DAL/Todo.DAL.csproj | 9 ++ Todo.sln | 46 +++++++ Todo/Program.cs | 11 ++ .../Properties}/launchSettings.json | 24 ++-- Todo/Todo.csproj | 9 ++ Todo/appsettings.Development.json | 8 ++ appsettings.json => Todo/appsettings.json | 3 +- TodoApiDTO.csproj | 18 --- TodoApiDTO.sln | 25 ---- appsettings.Development.json | 9 -- 17 files changed, 107 insertions(+), 298 deletions(-) delete mode 100644 Controllers/TodoItemsController.cs delete mode 100644 Models/TodoContext.cs delete mode 100644 Models/TodoItem.cs delete mode 100644 Models/TodoItemDTO.cs delete mode 100644 Program.cs delete mode 100644 Startup.cs create mode 100644 Todo.BLL/Todo.BLL.csproj create mode 100644 Todo.DAL/Todo.DAL.csproj create mode 100644 Todo.sln create mode 100644 Todo/Program.cs rename {Properties => Todo/Properties}/launchSettings.json (57%) create mode 100644 Todo/Todo.csproj create mode 100644 Todo/appsettings.Development.json rename appsettings.json => Todo/appsettings.json (56%) delete mode 100644 TodoApiDTO.csproj delete mode 100644 TodoApiDTO.sln delete mode 100644 appsettings.Development.json diff --git a/Controllers/TodoItemsController.cs b/Controllers/TodoItemsController.cs deleted file mode 100644 index 0ef138e7..00000000 --- a/Controllers/TodoItemsController.cs +++ /dev/null @@ -1,116 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using TodoApi.Models; - -namespace TodoApi.Controllers -{ - [Route("api/[controller]")] - [ApiController] - public class TodoItemsController : ControllerBase - { - private readonly TodoContext _context; - - public TodoItemsController(TodoContext context) - { - _context = context; - } - - [HttpGet] - public async Task>> GetTodoItems() - { - return await _context.TodoItems - .Select(x => ItemToDTO(x)) - .ToListAsync(); - } - - [HttpGet("{id}")] - public async Task> GetTodoItem(long id) - { - var todoItem = await _context.TodoItems.FindAsync(id); - - if (todoItem == null) - { - return NotFound(); - } - - return ItemToDTO(todoItem); - } - - [HttpPut("{id}")] - public async Task UpdateTodoItem(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; - - try - { - await _context.SaveChangesAsync(); - } - catch (DbUpdateConcurrencyException) when (!TodoItemExists(id)) - { - return NotFound(); - } - - return NoContent(); - } - - [HttpPost] - public async Task> CreateTodoItem(TodoItemDTO todoItemDTO) - { - var todoItem = new TodoItem - { - IsComplete = todoItemDTO.IsComplete, - Name = todoItemDTO.Name - }; - - _context.TodoItems.Add(todoItem); - await _context.SaveChangesAsync(); - - return CreatedAtAction( - nameof(GetTodoItem), - new { id = todoItem.Id }, - ItemToDTO(todoItem)); - } - - [HttpDelete("{id}")] - public async Task DeleteTodoItem(long id) - { - var todoItem = await _context.TodoItems.FindAsync(id); - - if (todoItem == null) - { - 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/Models/TodoContext.cs deleted file mode 100644 index 6e59e363..00000000 --- a/Models/TodoContext.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Microsoft.EntityFrameworkCore; - -namespace TodoApi.Models -{ - public class TodoContext : DbContext - { - public TodoContext(DbContextOptions options) - : base(options) - { - } - - public DbSet TodoItems { get; set; } - } -} \ No newline at end of file diff --git a/Models/TodoItem.cs b/Models/TodoItem.cs deleted file mode 100644 index 1f6e5465..00000000 --- a/Models/TodoItem.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace TodoApi.Models -{ - #region snippet - public class TodoItem - { - public long Id { get; set; } - public string Name { get; set; } - 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 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/Program.cs b/Program.cs deleted file mode 100644 index b27ac16a..00000000 --- a/Program.cs +++ /dev/null @@ -1,26 +0,0 @@ -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 -{ - public class Program - { - public static void Main(string[] args) - { - CreateHostBuilder(args).Build().Run(); - } - - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - }); - } -} diff --git a/Startup.cs b/Startup.cs deleted file mode 100644 index bbfbc83d..00000000 --- a/Startup.cs +++ /dev/null @@ -1,55 +0,0 @@ -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; - -namespace TodoApi -{ - public class Startup - { - public Startup(IConfiguration configuration) - { - Configuration = configuration; - } - - public IConfiguration Configuration { get; } - - // 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(); - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - - app.UseHttpsRedirection(); - - app.UseRouting(); - - app.UseAuthorization(); - - app.UseEndpoints(endpoints => - { - endpoints.MapControllers(); - }); - } - } -} diff --git a/Todo.BLL/Todo.BLL.csproj b/Todo.BLL/Todo.BLL.csproj new file mode 100644 index 00000000..132c02c5 --- /dev/null +++ b/Todo.BLL/Todo.BLL.csproj @@ -0,0 +1,9 @@ + + + + net6.0 + enable + enable + + + diff --git a/Todo.DAL/Todo.DAL.csproj b/Todo.DAL/Todo.DAL.csproj new file mode 100644 index 00000000..132c02c5 --- /dev/null +++ b/Todo.DAL/Todo.DAL.csproj @@ -0,0 +1,9 @@ + + + + net6.0 + enable + enable + + + diff --git a/Todo.sln b/Todo.sln new file mode 100644 index 00000000..a621b540 --- /dev/null +++ b/Todo.sln @@ -0,0 +1,46 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.6.33723.286 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{F1C2A150-F045-49E3-B04A-28E49DAFCA74}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{AE374853-4D57-49A8-A80B-8D905FB916A1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Todo", "Todo\Todo.csproj", "{9FD16944-03B3-4470-848D-F9295C30BB5A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Todo.BLL", "Todo.BLL\Todo.BLL.csproj", "{E05D0E88-CC60-493C-B559-E8B8403AF0DC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Todo.DAL", "Todo.DAL\Todo.DAL.csproj", "{919C9C3A-C976-459F-939A-FA87759D4FE6}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {9FD16944-03B3-4470-848D-F9295C30BB5A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9FD16944-03B3-4470-848D-F9295C30BB5A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9FD16944-03B3-4470-848D-F9295C30BB5A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9FD16944-03B3-4470-848D-F9295C30BB5A}.Release|Any CPU.Build.0 = Release|Any CPU + {E05D0E88-CC60-493C-B559-E8B8403AF0DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E05D0E88-CC60-493C-B559-E8B8403AF0DC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E05D0E88-CC60-493C-B559-E8B8403AF0DC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E05D0E88-CC60-493C-B559-E8B8403AF0DC}.Release|Any CPU.Build.0 = Release|Any CPU + {919C9C3A-C976-459F-939A-FA87759D4FE6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {919C9C3A-C976-459F-939A-FA87759D4FE6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {919C9C3A-C976-459F-939A-FA87759D4FE6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {919C9C3A-C976-459F-939A-FA87759D4FE6}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {9FD16944-03B3-4470-848D-F9295C30BB5A} = {F1C2A150-F045-49E3-B04A-28E49DAFCA74} + {E05D0E88-CC60-493C-B559-E8B8403AF0DC} = {F1C2A150-F045-49E3-B04A-28E49DAFCA74} + {919C9C3A-C976-459F-939A-FA87759D4FE6} = {F1C2A150-F045-49E3-B04A-28E49DAFCA74} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {60984BC9-01D1-4B40-9733-E459B1231F96} + EndGlobalSection +EndGlobal diff --git a/Todo/Program.cs b/Todo/Program.cs new file mode 100644 index 00000000..c721f6fb --- /dev/null +++ b/Todo/Program.cs @@ -0,0 +1,11 @@ +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. + +var app = builder.Build(); + +// Configure the HTTP request pipeline. + +app.UseHttpsRedirection(); + +app.Run(); \ No newline at end of file diff --git a/Properties/launchSettings.json b/Todo/Properties/launchSettings.json similarity index 57% rename from Properties/launchSettings.json rename to Todo/Properties/launchSettings.json index 6766196a..00624a7b 100644 --- a/Properties/launchSettings.json +++ b/Todo/Properties/launchSettings.json @@ -1,27 +1,31 @@ -{ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", "iisSettings": { "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { - "applicationUrl": "http://localhost:56416/", - "sslPort": 44331 + "applicationUrl": "http://localhost:60930", + "sslPort": 44360 } }, "profiles": { - "IIS Express": { - "commandName": "IISExpress", + "Todo": { + "commandName": "Project", + "dotnetRunMessages": true, "launchBrowser": true, + "launchUrl": "weatherforecast", + "applicationUrl": "https://localhost:7111;http://localhost:5281", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, - "TodoApiDTO": { - "commandName": "Project", + "IIS Express": { + "commandName": "IISExpress", "launchBrowser": true, + "launchUrl": "weatherforecast", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" - }, - "applicationUrl": "https://localhost:5001;http://localhost:5000" + } } } -} \ No newline at end of file +} diff --git a/Todo/Todo.csproj b/Todo/Todo.csproj new file mode 100644 index 00000000..c78c9c7e --- /dev/null +++ b/Todo/Todo.csproj @@ -0,0 +1,9 @@ + + + + net6.0 + enable + enable + + + diff --git a/Todo/appsettings.Development.json b/Todo/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/Todo/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/appsettings.json b/Todo/appsettings.json similarity index 56% rename from appsettings.json rename to Todo/appsettings.json index d9d9a9bf..10f68b8c 100644 --- a/appsettings.json +++ b/Todo/appsettings.json @@ -2,8 +2,7 @@ "Logging": { "LogLevel": { "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" + "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*" diff --git a/TodoApiDTO.csproj b/TodoApiDTO.csproj deleted file mode 100644 index bba6f6af..00000000 --- a/TodoApiDTO.csproj +++ /dev/null @@ -1,18 +0,0 @@ - - - - netcoreapp3.1 - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - diff --git a/TodoApiDTO.sln b/TodoApiDTO.sln deleted file mode 100644 index e49c182b..00000000 --- a/TodoApiDTO.sln +++ /dev/null @@ -1,25 +0,0 @@ - -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}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {623124F9-F5BA-42DD-BC26-A1720774229C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {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 - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {60984BC9-01D1-4B40-9733-E459B1231F96} - EndGlobalSection -EndGlobal diff --git a/appsettings.Development.json b/appsettings.Development.json deleted file mode 100644 index 8983e0fc..00000000 --- a/appsettings.Development.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" - } - } -} From d479f1de9ccb885585288b2ff6d474c8ea651828 Mon Sep 17 00:00:00 2001 From: mablakulova Date: Sat, 5 Aug 2023 05:16:06 +0500 Subject: [PATCH 02/10] added entity and dbcontext classes --- Todo.DAL/DbContexts/TodoDbContext.cs | 14 ++++++++++++++ Todo.DAL/Entities/TodoItem.cs | 12 ++++++++++++ Todo.DAL/Todo.DAL.csproj | 9 +++++++++ 3 files changed, 35 insertions(+) create mode 100644 Todo.DAL/DbContexts/TodoDbContext.cs create mode 100644 Todo.DAL/Entities/TodoItem.cs diff --git a/Todo.DAL/DbContexts/TodoDbContext.cs b/Todo.DAL/DbContexts/TodoDbContext.cs new file mode 100644 index 00000000..d3ed20ad --- /dev/null +++ b/Todo.DAL/DbContexts/TodoDbContext.cs @@ -0,0 +1,14 @@ +using Microsoft.EntityFrameworkCore; +using Todo.DAL.Entities; + +namespace Todo.DAL.DbContexts; + +public class TodoDbContext : DbContext +{ + public TodoDbContext(DbContextOptions options) + : base(options) + { + } + + public DbSet TodoItems { get; set; } +} diff --git a/Todo.DAL/Entities/TodoItem.cs b/Todo.DAL/Entities/TodoItem.cs new file mode 100644 index 00000000..9eb3bd74 --- /dev/null +++ b/Todo.DAL/Entities/TodoItem.cs @@ -0,0 +1,12 @@ +namespace Todo.DAL.Entities; + +public class TodoItem +{ + public Guid Id { get; set; } + + public string Name { get; set; } + + public bool IsComplete { get; set; } + + public string? Secret { get; set; } +} diff --git a/Todo.DAL/Todo.DAL.csproj b/Todo.DAL/Todo.DAL.csproj index 132c02c5..71552da6 100644 --- a/Todo.DAL/Todo.DAL.csproj +++ b/Todo.DAL/Todo.DAL.csproj @@ -6,4 +6,13 @@ enable + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + From 3b34fe9c2c3a3b3f86b7572e906ce480938580b7 Mon Sep 17 00:00:00 2001 From: mablakulova Date: Sat, 5 Aug 2023 05:19:03 +0500 Subject: [PATCH 03/10] added repository classes --- Todo.DAL/IRepository.cs | 18 ++++++++++++++++++ Todo.DAL/Repository.cs | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 Todo.DAL/IRepository.cs create mode 100644 Todo.DAL/Repository.cs diff --git a/Todo.DAL/IRepository.cs b/Todo.DAL/IRepository.cs new file mode 100644 index 00000000..15f93d23 --- /dev/null +++ b/Todo.DAL/IRepository.cs @@ -0,0 +1,18 @@ +using System.Linq.Expressions; + +namespace Todo.DAL; + +public interface IRepository where T : class +{ + Task> GetAll(bool trackChanges); + + Task GetByCondition(Expression> expression, bool trackChanges); + + Task SaveChanges(); + + void Create(T entity); + + void Update(T entity); + + void Delete(T entity); +} diff --git a/Todo.DAL/Repository.cs b/Todo.DAL/Repository.cs new file mode 100644 index 00000000..e7670248 --- /dev/null +++ b/Todo.DAL/Repository.cs @@ -0,0 +1,36 @@ +using Microsoft.EntityFrameworkCore; +using System.Linq.Expressions; +using Todo.DAL.DbContexts; + +namespace Todo.DAL; + +public class Repository : IRepository where T : class +{ + private readonly TodoDbContext _dbContext; + + public Repository(TodoDbContext dbContext) + => _dbContext = dbContext; + + public async Task> GetAll(bool trackChanges) + { + IQueryable query = !trackChanges ? _dbContext.Set().AsNoTracking() : _dbContext.Set(); + + return await query.ToListAsync(); + } + + public async Task GetByCondition(Expression> expression, bool trackChanges) + { + IQueryable query = !trackChanges ? _dbContext.Set().Where(expression).AsNoTracking() + : _dbContext.Set().Where(expression); + + return await query.FirstOrDefaultAsync(); + } + + public void Create(T entity) => _dbContext.Set().Add(entity); + + public void Update(T entity) => _dbContext.Set().Update(entity); + + public void Delete(T entity) => _dbContext.Set().Remove(entity); + + public async Task SaveChanges() => await _dbContext.SaveChangesAsync(); +} From 98b70abf64832e190ba3bce289b231cad9b61363 Mon Sep 17 00:00:00 2001 From: mablakulova Date: Sat, 5 Aug 2023 05:26:10 +0500 Subject: [PATCH 04/10] added (I)TodoItemService business logic classes --- Todo.BLL/Interfaces/ITodoItemService.cs | 16 +++++++++ Todo.BLL/Services/TodoItemService.cs | 43 +++++++++++++++++++++++++ Todo.BLL/Todo.BLL.csproj | 4 +++ 3 files changed, 63 insertions(+) create mode 100644 Todo.BLL/Interfaces/ITodoItemService.cs create mode 100644 Todo.BLL/Services/TodoItemService.cs diff --git a/Todo.BLL/Interfaces/ITodoItemService.cs b/Todo.BLL/Interfaces/ITodoItemService.cs new file mode 100644 index 00000000..4570b261 --- /dev/null +++ b/Todo.BLL/Interfaces/ITodoItemService.cs @@ -0,0 +1,16 @@ +using Todo.DAL.Entities; + +namespace Todo.BLL.Interfaces; + +public interface ITodoItemService +{ + Task> GetTodoItemsAsync(bool trackChanges); + + Task GetTodoItemAsync(Guid todoItemId, bool trackChanges); + + Task CreateToDoItemAsync(TodoItem todoItem); + + Task UpdateTodoItemAsync(TodoItem todoItem); + + Task DeleteTodoItemAsync(TodoItem todoItem); +} diff --git a/Todo.BLL/Services/TodoItemService.cs b/Todo.BLL/Services/TodoItemService.cs new file mode 100644 index 00000000..ab0d7248 --- /dev/null +++ b/Todo.BLL/Services/TodoItemService.cs @@ -0,0 +1,43 @@ +using Todo.DAL.Entities; +using Todo.DAL; +using Todo.BLL.Interfaces; + +namespace Todo.BLL.Services; + +public class TodoItemService : ITodoItemService +{ + private readonly IRepository _repository; + + public TodoItemService(IRepository repository) => _repository = repository; + + public async Task> GetTodoItemsAsync(bool trackChanges) + { + return await _repository.GetAll(trackChanges); + } + + public async Task GetTodoItemAsync(Guid todoItemId, bool trackChanges) + { + return await _repository.GetByCondition(l => l.Id == todoItemId, trackChanges); + } + + public async Task CreateToDoItemAsync(TodoItem todoItem) + { + _repository.Create(todoItem); + + await _repository.SaveChanges(); + } + + public async Task DeleteTodoItemAsync(TodoItem todoItem) + { + _repository.Delete(todoItem); + + await _repository.SaveChanges(); + } + + public async Task UpdateTodoItemAsync(TodoItem todoItem) + { + _repository.Update(todoItem); + + await _repository.SaveChanges(); + } +} diff --git a/Todo.BLL/Todo.BLL.csproj b/Todo.BLL/Todo.BLL.csproj index 132c02c5..49c4d696 100644 --- a/Todo.BLL/Todo.BLL.csproj +++ b/Todo.BLL/Todo.BLL.csproj @@ -6,4 +6,8 @@ enable + + + + From 38cc3ca8a7e1d9debf5deaeb59c504c5badee81b Mon Sep 17 00:00:00 2001 From: mablakulova Date: Sat, 5 Aug 2023 05:51:21 +0500 Subject: [PATCH 05/10] created dto classes and controller for crud operations --- Todo/Controllers/TodoItemsController.cs | 67 +++++++++++++++++++++++++ Todo/Dtos/TodoItemDto.cs | 3 ++ Todo/Dtos/TodoItemForCreationDto.cs | 3 ++ Todo/Dtos/TodoItemForManipulationDto.cs | 12 +++++ Todo/Dtos/TodoItemForUpdateDto.cs | 3 ++ 5 files changed, 88 insertions(+) create mode 100644 Todo/Controllers/TodoItemsController.cs create mode 100644 Todo/Dtos/TodoItemDto.cs create mode 100644 Todo/Dtos/TodoItemForCreationDto.cs create mode 100644 Todo/Dtos/TodoItemForManipulationDto.cs create mode 100644 Todo/Dtos/TodoItemForUpdateDto.cs diff --git a/Todo/Controllers/TodoItemsController.cs b/Todo/Controllers/TodoItemsController.cs new file mode 100644 index 00000000..b8874156 --- /dev/null +++ b/Todo/Controllers/TodoItemsController.cs @@ -0,0 +1,67 @@ +using AutoMapper; +using Microsoft.AspNetCore.Mvc; +using Todo.BLL.Interfaces; +using Todo.DAL.Entities; +using Todo.Dtos; + +namespace Todo.API.Controllers; + +[Route("api/[controller]")] +[ApiController] +public class TodoItemsController : ControllerBase +{ + private readonly ITodoItemService _service; + private readonly IMapper _mapper; + + public TodoItemsController(ITodoItemService service, IMapper mapper) + { + _service = service; + _mapper = mapper; + } + + [HttpGet("getAll")] + public async Task>> GetTodoItems() + { + var todoItems = await _service.GetTodoItemsAsync(trackChanges: false); + + return Ok(_mapper.Map>(todoItems)); + } + + [HttpGet("{todoItemId:guid}", Name = "TodoItemById")] + public async Task> GetTodoItem(Guid todoItemId) + { + var todoItem = await _service.GetTodoItemAsync(todoItemId, trackChanges: false); + + return Ok(_mapper.Map(todoItem)); + } + + [HttpPost("create")] + public async Task CreateTodoItem([FromBody] TodoItemForCreationDto itemDto) + { + var todoItemEntity = _mapper.Map(itemDto); + await _service.CreateToDoItemAsync(todoItemEntity); + + var createdTodoItem = _mapper.Map(todoItemEntity); + return CreatedAtRoute("TodoItemById", new { todoItemId = createdTodoItem.Id }, createdTodoItem); + } + + [HttpPut("{todoItemId:guid}")] + public async Task UpdateTodoItem(Guid todoItemId, [FromBody] TodoItemForUpdateDto itemDto) + { + var todoItem = await _service.GetTodoItemAsync(todoItemId, trackChanges: false); + _mapper.Map(itemDto, todoItem); + + await _service.UpdateTodoItemAsync(todoItem); + + return NoContent(); + } + + [HttpDelete("{todoItemId:guid}")] + public async Task DeleteTodoItem(Guid todoItemId) + { + var todoItem = await _service.GetTodoItemAsync(todoItemId, trackChanges: false); + await _service.DeleteTodoItemAsync(todoItem); + + return NoContent(); + } +} diff --git a/Todo/Dtos/TodoItemDto.cs b/Todo/Dtos/TodoItemDto.cs new file mode 100644 index 00000000..c53fd3fe --- /dev/null +++ b/Todo/Dtos/TodoItemDto.cs @@ -0,0 +1,3 @@ +namespace Todo.Dtos; + +public record TodoItemDto(Guid Id, string Name, bool IsComplete); \ No newline at end of file diff --git a/Todo/Dtos/TodoItemForCreationDto.cs b/Todo/Dtos/TodoItemForCreationDto.cs new file mode 100644 index 00000000..a2de78b0 --- /dev/null +++ b/Todo/Dtos/TodoItemForCreationDto.cs @@ -0,0 +1,3 @@ +namespace Todo.Dtos; + +public record TodoItemForCreationDto : TodoItemForManipulationDto; diff --git a/Todo/Dtos/TodoItemForManipulationDto.cs b/Todo/Dtos/TodoItemForManipulationDto.cs new file mode 100644 index 00000000..583bf420 --- /dev/null +++ b/Todo/Dtos/TodoItemForManipulationDto.cs @@ -0,0 +1,12 @@ +using System.ComponentModel.DataAnnotations; + +namespace Todo.Dtos; + +public abstract record TodoItemForManipulationDto +{ + [Required(ErrorMessage = "Name is a required field.")] + public string Name { get; init; } + + [Required(ErrorMessage = "IsComplete is a required field.")] + public bool IsComplete { get; init; } +} diff --git a/Todo/Dtos/TodoItemForUpdateDto.cs b/Todo/Dtos/TodoItemForUpdateDto.cs new file mode 100644 index 00000000..37869ac8 --- /dev/null +++ b/Todo/Dtos/TodoItemForUpdateDto.cs @@ -0,0 +1,3 @@ +namespace Todo.Dtos; + +public record TodoItemForUpdateDto : TodoItemForManipulationDto; \ No newline at end of file From 6d8a87b466e824c9b6ed24d5e487777301c17190 Mon Sep 17 00:00:00 2001 From: mablakulova Date: Sat, 5 Aug 2023 05:52:31 +0500 Subject: [PATCH 06/10] added swagger, mapped dtos and configured extensions --- Todo/Extensions/ServiceExtensions.cs | 28 ++++++++++++++++++++++++++++ Todo/MappingProfile.cs | 17 +++++++++++++++++ Todo/Program.cs | 27 +++++++++++++++++++++++++-- Todo/Properties/launchSettings.json | 4 ++-- Todo/Todo.csproj | 10 ++++++++++ Todo/appsettings.json | 3 +++ 6 files changed, 85 insertions(+), 4 deletions(-) create mode 100644 Todo/Extensions/ServiceExtensions.cs create mode 100644 Todo/MappingProfile.cs diff --git a/Todo/Extensions/ServiceExtensions.cs b/Todo/Extensions/ServiceExtensions.cs new file mode 100644 index 00000000..568cefbb --- /dev/null +++ b/Todo/Extensions/ServiceExtensions.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.OpenApi.Models; +using Todo.BLL.Interfaces; +using Todo.BLL.Services; +using Todo.DAL; +using Todo.DAL.DbContexts; +using Todo.DAL.Entities; + +namespace Todo.Extensions; + +public static class ServiceExtensions +{ + public static void ConfigureRepositories(this IServiceCollection services) => + services.AddScoped, Repository>(); + + public static void ConfigureSqlContext(this IServiceCollection services, IConfiguration configuration) => + services.AddDbContext(opts => + opts.UseSqlServer(configuration.GetConnectionString("SqlConnection"))); + + public static void ConfigureServices(this IServiceCollection services) => + services.AddScoped(); + + public static void ConfigureSwagger(this IServiceCollection services) => + services.AddSwaggerGen(options => + { + options.SwaggerDoc("v1", new OpenApiInfo { Title = "Todo", Version = "v1" }); + }); +} diff --git a/Todo/MappingProfile.cs b/Todo/MappingProfile.cs new file mode 100644 index 00000000..4ef8a9b5 --- /dev/null +++ b/Todo/MappingProfile.cs @@ -0,0 +1,17 @@ +using AutoMapper; +using Todo.Dtos; +using Todo.DAL.Entities; + +namespace Todo; + +public class MappingProfile : Profile +{ + public MappingProfile() + { + CreateMap(); + + CreateMap(); + + CreateMap().ReverseMap(); + } +} diff --git a/Todo/Program.cs b/Todo/Program.cs index c721f6fb..d95f8504 100644 --- a/Todo/Program.cs +++ b/Todo/Program.cs @@ -1,11 +1,34 @@ +using Todo.Extensions; + var builder = WebApplication.CreateBuilder(args); -// Add services to the container. +builder.Services.ConfigureRepositories(); +builder.Services.ConfigureServices(); + +builder.Services.ConfigureSqlContext(builder.Configuration); + +builder.Services.AddAutoMapper(typeof(Program)); + +builder.Services.ConfigureSwagger(); +builder.Services.AddControllers(); var app = builder.Build(); -// Configure the HTTP request pipeline. +if (app.Environment.IsProduction()) + app.UseHsts(); app.UseHttpsRedirection(); +app.UseStaticFiles(); + +app.UseAuthentication(); +app.UseAuthorization(); + +app.UseSwagger(); +app.UseSwaggerUI(s => +{ + s.SwaggerEndpoint("/swagger/v1/swagger.json", "Todo v1"); +}); + +app.MapControllers(); app.Run(); \ No newline at end of file diff --git a/Todo/Properties/launchSettings.json b/Todo/Properties/launchSettings.json index 00624a7b..acda0447 100644 --- a/Todo/Properties/launchSettings.json +++ b/Todo/Properties/launchSettings.json @@ -13,7 +13,7 @@ "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, - "launchUrl": "weatherforecast", + "launchUrl": "swagger", "applicationUrl": "https://localhost:7111;http://localhost:5281", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" @@ -22,7 +22,7 @@ "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, - "launchUrl": "weatherforecast", + "launchUrl": "swagger", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/Todo/Todo.csproj b/Todo/Todo.csproj index c78c9c7e..09560f8e 100644 --- a/Todo/Todo.csproj +++ b/Todo/Todo.csproj @@ -6,4 +6,14 @@ enable + + + + + + + + + + diff --git a/Todo/appsettings.json b/Todo/appsettings.json index 10f68b8c..518cf1f7 100644 --- a/Todo/appsettings.json +++ b/Todo/appsettings.json @@ -5,5 +5,8 @@ "Microsoft.AspNetCore": "Warning" } }, + "ConnectionStrings": { + "SqlConnection": "Server=DESKTOP-OTFC9HN;Database=todoDB;Trusted_Connection=True;MultipleActiveResultSets=true" + }, "AllowedHosts": "*" } From bbb363a873716cccc437d937e80103d518022e9a Mon Sep 17 00:00:00 2001 From: mablakulova Date: Sat, 5 Aug 2023 07:57:43 +0500 Subject: [PATCH 07/10] connected with database --- .../20230805025418_Initial.Designer.cs | 51 +++++++++++++++++++ Todo.DAL/Migrations/20230805025418_Initial.cs | 36 +++++++++++++ .../Migrations/TodoDbContextModelSnapshot.cs | 48 +++++++++++++++++ 3 files changed, 135 insertions(+) create mode 100644 Todo.DAL/Migrations/20230805025418_Initial.Designer.cs create mode 100644 Todo.DAL/Migrations/20230805025418_Initial.cs create mode 100644 Todo.DAL/Migrations/TodoDbContextModelSnapshot.cs diff --git a/Todo.DAL/Migrations/20230805025418_Initial.Designer.cs b/Todo.DAL/Migrations/20230805025418_Initial.Designer.cs new file mode 100644 index 00000000..5261c3a6 --- /dev/null +++ b/Todo.DAL/Migrations/20230805025418_Initial.Designer.cs @@ -0,0 +1,51 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Todo.DAL.DbContexts; + +#nullable disable + +namespace Todo.DAL.Migrations +{ + [DbContext(typeof(TodoDbContext))] + [Migration("20230805025418_Initial")] + partial class Initial + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.9") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Todo.DAL.Entities.TodoItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("IsComplete") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Secret") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("TodoItems"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Todo.DAL/Migrations/20230805025418_Initial.cs b/Todo.DAL/Migrations/20230805025418_Initial.cs new file mode 100644 index 00000000..8a7d6f3f --- /dev/null +++ b/Todo.DAL/Migrations/20230805025418_Initial.cs @@ -0,0 +1,36 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Todo.DAL.Migrations +{ + /// + public partial class Initial : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "TodoItems", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + Name = table.Column(type: "nvarchar(max)", nullable: false), + IsComplete = table.Column(type: "bit", nullable: false), + Secret = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_TodoItems", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "TodoItems"); + } + } +} diff --git a/Todo.DAL/Migrations/TodoDbContextModelSnapshot.cs b/Todo.DAL/Migrations/TodoDbContextModelSnapshot.cs new file mode 100644 index 00000000..e609451f --- /dev/null +++ b/Todo.DAL/Migrations/TodoDbContextModelSnapshot.cs @@ -0,0 +1,48 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Todo.DAL.DbContexts; + +#nullable disable + +namespace Todo.DAL.Migrations +{ + [DbContext(typeof(TodoDbContext))] + partial class TodoDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.9") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Todo.DAL.Entities.TodoItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("IsComplete") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Secret") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("TodoItems"); + }); +#pragma warning restore 612, 618 + } + } +} From 6b6a1034414fa1ccfdab99a10bb8a9237e782a24 Mon Sep 17 00:00:00 2001 From: mablakulova Date: Sat, 5 Aug 2023 14:26:33 +0500 Subject: [PATCH 08/10] added exceptions and middleware extensions --- Todo/Controllers/TodoItemsController.cs | 18 +++++++--- Todo/Exceptions/ErrorDetails.cs | 11 ++++++ Todo/Exceptions/NotFoundException.cs | 8 +++++ .../TodoItem/TodoItemNotFoundException.cs | 9 +++++ .../ExceptionMiddlewareExtensions.cs | 34 +++++++++++++++++++ Todo/Program.cs | 3 +- Todo/Todo.csproj | 4 +++ 7 files changed, 82 insertions(+), 5 deletions(-) create mode 100644 Todo/Exceptions/ErrorDetails.cs create mode 100644 Todo/Exceptions/NotFoundException.cs create mode 100644 Todo/Exceptions/TodoItem/TodoItemNotFoundException.cs create mode 100644 Todo/Extensions/ExceptionMiddlewareExtensions.cs diff --git a/Todo/Controllers/TodoItemsController.cs b/Todo/Controllers/TodoItemsController.cs index b8874156..b07ef988 100644 --- a/Todo/Controllers/TodoItemsController.cs +++ b/Todo/Controllers/TodoItemsController.cs @@ -3,8 +3,9 @@ using Todo.BLL.Interfaces; using Todo.DAL.Entities; using Todo.Dtos; +using Todo.Exceptions.TodoItem; -namespace Todo.API.Controllers; +namespace Todo.Controllers; [Route("api/[controller]")] [ApiController] @@ -30,7 +31,7 @@ public async Task>> GetTodoItems() [HttpGet("{todoItemId:guid}", Name = "TodoItemById")] public async Task> GetTodoItem(Guid todoItemId) { - var todoItem = await _service.GetTodoItemAsync(todoItemId, trackChanges: false); + var todoItem = await GetTodoItemAndCheckIfItExists(todoItemId); return Ok(_mapper.Map(todoItem)); } @@ -48,7 +49,7 @@ public async Task CreateTodoItem([FromBody] TodoItemForCreationDt [HttpPut("{todoItemId:guid}")] public async Task UpdateTodoItem(Guid todoItemId, [FromBody] TodoItemForUpdateDto itemDto) { - var todoItem = await _service.GetTodoItemAsync(todoItemId, trackChanges: false); + var todoItem = await GetTodoItemAndCheckIfItExists(todoItemId); _mapper.Map(itemDto, todoItem); await _service.UpdateTodoItemAsync(todoItem); @@ -59,9 +60,18 @@ public async Task UpdateTodoItem(Guid todoItemId, [FromBody] Todo [HttpDelete("{todoItemId:guid}")] public async Task DeleteTodoItem(Guid todoItemId) { - var todoItem = await _service.GetTodoItemAsync(todoItemId, trackChanges: false); + var todoItem = await GetTodoItemAndCheckIfItExists(todoItemId); await _service.DeleteTodoItemAsync(todoItem); return NoContent(); } + + private async Task GetTodoItemAndCheckIfItExists(Guid id) + { + var todoItem = await _service.GetTodoItemAsync(id, trackChanges: false); + if (todoItem is null) + throw new TodoItemNotFoundException(id); + + return todoItem; + } } diff --git a/Todo/Exceptions/ErrorDetails.cs b/Todo/Exceptions/ErrorDetails.cs new file mode 100644 index 00000000..b3b2f90c --- /dev/null +++ b/Todo/Exceptions/ErrorDetails.cs @@ -0,0 +1,11 @@ +using System.Text.Json; + +namespace Todo.Exceptions; + +public class ErrorDetails +{ + public int StatusCode { get; set; } + public string? Message { get; set; } + + public override string ToString() => JsonSerializer.Serialize(this); +} diff --git a/Todo/Exceptions/NotFoundException.cs b/Todo/Exceptions/NotFoundException.cs new file mode 100644 index 00000000..a36d0034 --- /dev/null +++ b/Todo/Exceptions/NotFoundException.cs @@ -0,0 +1,8 @@ +namespace Todo.Exceptions; + +public abstract class NotFoundException : Exception +{ + protected NotFoundException(string message) + : base(message) + { } +} diff --git a/Todo/Exceptions/TodoItem/TodoItemNotFoundException.cs b/Todo/Exceptions/TodoItem/TodoItemNotFoundException.cs new file mode 100644 index 00000000..5650e8cb --- /dev/null +++ b/Todo/Exceptions/TodoItem/TodoItemNotFoundException.cs @@ -0,0 +1,9 @@ +namespace Todo.Exceptions.TodoItem; + +public class TodoItemNotFoundException : NotFoundException +{ + public TodoItemNotFoundException(Guid todoItemId) + : base($"TodoItem with id: {todoItemId} doesn't exist in the database.") + { + } +} diff --git a/Todo/Extensions/ExceptionMiddlewareExtensions.cs b/Todo/Extensions/ExceptionMiddlewareExtensions.cs new file mode 100644 index 00000000..2216ea4e --- /dev/null +++ b/Todo/Extensions/ExceptionMiddlewareExtensions.cs @@ -0,0 +1,34 @@ +using Microsoft.AspNetCore.Diagnostics; +using Todo.Exceptions; + +namespace Todo.Extensions; + +public static class ExceptionMiddlewareExtensions +{ + public static void ConfigureExceptionHandler(this WebApplication app) + { + app.UseExceptionHandler(appError => + { + appError.Run(async context => + { + context.Response.ContentType = "application/json"; + + var contextFeature = context.Features.Get(); + if (contextFeature != null) + { + context.Response.StatusCode = contextFeature.Error switch + { + NotFoundException => StatusCodes.Status404NotFound, + _ => StatusCodes.Status500InternalServerError + }; + + await context.Response.WriteAsync(new ErrorDetails() + { + StatusCode = context.Response.StatusCode, + Message = contextFeature.Error.Message, + }.ToString()); + } + }); + }); + } +} diff --git a/Todo/Program.cs b/Todo/Program.cs index d95f8504..33ec9b62 100644 --- a/Todo/Program.cs +++ b/Todo/Program.cs @@ -11,9 +11,10 @@ builder.Services.ConfigureSwagger(); builder.Services.AddControllers(); - var app = builder.Build(); +app.ConfigureExceptionHandler(); + if (app.Environment.IsProduction()) app.UseHsts(); diff --git a/Todo/Todo.csproj b/Todo/Todo.csproj index 09560f8e..f9eedc02 100644 --- a/Todo/Todo.csproj +++ b/Todo/Todo.csproj @@ -9,6 +9,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + From 68cf78ac93ac74bdf82c081d39fcc0271c1651d8 Mon Sep 17 00:00:00 2001 From: mablakulova Date: Sun, 6 Aug 2023 01:04:45 +0500 Subject: [PATCH 09/10] covered services and controllers with unit tests --- Todo.BLL.Tests/Todo.BLL.Tests.csproj | 30 ++++++ Todo.BLL.Tests/TodoItemServiceTests.cs | 103 ++++++++++++++++++ Todo.Tests/Todo.Tests.csproj | 31 ++++++ Todo.Tests/TodoItemsControllerTests.cs | 143 +++++++++++++++++++++++++ Todo.sln | 20 +++- 5 files changed, 324 insertions(+), 3 deletions(-) create mode 100644 Todo.BLL.Tests/Todo.BLL.Tests.csproj create mode 100644 Todo.BLL.Tests/TodoItemServiceTests.cs create mode 100644 Todo.Tests/Todo.Tests.csproj create mode 100644 Todo.Tests/TodoItemsControllerTests.cs diff --git a/Todo.BLL.Tests/Todo.BLL.Tests.csproj b/Todo.BLL.Tests/Todo.BLL.Tests.csproj new file mode 100644 index 00000000..403a614d --- /dev/null +++ b/Todo.BLL.Tests/Todo.BLL.Tests.csproj @@ -0,0 +1,30 @@ + + + + net6.0 + enable + enable + + false + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + diff --git a/Todo.BLL.Tests/TodoItemServiceTests.cs b/Todo.BLL.Tests/TodoItemServiceTests.cs new file mode 100644 index 00000000..80ee5f94 --- /dev/null +++ b/Todo.BLL.Tests/TodoItemServiceTests.cs @@ -0,0 +1,103 @@ +using Moq; +using Todo.BLL.Interfaces; +using Todo.BLL.Services; +using Todo.DAL; +using Todo.DAL.Entities; +using Xunit; + +namespace Todo.BLL.Tests; + +public class TodoItemServiceTests +{ + private readonly Mock> repositoryMock; + private readonly ITodoItemService todoItemService; + + public TodoItemServiceTests() + { + repositoryMock = new Mock>(); + todoItemService = new TodoItemService(repositoryMock.Object); + } + + [Fact] + public async Task GetTodoItemsAsync_ShouldReturnAllTodoItems() + { + // Arrange + repositoryMock.Setup(repo => repo.GetAll(false)).ReturnsAsync(GetTestTodoItems()); + + // Act + var todoItems = await todoItemService.GetTodoItemsAsync(false); + + // Assert + repositoryMock.Verify(t => t.GetAll(false)); + Assert.NotNull(todoItems); + } + + [Fact] + public async Task GetTodoItemAsync_ShouldReturnTodoItemById() + { + // Arrange + var todoItems = GetTestTodoItems(); + var id = todoItems[0].Id; + + repositoryMock.Setup(repo => repo.GetByCondition(l => l.Id == id, false)).ReturnsAsync(todoItems[0]); + + // Act + var result = await todoItemService.GetTodoItemAsync(id, false); + + // Assert + Assert.Equal(todoItems[0], result); + } + + [Fact] + public async Task CreateToDoItemAsync_ShouldAddNewTodoItem() + { + // Arrange + var todoItem = GetTestTodoItems()[0]; + + // Act + await todoItemService.CreateToDoItemAsync(todoItem); + + // Assert + repositoryMock.Verify(t => t.Create(It.IsAny())); + repositoryMock.Verify(t => t.SaveChanges()); + } + + [Fact] + public async Task UpdateTodoItemAsync_ShouldUpdateTodoItem_CallOnce() + { + // Arrange + var todoItem = GetTestTodoItems()[0]; + + // Act + await todoItemService.UpdateTodoItemAsync(todoItem); + + // Assert + repositoryMock.Verify(t => t.Update(todoItem), Times.Once); + repositoryMock.Verify(t => t.SaveChanges()); + } + + [Fact] + public async Task DeleteTodoItemAsync_ShouldDeleteTodoItem_CallOnce() + { + // Arrange + var todoItem = GetTestTodoItems()[0]; + + // Act + await todoItemService.DeleteTodoItemAsync(todoItem); + + // Assert + repositoryMock.Verify(t => t.Delete(todoItem), Times.Once); + repositoryMock.Verify(t => t.SaveChanges()); + } + + private List GetTestTodoItems() + { + return new List + { + new TodoItem { Id = Guid.NewGuid(), Name = "Buy groceries", IsComplete = false }, + new TodoItem { Id = Guid.NewGuid(), Name = "Do laundry", IsComplete = true }, + new TodoItem { Id = Guid.NewGuid(), Name = "Read a book", IsComplete = false } + }; + } + +} diff --git a/Todo.Tests/Todo.Tests.csproj b/Todo.Tests/Todo.Tests.csproj new file mode 100644 index 00000000..1ecaeb49 --- /dev/null +++ b/Todo.Tests/Todo.Tests.csproj @@ -0,0 +1,31 @@ + + + + net6.0 + enable + enable + + false + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + diff --git a/Todo.Tests/TodoItemsControllerTests.cs b/Todo.Tests/TodoItemsControllerTests.cs new file mode 100644 index 00000000..4cf85abb --- /dev/null +++ b/Todo.Tests/TodoItemsControllerTests.cs @@ -0,0 +1,143 @@ +using AutoMapper; +using Microsoft.AspNetCore.Mvc; +using Moq; +using Todo.BLL.Interfaces; +using Todo.Controllers; +using Todo.DAL.Entities; +using Todo.Dtos; +using Todo.Exceptions.TodoItem; +using Xunit; + +namespace Todo.Tests; + +public class TodoItemsControllerTests +{ + private static readonly Guid todoItemId = Guid.NewGuid(); + private readonly Mock mockMapper = new Mock(); + private readonly Mock mockTodoItemService = new Mock(); + + private readonly IReadOnlyList todoItems = + new List + { + new TodoItem { Id = todoItemId, Name = "Buy groceries", IsComplete = false }, + new TodoItem { Id = Guid.NewGuid(), Name = "Do homework", IsComplete = true }, + new TodoItem { Id = Guid.NewGuid(), Name = "Read a book", IsComplete = false } + }; + + private TodoItemsController todoItemsController; + + public TodoItemsControllerTests() + { + mockMapper.Setup(m => m.Map>(It.IsAny>())) + .Returns((List items) => items.Select(item => new TodoItemDto(item.Id, item.Name, item.IsComplete))); + + mockMapper.Setup(m => m.Map(It.IsAny())) + .Returns((TodoItem item) => new TodoItemDto(item.Id, item.Name, item.IsComplete)); + + mockTodoItemService.Setup(service => service.GetTodoItemsAsync(false)).ReturnsAsync(todoItems); + + mockTodoItemService.Setup(t => t.GetTodoItemAsync(todoItemId, false)).ReturnsAsync(todoItems[0]); + + todoItemsController = new TodoItemsController(mockTodoItemService.Object, mockMapper.Object); + } + + [Fact] + public async Task GetTodoItems_ShouldReturnOkAndCallOnce_WhenTodoItemsExist() + { + // Act + var result = await todoItemsController.GetTodoItems(); + + // Assert + Assert.IsType>>(result); + mockTodoItemService.Verify(mock => mock.GetTodoItemsAsync(false), Times.Once()); + } + + [Fact] + public async Task GetTodoItem_ShouldReturnOkAndCallOnce_WhenTodoItemExists() + { + // Act + var result = await todoItemsController.GetTodoItem(todoItemId); + + // Assert + Assert.IsType(result.Result); + mockTodoItemService.Verify(mock => mock.GetTodoItemAsync(todoItemId, false), Times.Once()); + } + + [Fact] + public async Task GetTodoItem_ShouldThrowException_WhenTodoItemDoesNotExist() + { + // Arrange + var mockTodoItemId = Guid.NewGuid(); + mockTodoItemService.Setup(t => t.GetTodoItemAsync(mockTodoItemId, false)).ReturnsAsync((TodoItem)null); + + // Act and Assert + await Assert.ThrowsAsync(() => todoItemsController.GetTodoItem(mockTodoItemId)); + } + + [Fact] + public async Task CreateTodoItem_ShouldReturnCreatedAtRouteResultWithCreatedTodoItem() + { + // Arrange + var mockCreationDto = new TodoItemForCreationDto { Name = "Write unit tests", IsComplete = false }; + var mockEntity = new TodoItem { Id = Guid.NewGuid(), Name = "Write unit tests", IsComplete = false }; + + mockMapper.Setup(m => m.Map(mockCreationDto)).Returns(mockEntity); + mockTodoItemService.Setup(s => s.CreateToDoItemAsync(mockEntity)).Returns(Task.CompletedTask); + + // Act + var result = await todoItemsController.CreateTodoItem(mockCreationDto); + + // Assert + Assert.IsType(result); + } + + [Fact] + public async Task UpdateTodoItem_ShouldReturnNoContentResult() + { + // Arrange + var updateItemDto = new TodoItemForUpdateDto { Name = "Do workout", IsComplete = true }; + var updateEntity = new TodoItem { Id = todoItemId, Name = "Do workout", IsComplete = true }; + + mockMapper.Setup(mapper => mapper.Map(updateItemDto, todoItems[0])).Returns(updateEntity); + mockTodoItemService.Setup(service => service.UpdateTodoItemAsync(updateEntity)).Returns(Task.CompletedTask); + + // Act + var result = await todoItemsController.UpdateTodoItem(todoItemId, updateItemDto); + + // Assert + Assert.IsType(result); + } + + [Fact] + public async Task UpdateTodoItem_ShouldThrowException_WhenTodoItemDoesNotExist() + { + // Arrange + var mockTodoItemId = Guid.NewGuid(); + var updateItemDto = new TodoItemForUpdateDto { Name = "Do workout", IsComplete = true }; + mockTodoItemService.Setup(t => t.GetTodoItemAsync(mockTodoItemId, false)).ReturnsAsync((TodoItem)null); + + // Act and Assert + await Assert.ThrowsAsync(() => todoItemsController.UpdateTodoItem(mockTodoItemId, updateItemDto)); + } + + [Fact] + public async Task DeleteTodoItem_ShouldReturnNoContentResult() + { + // Act + var result = await todoItemsController.DeleteTodoItem(todoItemId); + + // Assert + Assert.IsType(result); + } + + [Fact] + public async Task DeleteTodoItem_ShouldThrowException_WhenTodoItemDoesNotExist() + { + // Arrange + var mockTodoItemId = Guid.NewGuid(); + mockTodoItemService.Setup(t => t.GetTodoItemAsync(mockTodoItemId, false)).ReturnsAsync((TodoItem)null); + + // Act and Assert + await Assert.ThrowsAsync(() => todoItemsController.DeleteTodoItem(mockTodoItemId)); + } +} diff --git a/Todo.sln b/Todo.sln index a621b540..2b73233d 100644 --- a/Todo.sln +++ b/Todo.sln @@ -7,11 +7,15 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{F1C2A150-F04 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{AE374853-4D57-49A8-A80B-8D905FB916A1}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Todo", "Todo\Todo.csproj", "{9FD16944-03B3-4470-848D-F9295C30BB5A}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Todo", "Todo\Todo.csproj", "{9FD16944-03B3-4470-848D-F9295C30BB5A}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Todo.BLL", "Todo.BLL\Todo.BLL.csproj", "{E05D0E88-CC60-493C-B559-E8B8403AF0DC}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Todo.BLL", "Todo.BLL\Todo.BLL.csproj", "{E05D0E88-CC60-493C-B559-E8B8403AF0DC}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Todo.DAL", "Todo.DAL\Todo.DAL.csproj", "{919C9C3A-C976-459F-939A-FA87759D4FE6}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Todo.DAL", "Todo.DAL\Todo.DAL.csproj", "{919C9C3A-C976-459F-939A-FA87759D4FE6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Todo.BLL.Tests", "Todo.BLL.Tests\Todo.BLL.Tests.csproj", "{8794766B-E3C6-4C11-8A6D-B10074B54FE5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Todo.Tests", "Todo.Tests\Todo.Tests.csproj", "{8C68602A-D794-4726-8353-D3AD9CF24C30}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -31,6 +35,14 @@ Global {919C9C3A-C976-459F-939A-FA87759D4FE6}.Debug|Any CPU.Build.0 = Debug|Any CPU {919C9C3A-C976-459F-939A-FA87759D4FE6}.Release|Any CPU.ActiveCfg = Release|Any CPU {919C9C3A-C976-459F-939A-FA87759D4FE6}.Release|Any CPU.Build.0 = Release|Any CPU + {8794766B-E3C6-4C11-8A6D-B10074B54FE5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8794766B-E3C6-4C11-8A6D-B10074B54FE5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8794766B-E3C6-4C11-8A6D-B10074B54FE5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8794766B-E3C6-4C11-8A6D-B10074B54FE5}.Release|Any CPU.Build.0 = Release|Any CPU + {8C68602A-D794-4726-8353-D3AD9CF24C30}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8C68602A-D794-4726-8353-D3AD9CF24C30}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8C68602A-D794-4726-8353-D3AD9CF24C30}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8C68602A-D794-4726-8353-D3AD9CF24C30}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -39,6 +51,8 @@ Global {9FD16944-03B3-4470-848D-F9295C30BB5A} = {F1C2A150-F045-49E3-B04A-28E49DAFCA74} {E05D0E88-CC60-493C-B559-E8B8403AF0DC} = {F1C2A150-F045-49E3-B04A-28E49DAFCA74} {919C9C3A-C976-459F-939A-FA87759D4FE6} = {F1C2A150-F045-49E3-B04A-28E49DAFCA74} + {8794766B-E3C6-4C11-8A6D-B10074B54FE5} = {AE374853-4D57-49A8-A80B-8D905FB916A1} + {8C68602A-D794-4726-8353-D3AD9CF24C30} = {AE374853-4D57-49A8-A80B-8D905FB916A1} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {60984BC9-01D1-4B40-9733-E459B1231F96} From caa994833a203678b2c11bac87e219481223a189 Mon Sep 17 00:00:00 2001 From: mablakulova Date: Sun, 6 Aug 2023 01:49:10 +0500 Subject: [PATCH 10/10] updated readme, added changes description --- README.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 466e41fd..f6d6006a 100644 --- a/README.md +++ b/README.md @@ -1 +1,17 @@ -# TodoService \ No newline at end of file +## Todo application + +**Requirements and additional updates** +- BLL and DAL layers are added, main TodoApiDTO is renamed to Todo. +- Unit tests are written for Todo: controllers and Todo.BLL: services +- Added swagger and implemented MS SQL, appsettings.json are updated +- Implemented Automapper and repository pattern +- Added exceptions model for API error handling +- Updated TodoItemDTO model, added separate DTO models for create and update operations + +**Layers/folder structure and unit tests** + +![folders_tests](https://github.com/ebushuev/VelvetechTestTask/assets/84620072/c5aa40d2-5d64-4527-b1d7-fbed16207189) + +**Live application on Swagger** + +![swagger](https://github.com/ebushuev/VelvetechTestTask/assets/84620072/99a9f0e2-2cd0-4328-9af7-2d53f0269aff)