diff --git a/BLL/Services/ITodoItemService.cs b/BLL/Services/ITodoItemService.cs new file mode 100644 index 00000000..c90f46dd --- /dev/null +++ b/BLL/Services/ITodoItemService.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using TodoApi.Models; +using TodoApiDTO.DAL.Entities; + +namespace TodoApiDTO.BLL.Services +{ + public interface ITodoItemService + { + Task> GetTodoItemsAsync(); + Task GetTodoItemAsync(long id); + Task AddAsync(TodoItemDTO todoItemDTO); + Task EditAsync(TodoItemDTO todoItemDTO); + Task DeleteAsync(long id); + } +} \ No newline at end of file diff --git a/BLL/Services/Implementations/TodoItemService.cs b/BLL/Services/Implementations/TodoItemService.cs new file mode 100644 index 00000000..b3d4695c --- /dev/null +++ b/BLL/Services/Implementations/TodoItemService.cs @@ -0,0 +1,78 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using TodoApi.Models; +using TodoApiDTO.BLL.Services; +using TodoApiDTO.DAL.Entities; +using TodoApiDTO.DAL.Repositories; + + +namespace TodoApiDTO.BLL.Implementations.Services +{ + public class TodoItemService : ITodoItemService + { + private readonly ITodoItemRepository repository; + + public TodoItemService(ITodoItemRepository repository) + { + this.repository = repository; + } + + public async Task> GetTodoItemsAsync() + { + var todoItems = await repository.GetTodoItemsAsync(); + return todoItems + .Select(x => ItemToDTO(x)); + } + + public async Task GetTodoItemAsync(long id) + { + var todoItem = await repository.GetTodoItemByIdAsync(id); + var todoItemDTO = todoItem != null ? ItemToDTO(todoItem) : null; + return todoItemDTO; + } + public async Task AddAsync(TodoItemDTO todoItemDTO) + { + var todoItem = new TodoItem + { + IsComplete = todoItemDTO.IsComplete, + Name = todoItemDTO.Name + }; + + await repository.InsertTodoItemAsync(todoItem); + await repository.SaveAsync(); + return ItemToDTO(todoItem); + } + public async Task EditAsync(TodoItemDTO todoItemDTO) + { + await repository.UpdateTodoItemAsync(todoItemDTO); + + try + { + await repository.SaveAsync(); + } + catch (DbUpdateConcurrencyException) when (!TodoItemExists(todoItemDTO.Id)) + { + throw new TodoItemException("Item not found"); + } + } + public async Task DeleteAsync(long id) + { + await repository.DeleteTodoItemAsync(id); + await repository.SaveAsync(); + + + } + private bool TodoItemExists(long id) => + repository.GetTodoItemsAsync().Result.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/BLL/TodoItemException.cs b/BLL/TodoItemException.cs new file mode 100644 index 00000000..94dbc90d --- /dev/null +++ b/BLL/TodoItemException.cs @@ -0,0 +1,15 @@ +using System; + +namespace TodoApiDTO.BLL +{ + public class TodoItemException:Exception + { + public TodoItemException() { } + + public TodoItemException(string message) + : base(message) { } + + public TodoItemException(string message, Exception inner) + : base(message, inner) { } + } +} diff --git a/Controllers/TodoItemsController.cs b/Controllers/TodoItemsController.cs index 0ef138e7..e6507b0e 100644 --- a/Controllers/TodoItemsController.cs +++ b/Controllers/TodoItemsController.cs @@ -1,9 +1,10 @@ +using System; 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.BLL.Services; namespace TodoApi.Controllers { @@ -11,32 +12,31 @@ namespace TodoApi.Controllers [ApiController] public class TodoItemsController : ControllerBase { - private readonly TodoContext _context; + private readonly ITodoItemService todoItemService; - public TodoItemsController(TodoContext context) + public TodoItemsController(ITodoItemService todoItemService) { - _context = context; + this.todoItemService = todoItemService; } [HttpGet] public async Task>> GetTodoItems() { - return await _context.TodoItems - .Select(x => ItemToDTO(x)) - .ToListAsync(); + var todoItems = await todoItemService.GetTodoItemsAsync(); + return Ok(todoItems); } [HttpGet("{id}")] public async Task> GetTodoItem(long id) { - var todoItem = await _context.TodoItems.FindAsync(id); + var todoItem = await todoItemService.GetTodoItemAsync(id); if (todoItem == null) { return NotFound(); } - return ItemToDTO(todoItem); + return todoItem; } [HttpPut("{id}")] @@ -47,20 +47,18 @@ public async Task UpdateTodoItem(long id, TodoItemDTO todoItemDTO return BadRequest(); } - var todoItem = await _context.TodoItems.FindAsync(id); + var todoItem = await todoItemService.GetTodoItemAsync(id); + if (todoItem == null) { return NotFound(); } - todoItem.Name = todoItemDTO.Name; - todoItem.IsComplete = todoItemDTO.IsComplete; - try { - await _context.SaveChangesAsync(); + await todoItemService.EditAsync(todoItemDTO); } - catch (DbUpdateConcurrencyException) when (!TodoItemExists(id)) + catch (TodoItemException ex) { return NotFound(); } @@ -71,46 +69,28 @@ 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 todoItem = await todoItemService.AddAsync(todoItemDTO); return CreatedAtAction( nameof(GetTodoItem), new { id = todoItem.Id }, - ItemToDTO(todoItem)); + todoItem); } [HttpDelete("{id}")] public async Task DeleteTodoItem(long id) { - var todoItem = await _context.TodoItems.FindAsync(id); + var todoItem = await todoItemService.GetTodoItemAsync(id); if (todoItem == null) { return NotFound(); } - _context.TodoItems.Remove(todoItem); - await _context.SaveChangesAsync(); + await todoItemService.DeleteAsync(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/Models/TodoContext.cs b/DAL/DataContexts/TodoContext.cs similarity index 71% rename from Models/TodoContext.cs rename to DAL/DataContexts/TodoContext.cs index 6e59e363..735add38 100644 --- a/Models/TodoContext.cs +++ b/DAL/DataContexts/TodoContext.cs @@ -1,12 +1,14 @@ using Microsoft.EntityFrameworkCore; +using TodoApiDTO.DAL.Entities; -namespace TodoApi.Models +namespace TodoApiDTO.DAL.DataContexts { public class TodoContext : DbContext { public TodoContext(DbContextOptions options) : base(options) { + Database.EnsureCreated(); } public DbSet TodoItems { get; set; } diff --git a/Models/TodoItem.cs b/DAL/Entities/TodoItem.cs similarity index 86% rename from Models/TodoItem.cs rename to DAL/Entities/TodoItem.cs index 1f6e5465..7c554157 100644 --- a/Models/TodoItem.cs +++ b/DAL/Entities/TodoItem.cs @@ -1,4 +1,4 @@ -namespace TodoApi.Models +namespace TodoApiDTO.DAL.Entities { #region snippet public class TodoItem diff --git a/DAL/Repositories/ITodoItemRepository.cs b/DAL/Repositories/ITodoItemRepository.cs new file mode 100644 index 00000000..398a88cd --- /dev/null +++ b/DAL/Repositories/ITodoItemRepository.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using TodoApi.Models; +using TodoApiDTO.DAL.Entities; + +namespace TodoApiDTO.DAL.Repositories +{ + public interface ITodoItemRepository + { + Task> GetTodoItemsAsync(); + Task GetTodoItemByIdAsync(long id); + Task InsertTodoItemAsync(TodoItem todoItem); + Task DeleteTodoItemAsync(long todoItemID); + Task UpdateTodoItemAsync(TodoItemDTO todoItemDTO); + Task SaveAsync(); + } +} \ No newline at end of file diff --git a/DAL/Repositories/Implementations/TodoItemRepository.cs b/DAL/Repositories/Implementations/TodoItemRepository.cs new file mode 100644 index 00000000..380ec15e --- /dev/null +++ b/DAL/Repositories/Implementations/TodoItemRepository.cs @@ -0,0 +1,63 @@ +using Microsoft.EntityFrameworkCore; +using System.Collections.Generic; +using System; +using System.Linq; +using System.Threading.Tasks; +using TodoApiDTO.DAL.DataContexts; +using TodoApiDTO.DAL.Entities; +using TodoApi.Models; + +namespace TodoApiDTO.DAL.Repositories.Implementations +{ + + public class TodoItemRepository : ITodoItemRepository + { + private readonly TodoContext context; + + public TodoItemRepository(TodoContext context) + { + this.context = context; + } + + public async Task> GetTodoItemsAsync() + { + return await context.TodoItems.ToListAsync(); + } + + public async Task GetTodoItemByIdAsync(long id) + { + + return await context.TodoItems.FindAsync(id); + } + + public async Task InsertTodoItemAsync(TodoItem todoItem) + { + await context.TodoItems.AddAsync(todoItem); + } + + public async Task DeleteTodoItemAsync(long todoItemID) + { + TodoItem todoItem = await context.TodoItems.FindAsync(todoItemID); + if (todoItem != null) + { + context.TodoItems.Remove(todoItem); + } + } + + public async Task UpdateTodoItemAsync(TodoItemDTO todoItemDTO) + { + var todoItem = await GetTodoItemByIdAsync(todoItemDTO.Id); + if (todoItem != null) + { + todoItem.Name = todoItemDTO.Name; + todoItem.IsComplete = todoItemDTO.IsComplete; + } + } + + public async Task SaveAsync() + { + await context.SaveChangesAsync(); + } + + } +} diff --git a/GlobalErrorHandling/Extensions/ExceptionMiddlewareExtensions.cs b/GlobalErrorHandling/Extensions/ExceptionMiddlewareExtensions.cs new file mode 100644 index 00000000..df09f0cd --- /dev/null +++ b/GlobalErrorHandling/Extensions/ExceptionMiddlewareExtensions.cs @@ -0,0 +1,34 @@ +using System.Net; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics; +using Microsoft.AspNetCore.Http; +using TodoApiDTO.GlobalErrorHandling.Models; +using ILogger = Serilog.ILogger; + +namespace TodoApiDTO.GlobalErrorHandling.Extensions +{ + public static class ExceptionMiddlewareExtensions + { + public static void ConfigureExceptionHandler(this IApplicationBuilder app, ILogger logger) + { + app.UseExceptionHandler(appError => + { + appError.Run(async context => + { + context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; + context.Response.ContentType = "application/json"; + var contextFeature = context.Features.Get(); + if (contextFeature != null) + { + logger.Error($"Something went wrong: {contextFeature.Error}"); + await context.Response.WriteAsync(new ErrorDetails() + { + StatusCode = context.Response.StatusCode, + Message = "Internal Server Error." + }.ToString()); + } + }); + }); + } + } +} diff --git a/GlobalErrorHandling/Models/ErrorDetails.cs b/GlobalErrorHandling/Models/ErrorDetails.cs new file mode 100644 index 00000000..6280dc89 --- /dev/null +++ b/GlobalErrorHandling/Models/ErrorDetails.cs @@ -0,0 +1,14 @@ +using System.Text.Json; + +namespace TodoApiDTO.GlobalErrorHandling.Models +{ + public class ErrorDetails + { + public int StatusCode { get; set; } + public string Message { get; set; } + public override string ToString() + { + return JsonSerializer.Serialize(this); + } + } +} diff --git a/Program.cs b/Program.cs index b27ac16a..f15272b3 100644 --- a/Program.cs +++ b/Program.cs @@ -6,6 +6,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using Serilog; namespace TodoApi { @@ -13,6 +14,9 @@ public class Program { public static void Main(string[] args) { + var builder = CreateHostBuilder(args); + builder.UseSerilog((hcxt, prov, config) => config.ReadFrom.Configuration(hcxt.Configuration)); + builder.Build().Run(); CreateHostBuilder(args).Build().Run(); } diff --git a/Properties/launchSettings.json b/Properties/launchSettings.json index 6766196a..cf5e46de 100644 --- a/Properties/launchSettings.json +++ b/Properties/launchSettings.json @@ -11,6 +11,7 @@ "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, + "launchUrl": "swagger", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } @@ -18,6 +19,7 @@ "TodoApiDTO": { "commandName": "Project", "launchBrowser": true, + "launchUrl": "swagger", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, diff --git a/Startup.cs b/Startup.cs index bbfbc83d..c4b98d9f 100644 --- a/Startup.cs +++ b/Startup.cs @@ -1,17 +1,16 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.HttpsPolicy; -using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using TodoApi.Models; +using TodoApiDTO.BLL.Implementations.Services; +using TodoApiDTO.BLL.Services; +using TodoApiDTO.DAL.DataContexts; +using TodoApiDTO.DAL.Repositories; +using TodoApiDTO.DAL.Repositories.Implementations; +using TodoApiDTO.GlobalErrorHandling.Extensions; +using ILogger = Serilog.ILogger; namespace TodoApi { @@ -27,19 +26,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.AddDbContext(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnectionString"))); + services.AddScoped(); + services.AddScoped(); + services.AddSwaggerGen(); + 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) + public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILogger logger) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } + app.ConfigureExceptionHandler(logger); + app.UseSwagger(); + app.UseSwaggerUI(); + app.UseHttpsRedirection(); app.UseRouting(); diff --git a/TodoApiDTO.csproj b/TodoApiDTO.csproj index bba6f6af..f769ea31 100644 --- a/TodoApiDTO.csproj +++ b/TodoApiDTO.csproj @@ -12,6 +12,10 @@ + + + + diff --git a/appsettings.json b/appsettings.json index d9d9a9bf..3a7e74c1 100644 --- a/appsettings.json +++ b/appsettings.json @@ -6,5 +6,28 @@ "Microsoft.Hosting.Lifetime": "Information" } }, - "AllowedHosts": "*" -} + "AllowedHosts": "*", + + "ConnectionStrings": { + "DefaultConnectionString": "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=TodoList;Integrated Security=True;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False;ApplicationIntent=ReadWrite;MultiSubnetFailover=False" + }, + "Serilog": { + "MinimumLevel": { + "Default": "Information", + "Override": { + "Microsoft": "Error" + } + }, + "WriteTo": [ + { + "Name": "File", + "Args": { + "path": "Logs/log.txt" + } + } + ], + "Properties": { + "Application": "Sample" + } + } +} \ No newline at end of file