diff --git a/Controllers/TodoItemsController.cs b/Controllers/TodoItemsController.cs index 0ef138e7..3185fad7 100644 --- a/Controllers/TodoItemsController.cs +++ b/Controllers/TodoItemsController.cs @@ -4,6 +4,8 @@ using System.Linq; using System.Threading.Tasks; using TodoApi.Models; +using TodoApiDTO.Data; +using TodoApiDTO.Services.Interfaces; namespace TodoApi.Controllers { @@ -11,99 +13,54 @@ 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; + _todoItemService = todoItemService; } [HttpGet] public async Task>> GetTodoItems() { - return await _context.TodoItems - .Select(x => ItemToDTO(x)) - .ToListAsync(); + var todoItems = await _todoItemService.GetTodoItems(); + return Ok(todoItems); } [HttpGet("{id}")] public async Task> GetTodoItem(long id) { - var todoItem = await _context.TodoItems.FindAsync(id); + var todoItem = await _todoItemService.GetTodoItem(id); if (todoItem == null) { return NotFound(); } - return ItemToDTO(todoItem); + return Ok(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(); + var result = await _todoItemService.UpdateTodoItem(id, todoItemDTO); + return result; } [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)); + var createdItem = await _todoItemService.CreateTodoItem(todoItemDTO); + return CreatedAtAction(nameof(GetTodoItem), new { id = createdItem.Id }, createdItem); } [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(); + var result = await _todoItemService.DeleteTodoItem(id); + return result; } - private bool TodoItemExists(long id) => - _context.TodoItems.Any(e => e.Id == id); private static TodoItemDTO ItemToDTO(TodoItem todoItem) => new TodoItemDTO diff --git a/CreateTableQuery.sql b/CreateTableQuery.sql new file mode 100644 index 00000000..8f6557c1 --- /dev/null +++ b/CreateTableQuery.sql @@ -0,0 +1,17 @@ +DROP TABLE TodoItems; + +CREATE TABLE TodoItems ( + id BIGINT NOT NULL PRIMARY KEY IDENTITY, + name VARCHAR(200) NOT NULL, + isComplete BIT NOT NULL, + secret VARCHAR(200) + ); + +INSERT INTO TodoItems (name, isComplete, secret) +VALUES +('Add Swagger', 1, 'Placeholder'), +('Integrate SQL in the project', 1, 'Placeholder'), +('Add Logging in file', 0, 'Placeholder'), +('Separate Business Layers', 0, 'Placeholder'), +('Refactoring', 0 , 'Placeholder') + diff --git a/Models/TodoContext.cs b/Data/TodoContext.cs similarity index 84% rename from Models/TodoContext.cs rename to Data/TodoContext.cs index 6e59e363..95ba6bdd 100644 --- a/Models/TodoContext.cs +++ b/Data/TodoContext.cs @@ -1,6 +1,7 @@ using Microsoft.EntityFrameworkCore; +using TodoApi.Models; -namespace TodoApi.Models +namespace TodoApiDTO.Data { public class TodoContext : DbContext { diff --git a/Logs202306221707.txt b/Logs202306221707.txt new file mode 100644 index 00000000..f5cb04f4 --- /dev/null +++ b/Logs202306221707.txt @@ -0,0 +1,39 @@ +2023-06-22 17:07:04.674 +04:00 [INF] User profile is available. Using 'C:\Users\David\AppData\Local\ASP.NET\DataProtection-Keys' as key repository and Windows DPAPI to encrypt keys at rest. +2023-06-22 17:07:05.234 +04:00 [INF] Application started. Press Ctrl+C to shut down. +2023-06-22 17:07:05.243 +04:00 [INF] Hosting environment: Development +2023-06-22 17:07:05.246 +04:00 [INF] Content root path: C:\Users\David\Source\Repos\VelvetechTestTask +2023-06-22 17:07:05.273 +04:00 [INF] Request starting HTTP/2.0 GET https://localhost:44331/api/todoitems +2023-06-22 17:07:05.341 +04:00 [INF] Executing endpoint 'TodoApi.Controllers.TodoItemsController.GetTodoItems (TodoApiDTO)' +2023-06-22 17:07:05.410 +04:00 [INF] Route matched with {action = "GetTodoItems", controller = "TodoItems"}. Executing controller action with signature System.Threading.Tasks.Task`1[Microsoft.AspNetCore.Mvc.ActionResult`1[System.Collections.Generic.IEnumerable`1[TodoApi.Models.TodoItemDTO]]] GetTodoItems() on controller TodoApi.Controllers.TodoItemsController (TodoApiDTO). +2023-06-22 17:07:05.967 +04:00 [INF] Entity Framework Core 3.1.0 initialized 'TodoContext' using provider 'Microsoft.EntityFrameworkCore.SqlServer' with options: None +2023-06-22 17:07:06.537 +04:00 [INF] Executed DbCommand (38ms) [Parameters=[], CommandType='"Text"', CommandTimeout='30'] +SELECT [t].[Id], [t].[IsComplete], [t].[Name], [t].[Secret] +FROM [TodoItems] AS [t] +2023-06-22 17:07:06.616 +04:00 [INF] Executing ObjectResult, writing value of type 'System.Collections.Generic.List`1[[TodoApi.Models.TodoItemDTO, TodoApiDTO, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]]'. +2023-06-22 17:07:06.738 +04:00 [INF] Executed action TodoApi.Controllers.TodoItemsController.GetTodoItems (TodoApiDTO) in 1322.1577ms +2023-06-22 17:07:06.740 +04:00 [INF] Executed endpoint 'TodoApi.Controllers.TodoItemsController.GetTodoItems (TodoApiDTO)' +2023-06-22 17:07:06.751 +04:00 [INF] Request finished in 1483.5408ms 200 application/json; charset=utf-8 +2023-06-22 17:07:12.178 +04:00 [INF] Request starting HTTP/2.0 GET https://localhost:44331/api/todoitems/1 +2023-06-22 17:07:12.183 +04:00 [INF] Executing endpoint 'TodoApi.Controllers.TodoItemsController.GetTodoItem (TodoApiDTO)' +2023-06-22 17:07:12.196 +04:00 [INF] Route matched with {action = "GetTodoItem", controller = "TodoItems"}. Executing controller action with signature System.Threading.Tasks.Task`1[Microsoft.AspNetCore.Mvc.ActionResult`1[TodoApi.Models.TodoItemDTO]] GetTodoItem(Int64) on controller TodoApi.Controllers.TodoItemsController (TodoApiDTO). +2023-06-22 17:07:12.238 +04:00 [INF] Entity Framework Core 3.1.0 initialized 'TodoContext' using provider 'Microsoft.EntityFrameworkCore.SqlServer' with options: None +2023-06-22 17:07:12.318 +04:00 [INF] Executed DbCommand (39ms) [Parameters=[@__p_0='?' (DbType = Int64)], CommandType='"Text"', CommandTimeout='30'] +SELECT TOP(1) [t].[Id], [t].[IsComplete], [t].[Name], [t].[Secret] +FROM [TodoItems] AS [t] +WHERE [t].[Id] = @__p_0 +2023-06-22 17:07:12.321 +04:00 [INF] Executing ObjectResult, writing value of type 'TodoApi.Models.TodoItemDTO'. +2023-06-22 17:07:12.323 +04:00 [INF] Executed action TodoApi.Controllers.TodoItemsController.GetTodoItem (TodoApiDTO) in 126.6063ms +2023-06-22 17:07:12.323 +04:00 [INF] Executed endpoint 'TodoApi.Controllers.TodoItemsController.GetTodoItem (TodoApiDTO)' +2023-06-22 17:07:12.323 +04:00 [INF] Request finished in 145.5845ms 200 application/json; charset=utf-8 +2023-06-22 17:07:15.623 +04:00 [INF] Request starting HTTP/2.0 GET https://localhost:44331/api/todoitems/6 +2023-06-22 17:07:15.625 +04:00 [INF] Executing endpoint 'TodoApi.Controllers.TodoItemsController.GetTodoItem (TodoApiDTO)' +2023-06-22 17:07:15.625 +04:00 [INF] Route matched with {action = "GetTodoItem", controller = "TodoItems"}. Executing controller action with signature System.Threading.Tasks.Task`1[Microsoft.AspNetCore.Mvc.ActionResult`1[TodoApi.Models.TodoItemDTO]] GetTodoItem(Int64) on controller TodoApi.Controllers.TodoItemsController (TodoApiDTO). +2023-06-22 17:07:15.635 +04:00 [INF] Entity Framework Core 3.1.0 initialized 'TodoContext' using provider 'Microsoft.EntityFrameworkCore.SqlServer' with options: None +2023-06-22 17:07:15.644 +04:00 [INF] Executed DbCommand (4ms) [Parameters=[@__p_0='?' (DbType = Int64)], CommandType='"Text"', CommandTimeout='30'] +SELECT TOP(1) [t].[Id], [t].[IsComplete], [t].[Name], [t].[Secret] +FROM [TodoItems] AS [t] +WHERE [t].[Id] = @__p_0 +2023-06-22 17:07:15.649 +04:00 [INF] Executing ObjectResult, writing value of type 'Microsoft.AspNetCore.Mvc.ProblemDetails'. +2023-06-22 17:07:15.655 +04:00 [INF] Executed action TodoApi.Controllers.TodoItemsController.GetTodoItem (TodoApiDTO) in 29.833ms +2023-06-22 17:07:15.655 +04:00 [INF] Executed endpoint 'TodoApi.Controllers.TodoItemsController.GetTodoItem (TodoApiDTO)' +2023-06-22 17:07:15.656 +04:00 [INF] Request finished in 33.5163ms 404 application/problem+json; charset=utf-8 diff --git a/Properties/launchSettings.json b/Properties/launchSettings.json index 6766196a..a93b0e87 100644 --- a/Properties/launchSettings.json +++ b/Properties/launchSettings.json @@ -11,6 +11,7 @@ "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, + "launchUrl": "api/todoitems", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/Services/Interfaces/ITodoItemService.cs b/Services/Interfaces/ITodoItemService.cs new file mode 100644 index 00000000..d6657c8d --- /dev/null +++ b/Services/Interfaces/ITodoItemService.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Mvc; +using System.Collections.Generic; +using System.Threading.Tasks; +using TodoApi.Models; + +namespace TodoApiDTO.Services.Interfaces +{ + public interface ITodoItemService + { + Task> GetTodoItems(); + Task CreateTodoItem(TodoItemDTO todoItemDTO); + Task GetTodoItem(long id); + Task UpdateTodoItem(long id, TodoItemDTO todoItemDTO); + Task DeleteTodoItem(long id); + } +} diff --git a/Services/TodoItemService.cs b/Services/TodoItemService.cs new file mode 100644 index 00000000..778fc39b --- /dev/null +++ b/Services/TodoItemService.cs @@ -0,0 +1,112 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using TodoApi.Models; +using TodoApiDTO.Data; +using TodoApiDTO.Services.Interfaces; + +namespace TodoApiDTO.Services +{ + public class TodoItemService : ITodoItemService + { + private readonly TodoContext _context; + + public TodoItemService(TodoContext context) + { + _context = context; + } + + public async Task> GetTodoItems() + { + var todoItems = await _context.TodoItems + .Select(x => ItemToDTO(x)) + .ToListAsync(); + + return todoItems; + } + + public async Task GetTodoItem(long id) + { + var todoItem = await _context.TodoItems.FindAsync(id); + + if (todoItem == null) + { + return null; + } + + var todoItemDTO = ItemToDTO(todoItem); + return todoItemDTO; + } + + public async Task UpdateTodoItem(long id, TodoItemDTO todoItemDTO) + { + if (id != todoItemDTO.Id) + { + return new BadRequestResult(); + } + + var todoItem = await _context.TodoItems.FindAsync(id); + if (todoItem == null) + { + return new NotFoundResult(); + } + + todoItem.Name = todoItemDTO.Name; + todoItem.IsComplete = todoItemDTO.IsComplete; + + try + { + await _context.SaveChangesAsync(); + } + catch (DbUpdateConcurrencyException) when (!TodoItemExists(id)) + { + return new NotFoundResult(); + } + + return new NoContentResult(); + } + + public async Task CreateTodoItem(TodoItemDTO todoItemDTO) + { + var todoItem = new TodoItem + { + IsComplete = todoItemDTO.IsComplete, + Name = todoItemDTO.Name + }; + + _context.TodoItems.Add(todoItem); + await _context.SaveChangesAsync(); + + return ItemToDTO(todoItem); + } + + public async Task DeleteTodoItem(long id) + { + var todoItem = await _context.TodoItems.FindAsync(id); + + if (todoItem == null) + { + return new NotFoundResult(); + } + + _context.TodoItems.Remove(todoItem); + await _context.SaveChangesAsync(); + + return new NoContentResult(); + } + + 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/Startup.cs b/Startup.cs index bbfbc83d..077b5ba0 100644 --- a/Startup.cs +++ b/Startup.cs @@ -11,7 +11,12 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using TodoApi.Models; +using Swashbuckle.AspNetCore; +using Microsoft.OpenApi.Models; +using Serilog; +using TodoApiDTO.Data; +using TodoApiDTO.Services; +using TodoApiDTO.Services.Interfaces; namespace TodoApi { @@ -27,9 +32,29 @@ 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) { + Log.Logger = new LoggerConfiguration() + .WriteTo.File("Logs.txt", rollingInterval: RollingInterval.Minute) + .CreateLogger(); + + + services.AddMvc(); + services.AddDbContext(opt => - opt.UseInMemoryDatabase("TodoList")); + opt.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))); services.AddControllers(); + + services.AddSwaggerGen(c => + { + c.SwaggerDoc("v1", new OpenApiInfo { Title = "TodoDTO API", Version = "v1" }); + }); + + services.AddLogging(logBuilder => + { + logBuilder.AddSerilog(dispose:true); + }); + + services.AddTransient(); + } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. @@ -50,6 +75,36 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { endpoints.MapControllers(); }); + + + + #region Swagger + + app.UseSwagger(); + app.UseSwaggerUI(c => + { + c.SwaggerEndpoint("v1/swagger.json", "TodoDTO API v1"); + }); + + #endregion + + #region Logging + + + app.UseExceptionHandler("/error"); + app.Use(async (context, next) => + { + try + { + await next.Invoke(); + } + catch (Exception ex) + { + Log.Error(ex, "An unhandled exception occurred."); + throw; + } + }); + #endregion } } } diff --git a/TodoApiDTO.csproj b/TodoApiDTO.csproj index bba6f6af..68e7dce7 100644 --- a/TodoApiDTO.csproj +++ b/TodoApiDTO.csproj @@ -12,6 +12,10 @@ + + + + diff --git a/appsettings.json b/appsettings.json index d9d9a9bf..5cae3617 100644 --- a/appsettings.json +++ b/appsettings.json @@ -1,4 +1,8 @@ { + "ConnectionStrings": { + "DefaultConnection" : "Server=(localdb)\\mssqllocaldb;Database=TodoDb;Trusted_Connection=True;MultipleActiveResultSets=true" + }, + "Logging": { "LogLevel": { "Default": "Information",