diff --git a/BLL/Exceptions/EntityNotFoundException.cs b/BLL/Exceptions/EntityNotFoundException.cs new file mode 100644 index 00000000..ed0ad605 --- /dev/null +++ b/BLL/Exceptions/EntityNotFoundException.cs @@ -0,0 +1,8 @@ +using System; + +namespace TodoApiDTO.BLL.Exceptions +{ + 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..5d3581e3 --- /dev/null +++ b/BLL/TodoItemManager.cs @@ -0,0 +1,82 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using TodoApi.Models; +using TodoApiDTO.BLL.Exceptions; +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..f0be3c65 --- /dev/null +++ b/ErrorHandlerMiddleware.cs @@ -0,0 +1,46 @@ +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; +using TodoApiDTO.BLL.Exceptions; + +namespace TodoApiDTO +{ + public class ErrorHandlerMiddleware + { + private readonly RequestDelegate _next; + private readonly ILogger _logger; + + public ErrorHandlerMiddleware(RequestDelegate next, ILogger logger) + { + _next = next; + _logger = logger; + } + + public async Task Invoke(HttpContext context) + { + try + { + await _next(context); + } + catch (Exception error) + { + _logger.LogError(error, string.Empty); + var response = context.Response; + if (error is EntityNotFoundException) + { + response.StatusCode = (int)HttpStatusCode.NotFound; + } + else + { + response.StatusCode = (int)HttpStatusCode.InternalServerError; + } + } + } + } +} 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/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/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..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 { @@ -28,16 +31,24 @@ 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(); + + services.AddTransient(); + services.AddTransient(); } // 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(); + app.UseSwagger(); + app.UseSwaggerUI(); } app.UseHttpsRedirection(); @@ -46,10 +57,19 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseAuthorization(); + app.UseMiddleware(); + app.UseEndpoints(endpoints => { 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/TodoApiDTO.csproj b/TodoApiDTO.csproj index bba6f6af..ab0d14af 100644 --- a/TodoApiDTO.csproj +++ b/TodoApiDTO.csproj @@ -12,6 +12,8 @@ + + diff --git a/appsettings.json b/appsettings.json index d9d9a9bf..0c389937 100644 --- a/appsettings.json +++ b/appsettings.json @@ -6,5 +6,35 @@ "Microsoft.Hosting.Lifetime": "Information" } }, - "AllowedHosts": "*" + "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" + } + ] + } }