From 2b8ad37522ab9da5c8b8737cbf20a5803cd4a542 Mon Sep 17 00:00:00 2001 From: Alexander Slesarenko Date: Sun, 18 Jun 2023 10:43:13 +0500 Subject: [PATCH 1/4] Swagger added --- Properties/launchSettings.json | 3 ++- Startup.cs | 3 +++ TodoApiDTO.csproj | 1 + 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Properties/launchSettings.json b/Properties/launchSettings.json index 6766196a..1b835ad0 100644 --- a/Properties/launchSettings.json +++ b/Properties/launchSettings.json @@ -21,7 +21,8 @@ "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, - "applicationUrl": "https://localhost:5001;http://localhost:5000" + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "launchUrl": "https://localhost:5001/swagger/index.html" } } } \ No newline at end of file diff --git a/Startup.cs b/Startup.cs index bbfbc83d..d0439973 100644 --- a/Startup.cs +++ b/Startup.cs @@ -30,6 +30,7 @@ public void ConfigureServices(IServiceCollection services) services.AddDbContext(opt => opt.UseInMemoryDatabase("TodoList")); services.AddControllers(); + services.AddSwaggerGen(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. @@ -38,6 +39,8 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); + app.UseSwagger(); + app.UseSwaggerUI(); } app.UseHttpsRedirection(); diff --git a/TodoApiDTO.csproj b/TodoApiDTO.csproj index bba6f6af..6c186056 100644 --- a/TodoApiDTO.csproj +++ b/TodoApiDTO.csproj @@ -12,6 +12,7 @@ + From 903a186cc967b1930ec2686aae686a7ba52f04d4 Mon Sep 17 00:00:00 2001 From: Alexander Slesarenko Date: Sun, 18 Jun 2023 11:29:02 +0500 Subject: [PATCH 2/4] feat: use SQL Server DB instead of in-memory DB --- Startup.cs | 11 ++++++++++- appsettings.json | 5 ++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/Startup.cs b/Startup.cs index d0439973..83355822 100644 --- a/Startup.cs +++ b/Startup.cs @@ -28,7 +28,7 @@ public Startup(IConfiguration configuration) public void ConfigureServices(IServiceCollection services) { services.AddDbContext(opt => - opt.UseInMemoryDatabase("TodoList")); + opt.UseSqlServer(Configuration.GetConnectionString(Environment.MachineName))); services.AddControllers(); services.AddSwaggerGen(); } @@ -36,6 +36,8 @@ public void ConfigureServices(IServiceCollection services) // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { + EnsureDbCreated(app); + if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); @@ -54,5 +56,12 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) endpoints.MapControllers(); }); } + + private static void EnsureDbCreated(IApplicationBuilder app) + { + using var serviceScope = app.ApplicationServices.GetService().CreateScope(); + var context = serviceScope.ServiceProvider.GetRequiredService(); + context.Database.EnsureCreated(); + } } } diff --git a/appsettings.json b/appsettings.json index d9d9a9bf..8af75388 100644 --- a/appsettings.json +++ b/appsettings.json @@ -6,5 +6,8 @@ "Microsoft.Hosting.Lifetime": "Information" } }, - "AllowedHosts": "*" + "AllowedHosts": "*", + "ConnectionStrings": { + "DESKTOP-OQH3EOQ": "Data Source=DESKTOP-OQH3EOQ;Initial Catalog=VelvetechTestTask;Integrated Security=True;" + } } From bf5acd30401320bd1a780f39db6483b8c1958884 Mon Sep 17 00:00:00 2001 From: Alexander Slesarenko Date: Mon, 19 Jun 2023 11:58:37 +0500 Subject: [PATCH 3/4] feat: split DAL and BLL --- BLL/Exceptions/EntityNotFoundException.cs | 8 +++ BLL/ITodoItemManager.cs | 16 +++++ BLL/TodoItemManager.cs | 81 +++++++++++++++++++++++ Controllers/TodoItemsController.cs | 78 ++++------------------ DAL/Repositories/ITodoItemRepository.cs | 16 +++++ DAL/Repositories/TodoItemRepository.cs | 42 ++++++++++++ {Models => DAL}/TodoContext.cs | 3 +- ErrorHandlerMiddleware.cs | 43 ++++++++++++ Models/TodoItemDTO.cs | 2 +- Startup.cs | 10 ++- 10 files changed, 231 insertions(+), 68 deletions(-) create mode 100644 BLL/Exceptions/EntityNotFoundException.cs create mode 100644 BLL/ITodoItemManager.cs create mode 100644 BLL/TodoItemManager.cs create mode 100644 DAL/Repositories/ITodoItemRepository.cs create mode 100644 DAL/Repositories/TodoItemRepository.cs rename {Models => DAL}/TodoContext.cs (85%) create mode 100644 ErrorHandlerMiddleware.cs diff --git a/BLL/Exceptions/EntityNotFoundException.cs b/BLL/Exceptions/EntityNotFoundException.cs new file mode 100644 index 00000000..1f797650 --- /dev/null +++ b/BLL/Exceptions/EntityNotFoundException.cs @@ -0,0 +1,8 @@ +using System; + +namespace TodoApiDTO +{ + public class EntityNotFoundException : Exception + { + } +} diff --git a/BLL/ITodoItemManager.cs b/BLL/ITodoItemManager.cs new file mode 100644 index 00000000..75c2e4b5 --- /dev/null +++ b/BLL/ITodoItemManager.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using TodoApiDTO.Models; + +namespace TodoApiDTO.BLL +{ + public interface ITodoItemManager + { + Task> GetAllTodoItems(); + Task GetTodoItemById(long id); + Task UpdateTodoItem(long id, TodoItemDTO newTodoItem); + Task Create(TodoItemDTO todoItemDto); + Task DeleteTodoItem(long id); + } +} diff --git a/BLL/TodoItemManager.cs b/BLL/TodoItemManager.cs new file mode 100644 index 00000000..26e6f7a4 --- /dev/null +++ b/BLL/TodoItemManager.cs @@ -0,0 +1,81 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using TodoApi.Models; +using TodoApiDTO.DAL.Repositories; +using TodoApiDTO.Models; + +namespace TodoApiDTO.BLL +{ + public class TodoItemManager : ITodoItemManager + { + private readonly ITodoItemRepository _repository; + + public TodoItemManager(ITodoItemRepository repository) + { + _repository = repository; + } + + public async Task> GetAllTodoItems() + { + var items = await _repository.GetAll(); + return items.Select(ItemToDTO).ToList(); + } + + public async Task GetTodoItemById(long id) + { + var item = await _repository.GetById(id); + if (item == null) + { + throw new EntityNotFoundException(); + } + + return ItemToDTO(item); + } + + public async Task UpdateTodoItem(long id, TodoItemDTO newTodoItem) + { + var todoItem = await _repository.GetById(id); + if (todoItem == null) + { + throw new EntityNotFoundException(); + } + + todoItem.Name = newTodoItem.Name; + todoItem.IsComplete = newTodoItem.IsComplete; + + await _repository.SaveAsync(); + } + + public async Task Create(TodoItemDTO todoItemDto) + { + var todoItem = new TodoItem + { + IsComplete = todoItemDto.IsComplete, + Name = todoItemDto.Name + }; + await _repository.Create(todoItem); + await _repository.SaveAsync(); + return ItemToDTO(todoItem); + } + + public async Task DeleteTodoItem(long id) + { + var item = await _repository.GetById(id); + if (item == null) + { + throw new EntityNotFoundException(); + } + _repository.Delete(item); + await _repository.SaveAsync(); + } + + private static TodoItemDTO ItemToDTO(TodoItem todoItem) => + new TodoItemDTO + { + Id = todoItem.Id, + Name = todoItem.Name, + IsComplete = todoItem.IsComplete + }; + } +} diff --git a/Controllers/TodoItemsController.cs b/Controllers/TodoItemsController.cs index 0ef138e7..db0527a2 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.BLL; +using TodoApiDTO.Models; namespace TodoApi.Controllers { @@ -11,32 +10,24 @@ namespace TodoApi.Controllers [ApiController] public class TodoItemsController : ControllerBase { - private readonly TodoContext _context; + private readonly ITodoItemManager _todoManager; - public TodoItemsController(TodoContext context) + public TodoItemsController(ITodoItemManager todoManager) { - _context = context; + _todoManager = todoManager; } [HttpGet] public async Task>> GetTodoItems() { - return await _context.TodoItems - .Select(x => ItemToDTO(x)) - .ToListAsync(); + return Ok(await _todoManager.GetAllTodoItems()); } [HttpGet("{id}")] public async Task> GetTodoItem(long id) { - var todoItem = await _context.TodoItems.FindAsync(id); - - if (todoItem == null) - { - return NotFound(); - } - - return ItemToDTO(todoItem); + var todoItem = await _todoManager.GetTodoItemById(id); + return Ok(todoItem); } [HttpPut("{id}")] @@ -47,23 +38,7 @@ public async Task UpdateTodoItem(long id, TodoItemDTO todoItemDTO return BadRequest(); } - var todoItem = await _context.TodoItems.FindAsync(id); - if (todoItem == null) - { - return NotFound(); - } - - todoItem.Name = todoItemDTO.Name; - todoItem.IsComplete = todoItemDTO.IsComplete; - - try - { - await _context.SaveChangesAsync(); - } - catch (DbUpdateConcurrencyException) when (!TodoItemExists(id)) - { - return NotFound(); - } + await _todoManager.UpdateTodoItem(id, todoItemDTO); return NoContent(); } @@ -71,46 +46,19 @@ public async Task UpdateTodoItem(long id, TodoItemDTO todoItemDTO [HttpPost] public async Task> CreateTodoItem(TodoItemDTO todoItemDTO) { - var todoItem = new TodoItem - { - IsComplete = todoItemDTO.IsComplete, - Name = todoItemDTO.Name - }; - - _context.TodoItems.Add(todoItem); - await _context.SaveChangesAsync(); + var createdTodoItem = await _todoManager.Create(todoItemDTO); return CreatedAtAction( nameof(GetTodoItem), - new { id = todoItem.Id }, - ItemToDTO(todoItem)); + new { id = createdTodoItem.Id }, + createdTodoItem); } [HttpDelete("{id}")] public async Task DeleteTodoItem(long id) { - var todoItem = await _context.TodoItems.FindAsync(id); - - if (todoItem == null) - { - return NotFound(); - } - - _context.TodoItems.Remove(todoItem); - await _context.SaveChangesAsync(); - + await _todoManager.DeleteTodoItem(id); 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/DAL/Repositories/ITodoItemRepository.cs b/DAL/Repositories/ITodoItemRepository.cs new file mode 100644 index 00000000..17302a48 --- /dev/null +++ b/DAL/Repositories/ITodoItemRepository.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using TodoApi.Models; + +namespace TodoApiDTO.DAL.Repositories +{ + public interface ITodoItemRepository + { + public Task> GetAll(); + public Task GetById(long todoItemId); + public Task Create(TodoItem newItem); + + public void Delete(TodoItem item); + Task SaveAsync(); + } +} diff --git a/DAL/Repositories/TodoItemRepository.cs b/DAL/Repositories/TodoItemRepository.cs new file mode 100644 index 00000000..48074faf --- /dev/null +++ b/DAL/Repositories/TodoItemRepository.cs @@ -0,0 +1,42 @@ +using Microsoft.EntityFrameworkCore; +using System.Collections.Generic; +using System.Threading.Tasks; +using TodoApi.Models; + +namespace TodoApiDTO.DAL.Repositories +{ + public class TodoItemRepository : ITodoItemRepository + { + private readonly TodoContext _context; + + public TodoItemRepository(TodoContext context) + { + _context = context; + } + + public async Task> GetAll() + { + return await _context.TodoItems.ToListAsync(); + } + + public async Task GetById(long todoItemId) + { + return await _context.TodoItems.FirstOrDefaultAsync(x => x.Id == todoItemId); + } + + public async Task Create(TodoItem todoItem) + { + await _context.TodoItems.AddAsync(todoItem); + } + + public void Delete(TodoItem todoItem) + { + _context.TodoItems.Remove(todoItem); + } + + public async Task SaveAsync() + { + await _context.SaveChangesAsync(); + } + } +} diff --git a/Models/TodoContext.cs b/DAL/TodoContext.cs similarity index 85% rename from Models/TodoContext.cs rename to DAL/TodoContext.cs index 6e59e363..e5fa5d62 100644 --- a/Models/TodoContext.cs +++ b/DAL/TodoContext.cs @@ -1,6 +1,7 @@ using Microsoft.EntityFrameworkCore; +using TodoApi.Models; -namespace TodoApi.Models +namespace TodoApiDTO.DAL { public class TodoContext : DbContext { diff --git a/ErrorHandlerMiddleware.cs b/ErrorHandlerMiddleware.cs new file mode 100644 index 00000000..f7ede416 --- /dev/null +++ b/ErrorHandlerMiddleware.cs @@ -0,0 +1,43 @@ +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using System.ComponentModel.DataAnnotations; +using System.Net; +using System.Threading.Tasks; +using System; + +namespace TodoApiDTO +{ + public class ErrorHandlerMiddleware + { + private readonly RequestDelegate _next; + + public ErrorHandlerMiddleware(RequestDelegate next) + { + _next = next; + } + + public async Task Invoke(HttpContext context) + { + try + { + await _next(context); + } + catch (Exception error) + { + var response = context.Response; + if (error is EntityNotFoundException) + { + response.StatusCode = (int)HttpStatusCode.NotFound; + } + else + { + response.StatusCode = (int)HttpStatusCode.InternalServerError; + } + //await _next(context); + } + } + } +} diff --git a/Models/TodoItemDTO.cs b/Models/TodoItemDTO.cs index e66a500a..19cc2d88 100644 --- a/Models/TodoItemDTO.cs +++ b/Models/TodoItemDTO.cs @@ -1,4 +1,4 @@ -namespace TodoApi.Models +namespace TodoApiDTO.Models { #region snippet public class TodoItemDTO diff --git a/Startup.cs b/Startup.cs index 83355822..59e9dba4 100644 --- a/Startup.cs +++ b/Startup.cs @@ -11,7 +11,10 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using TodoApi.Models; +using TodoApiDTO; +using TodoApiDTO.BLL; +using TodoApiDTO.DAL; +using TodoApiDTO.DAL.Repositories; namespace TodoApi { @@ -31,6 +34,9 @@ public void ConfigureServices(IServiceCollection services) opt.UseSqlServer(Configuration.GetConnectionString(Environment.MachineName))); services.AddControllers(); services.AddSwaggerGen(); + + services.AddTransient(); + services.AddTransient(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. @@ -51,6 +57,8 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseAuthorization(); + app.UseMiddleware(); + app.UseEndpoints(endpoints => { endpoints.MapControllers(); From 3c7cde928166091bc1b199a5846ef4234293d165 Mon Sep 17 00:00:00 2001 From: Alexander Slesarenko Date: Mon, 19 Jun 2023 15:36:15 +0500 Subject: [PATCH 4/4] feat: logging errors into file --- BLL/Exceptions/EntityNotFoundException.cs | 2 +- BLL/TodoItemManager.cs | 1 + ErrorHandlerMiddleware.cs | 7 ++++-- Program.cs | 13 ++++++++++- TodoApiDTO.csproj | 1 + appsettings.json | 27 +++++++++++++++++++++++ 6 files changed, 47 insertions(+), 4 deletions(-) diff --git a/BLL/Exceptions/EntityNotFoundException.cs b/BLL/Exceptions/EntityNotFoundException.cs index 1f797650..ed0ad605 100644 --- a/BLL/Exceptions/EntityNotFoundException.cs +++ b/BLL/Exceptions/EntityNotFoundException.cs @@ -1,6 +1,6 @@ using System; -namespace TodoApiDTO +namespace TodoApiDTO.BLL.Exceptions { public class EntityNotFoundException : Exception { diff --git a/BLL/TodoItemManager.cs b/BLL/TodoItemManager.cs index 26e6f7a4..5d3581e3 100644 --- a/BLL/TodoItemManager.cs +++ b/BLL/TodoItemManager.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Threading.Tasks; using TodoApi.Models; +using TodoApiDTO.BLL.Exceptions; using TodoApiDTO.DAL.Repositories; using TodoApiDTO.Models; diff --git a/ErrorHandlerMiddleware.cs b/ErrorHandlerMiddleware.cs index f7ede416..f0be3c65 100644 --- a/ErrorHandlerMiddleware.cs +++ b/ErrorHandlerMiddleware.cs @@ -7,16 +7,19 @@ using System.Net; using System.Threading.Tasks; using System; +using TodoApiDTO.BLL.Exceptions; namespace TodoApiDTO { public class ErrorHandlerMiddleware { private readonly RequestDelegate _next; + private readonly ILogger _logger; - public ErrorHandlerMiddleware(RequestDelegate next) + public ErrorHandlerMiddleware(RequestDelegate next, ILogger logger) { _next = next; + _logger = logger; } public async Task Invoke(HttpContext context) @@ -27,6 +30,7 @@ public async Task Invoke(HttpContext context) } catch (Exception error) { + _logger.LogError(error, string.Empty); var response = context.Response; if (error is EntityNotFoundException) { @@ -36,7 +40,6 @@ public async Task Invoke(HttpContext context) { response.StatusCode = (int)HttpStatusCode.InternalServerError; } - //await _next(context); } } } diff --git a/Program.cs b/Program.cs index b27ac16a..d7c9b7a1 100644 --- a/Program.cs +++ b/Program.cs @@ -6,6 +6,9 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using NLog; +using NLog.Web; +using LogLevel = Microsoft.Extensions.Logging.LogLevel; namespace TodoApi { @@ -13,7 +16,9 @@ public class Program { public static void Main(string[] args) { + LogManager.Setup().LoadConfigurationFromAppSettings(); CreateHostBuilder(args).Build().Run(); + LogManager.Shutdown(); } public static IHostBuilder CreateHostBuilder(string[] args) => @@ -21,6 +26,12 @@ public static IHostBuilder CreateHostBuilder(string[] args) => .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); - }); + }) + .ConfigureLogging(logging => + { + logging.ClearProviders(); + logging.SetMinimumLevel(LogLevel.Information); + } + ).UseNLog(); } } diff --git a/TodoApiDTO.csproj b/TodoApiDTO.csproj index 6c186056..ab0d14af 100644 --- a/TodoApiDTO.csproj +++ b/TodoApiDTO.csproj @@ -12,6 +12,7 @@ + diff --git a/appsettings.json b/appsettings.json index 8af75388..0c389937 100644 --- a/appsettings.json +++ b/appsettings.json @@ -9,5 +9,32 @@ "AllowedHosts": "*", "ConnectionStrings": { "DESKTOP-OQH3EOQ": "Data Source=DESKTOP-OQH3EOQ;Initial Catalog=VelvetechTestTask;Integrated Security=True;" + }, + "NLog": { + "internalLogLevel": "Info", + "internalLogFile": "c:\\temp\\internal-nlog.txt", + "extensions": [ + { "assembly": "NLog.Extensions.Logging" }, + { "assembly": "NLog.Web.AspNetCore" } + ], + "targets": { + "errors": { + "type": "File", + "fileName": "${basedir}\\errors-${shortdate}.log", + "layout": "${longdate}|${event-properties:item=EventId_Id}|${uppercase:${level}}|${logger}|${message} ${exception:format=tostring}" + } + }, + "rules": [ + { + "logger": "*", + "minLevel": "Error", + "writeTo": "errors" + }, + { + "logger": "Microsoft.*", + "maxLevel": "Info", + "final": "true" + } + ] } }