diff --git a/Backend/TodoList.Api/.dockerignore b/Backend/.dockerignore similarity index 100% rename from Backend/TodoList.Api/.dockerignore rename to Backend/.dockerignore diff --git a/Backend/TodoList.Api/Dockerfile b/Backend/Dockerfile similarity index 100% rename from Backend/TodoList.Api/Dockerfile rename to Backend/Dockerfile diff --git a/Backend/TodoList.Api.UnitTests/ControllerTests/TodoItemsControllerTests/PostTodoItemTests.cs b/Backend/TodoList.Api.UnitTests/ControllerTests/TodoItemsControllerTests/PostTodoItemTests.cs new file mode 100644 index 00000000..1115555e --- /dev/null +++ b/Backend/TodoList.Api.UnitTests/ControllerTests/TodoItemsControllerTests/PostTodoItemTests.cs @@ -0,0 +1,67 @@ +using System.Threading; +using System.Threading.Tasks; +using FakeItEasy; +using Microsoft.AspNetCore.Mvc; +using TodoList.Api.Controllers; +using TodoList.Services.Modules.TodoListModule; +using TodoList.Services.Modules.TodoListModule.Models; +using TodoList.Services.Modules.TodoListModule.ViewModels; +using TodoList.Services.Shared; +using Xunit; + +namespace TodoList.Api.UnitTests.ControllerTests.TodoItemsControllerTests +{ + public class PostTodoItemTests + { + [Fact] + public async Task ShouldReturnCreatedItemWhenValidInputPassed() + { + // Arrange + var cancellationToken = CancellationToken.None; + var description = "Test"; + var todoItemModel = new CreateTodoItemModel { Description = description }; + var itemCreated = new GetTodoItemViewModel { Description = description }; + var serviceResult = new ServiceResult(); + serviceResult.ToResourceCreatedResult(itemCreated); + + var fakeService = A.Fake(); + A.CallTo(() => fakeService.CreateTodoItem(todoItemModel, cancellationToken)) + .Returns(Task.FromResult(serviceResult)); + + var controller = new TodoItemsController(fakeService); + + // Act + var actionResult = await controller.PostTodoItem(todoItemModel, cancellationToken); + + // Assert + A.CallTo(() => fakeService.CreateTodoItem(todoItemModel, cancellationToken)).MustHaveHappenedOnceExactly(); + var createdAtActionResult = Assert.IsType(actionResult); + Assert.Equal(nameof(controller.GetTodoItem), createdAtActionResult.ActionName); + Assert.Equal(itemCreated, createdAtActionResult.Value); + } + + [Fact] + public async Task ShoudlReturnBadRequestWhenInvalidInputPassed() + { + // Arrange + var cancellationToken = CancellationToken.None; + CreateTodoItemModel todoItemModel = null; + + var serviceResult = new ServiceResult(); + serviceResult.BadRequestWithMessage("Description is required"); + + var fakeService = A.Fake(); + A.CallTo(() => fakeService.CreateTodoItem(todoItemModel, cancellationToken)) + .Returns(Task.FromResult(serviceResult)); + + var controller = new TodoItemsController(fakeService); + + // Act + var actionResult = await controller.PostTodoItem(todoItemModel, cancellationToken); + + // Assert + var badRequestResult = Assert.IsType(actionResult); + Assert.Equal("Description is required", badRequestResult.Value); + } + } +} diff --git a/Backend/TodoList.Api/TodoList.Api.UnitTests/TodoList.Api.UnitTests.csproj b/Backend/TodoList.Api.UnitTests/TodoList.Api.UnitTests.csproj similarity index 93% rename from Backend/TodoList.Api/TodoList.Api.UnitTests/TodoList.Api.UnitTests.csproj rename to Backend/TodoList.Api.UnitTests/TodoList.Api.UnitTests.csproj index 5956ac35..283f7733 100644 --- a/Backend/TodoList.Api/TodoList.Api.UnitTests/TodoList.Api.UnitTests.csproj +++ b/Backend/TodoList.Api.UnitTests/TodoList.Api.UnitTests.csproj @@ -7,6 +7,7 @@ + diff --git a/Backend/TodoList.Api.sln b/Backend/TodoList.Api.sln new file mode 100644 index 00000000..c56146ef --- /dev/null +++ b/Backend/TodoList.Api.sln @@ -0,0 +1,85 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.6.33927.249 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TodoList.Api", "TodoList.Api\TodoList.Api.csproj", "{065CE954-2618-4A24-A613-9C336C2154C6}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TodoList.Api.UnitTests", "TodoList.Api.UnitTests\TodoList.Api.UnitTests.csproj", "{2BBBFEED-85B6-4361-85B2-F9E08C86C55B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TodoList.Domain", "TodoList.Domain\TodoList.Domain.csproj", "{693E7B0C-DA2B-446E-B729-03FE4E1B184C}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "UnitTests", "UnitTests", "{81EDD5C8-EAE7-4964-80B5-6B90241C7028}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Infrastructure", "Infrastructure", "{739BBC78-14C8-425F-9627-8BB80757A021}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Presentations", "Presentations", "{54522759-2130-4896-822B-E4780B726E31}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Core", "Core", "{E05B50FF-13BB-495F-9B73-65A5683AE3C2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TodoList.Infrastructure.Persistence.EFCore", "TodoList.Infrastructure.Persistence.EFCore\TodoList.Infrastructure.Persistence.EFCore.csproj", "{9A26C735-BF26-4E9C-BA1E-6BE5BDCF6A4F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TodoList.Services", "TodoList.Services\TodoList.Services.csproj", "{25041BFD-A612-4DDC-BAE9-217ACD3E8188}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TodoList.Infrastructure.Application.Services", "TodoList.Infrastructure.Application.Services\TodoList.Infrastructure.Application.Services.csproj", "{D259C88D-A212-4496-AF88-0CDF210C055C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TodoList.Infrastructure.Persistence.EFCore.UnitTests", "TodoList.Infrastructure.Persistence.EFCore.UnitTests\TodoList.Infrastructure.Persistence.EFCore.UnitTests.csproj", "{4E87ADC6-EE79-4793-84AF-A5F4530C73D1}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TodoList.Infrastructure.Application.Services.UnitTests", "TodoList.Infrastructure.Application.Services.UnitTests\TodoList.Infrastructure.Application.Services.UnitTests.csproj", "{164F6FFB-637D-4359-93E4-0D5E84A526BF}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {065CE954-2618-4A24-A613-9C336C2154C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {065CE954-2618-4A24-A613-9C336C2154C6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {065CE954-2618-4A24-A613-9C336C2154C6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {065CE954-2618-4A24-A613-9C336C2154C6}.Release|Any CPU.Build.0 = Release|Any CPU + {2BBBFEED-85B6-4361-85B2-F9E08C86C55B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2BBBFEED-85B6-4361-85B2-F9E08C86C55B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2BBBFEED-85B6-4361-85B2-F9E08C86C55B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2BBBFEED-85B6-4361-85B2-F9E08C86C55B}.Release|Any CPU.Build.0 = Release|Any CPU + {693E7B0C-DA2B-446E-B729-03FE4E1B184C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {693E7B0C-DA2B-446E-B729-03FE4E1B184C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {693E7B0C-DA2B-446E-B729-03FE4E1B184C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {693E7B0C-DA2B-446E-B729-03FE4E1B184C}.Release|Any CPU.Build.0 = Release|Any CPU + {9A26C735-BF26-4E9C-BA1E-6BE5BDCF6A4F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9A26C735-BF26-4E9C-BA1E-6BE5BDCF6A4F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9A26C735-BF26-4E9C-BA1E-6BE5BDCF6A4F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9A26C735-BF26-4E9C-BA1E-6BE5BDCF6A4F}.Release|Any CPU.Build.0 = Release|Any CPU + {25041BFD-A612-4DDC-BAE9-217ACD3E8188}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {25041BFD-A612-4DDC-BAE9-217ACD3E8188}.Debug|Any CPU.Build.0 = Debug|Any CPU + {25041BFD-A612-4DDC-BAE9-217ACD3E8188}.Release|Any CPU.ActiveCfg = Release|Any CPU + {25041BFD-A612-4DDC-BAE9-217ACD3E8188}.Release|Any CPU.Build.0 = Release|Any CPU + {D259C88D-A212-4496-AF88-0CDF210C055C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D259C88D-A212-4496-AF88-0CDF210C055C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D259C88D-A212-4496-AF88-0CDF210C055C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D259C88D-A212-4496-AF88-0CDF210C055C}.Release|Any CPU.Build.0 = Release|Any CPU + {4E87ADC6-EE79-4793-84AF-A5F4530C73D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4E87ADC6-EE79-4793-84AF-A5F4530C73D1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4E87ADC6-EE79-4793-84AF-A5F4530C73D1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4E87ADC6-EE79-4793-84AF-A5F4530C73D1}.Release|Any CPU.Build.0 = Release|Any CPU + {164F6FFB-637D-4359-93E4-0D5E84A526BF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {164F6FFB-637D-4359-93E4-0D5E84A526BF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {164F6FFB-637D-4359-93E4-0D5E84A526BF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {164F6FFB-637D-4359-93E4-0D5E84A526BF}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {065CE954-2618-4A24-A613-9C336C2154C6} = {54522759-2130-4896-822B-E4780B726E31} + {2BBBFEED-85B6-4361-85B2-F9E08C86C55B} = {81EDD5C8-EAE7-4964-80B5-6B90241C7028} + {693E7B0C-DA2B-446E-B729-03FE4E1B184C} = {E05B50FF-13BB-495F-9B73-65A5683AE3C2} + {9A26C735-BF26-4E9C-BA1E-6BE5BDCF6A4F} = {739BBC78-14C8-425F-9627-8BB80757A021} + {25041BFD-A612-4DDC-BAE9-217ACD3E8188} = {E05B50FF-13BB-495F-9B73-65A5683AE3C2} + {D259C88D-A212-4496-AF88-0CDF210C055C} = {739BBC78-14C8-425F-9627-8BB80757A021} + {4E87ADC6-EE79-4793-84AF-A5F4530C73D1} = {81EDD5C8-EAE7-4964-80B5-6B90241C7028} + {164F6FFB-637D-4359-93E4-0D5E84A526BF} = {81EDD5C8-EAE7-4964-80B5-6B90241C7028} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {061F7879-5D14-4C5E-A4A9-782C6FEB1132} + EndGlobalSection +EndGlobal diff --git a/Backend/TodoList.Api/Configurations/ApiVersioningAndSwaggerConfiguration.cs b/Backend/TodoList.Api/Configurations/ApiVersioningAndSwaggerConfiguration.cs new file mode 100644 index 00000000..8e213ab9 --- /dev/null +++ b/Backend/TodoList.Api/Configurations/ApiVersioningAndSwaggerConfiguration.cs @@ -0,0 +1,36 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.OpenApi.Models; + +namespace TodoList.Api.Configurations +{ + public static class ApiVersioningAndSwaggerConfiguration + { + public static void AddApiVersioningAndSwagger(this IServiceCollection services) + { + services.AddApiVersioning(options => + { + options.ReportApiVersions = true; + options.DefaultApiVersion = new ApiVersion(1, 0); + options.ReportApiVersions = true; + options.AssumeDefaultVersionWhenUnspecified = true; + }); + + services.AddSwaggerGen(c => + { + c.SwaggerDoc("v1", new OpenApiInfo { Title = "TodoList.Api" }); + }); + + services.AddSwaggerGen(); + + } + + public static IApplicationBuilder UseSwaggerEndpoint(this IApplicationBuilder app) + { + app.UseSwagger(); + app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "TodoList.Api v1")); + return app; + } + } +} diff --git a/Backend/TodoList.Api/Configurations/CorsConfiguration.cs b/Backend/TodoList.Api/Configurations/CorsConfiguration.cs new file mode 100644 index 00000000..6e4eea7a --- /dev/null +++ b/Backend/TodoList.Api/Configurations/CorsConfiguration.cs @@ -0,0 +1,30 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +namespace TodoList.Api.Configurations +{ + public static class CorsConfiguration + { + public static IServiceCollection AddCorsPolicies(this IServiceCollection services, string policyName) + { + services.AddCors(options => + { + options.AddPolicy(policyName, + builder => + { + builder.AllowAnyOrigin() + .AllowAnyHeader() + .AllowAnyMethod(); + }); + }); + + return services; + } + + public static IApplicationBuilder UseCorsPolicy(this IApplicationBuilder applicationBuilder, string poliocyName) + { + applicationBuilder.UseCors(poliocyName); + return applicationBuilder; + } + } +} diff --git a/Backend/TodoList.Api/Configurations/InfrastructureConfiguration.cs b/Backend/TodoList.Api/Configurations/InfrastructureConfiguration.cs new file mode 100644 index 00000000..ba00958f --- /dev/null +++ b/Backend/TodoList.Api/Configurations/InfrastructureConfiguration.cs @@ -0,0 +1,18 @@ +using Microsoft.Extensions.DependencyInjection; +using TodoList.Infrastructure.Application.Services.Extensions; +using TodoList.Infrastructure.Persistence.EFCore.Extensions; + +namespace TodoList.Api.Configurations +{ + public static class InfrastructureConfiguration + { + public static void AddApplicationServices(this IServiceCollection services) + { + services.AddAppServices(); + } + public static void AddEfCoreAsPersistence(this IServiceCollection services) + { + services.AddInMemoryPersistence(); + } + } +} diff --git a/Backend/TodoList.Api/Controllers/ApiControllerBase.cs b/Backend/TodoList.Api/Controllers/ApiControllerBase.cs new file mode 100644 index 00000000..ed914a2a --- /dev/null +++ b/Backend/TodoList.Api/Controllers/ApiControllerBase.cs @@ -0,0 +1,8 @@ +using Microsoft.AspNetCore.Mvc; + +namespace TodoList.Api.Controllers +{ + public abstract class ApiControllerBase: ControllerBase + { + } +} diff --git a/Backend/TodoList.Api/Controllers/TodoItemsController.cs b/Backend/TodoList.Api/Controllers/TodoItemsController.cs new file mode 100644 index 00000000..d7d364a4 --- /dev/null +++ b/Backend/TodoList.Api/Controllers/TodoItemsController.cs @@ -0,0 +1,74 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using TodoList.Services.Modules.TodoListModule; +using TodoList.Services.Modules.TodoListModule.Models; +using TodoList.Services.Shared; + +namespace TodoList.Api.Controllers +{ + + [ApiVersion("1")] + [Route("api/v{version:apiVersion}/[controller]")] + [ApiController] + public class TodoItemsController : ApiControllerBase + { + private readonly ITodoListService todoListService; + + public TodoItemsController(ITodoListService todoListService) + { + this.todoListService = todoListService; + } + + // GET: api/TodoItems/... + [HttpGet] + public async Task GetTodoItem(CancellationToken token) + { + var result = await this.todoListService.GetItems(token).ConfigureAwait(false); + return Ok(result); + } + + [HttpGet("{id}")] + public async Task GetTodoItem(Guid id, CancellationToken token) + { + var result = await this.todoListService.GetItem(id, token).ConfigureAwait(false); + + if (result.ResultCode == ResultCode.NotFound) + { + return NotFound(); + } + + return Ok(result); + } + + // POST: api/TodoItems + [HttpPost] + public async Task PostTodoItem(CreateTodoItemModel todoItem, CancellationToken token) + { + var result = await this.todoListService.CreateTodoItem(todoItem, token).ConfigureAwait(false); + if (result.ResultCode == ResultCode.Created) + { + return CreatedAtAction(nameof(GetTodoItem), new { id = result.Result.Id }, result.Result); + } + + return BadRequest(result.Message); + + } + + // PUT: api/TodoItems/... + [HttpPut("{id}")] + public async Task PutTodoItem(Guid id, UpdateTodoItemModel todoItem, CancellationToken token) + { + var result = await this.todoListService.UpdateTodoItemStatus(id, todoItem, token).ConfigureAwait(false); + + if (result.ResultCode == ResultCode.NotFound) + { + return NotFound(); + } + + return NoContent(); + } + } +} diff --git a/Backend/TodoList.Api/Models/ApiErrorResponse.cs b/Backend/TodoList.Api/Models/ApiErrorResponse.cs new file mode 100644 index 00000000..cda8d97a --- /dev/null +++ b/Backend/TodoList.Api/Models/ApiErrorResponse.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; + +namespace TodoList.Api.Models +{ + public class ApiErrorResponse + { + public string Type { get; set; } + public string Title { get; set; } + public string Detail { get; set; } + public int Status { get; set; } + public string Instance { get; set; } + public IEnumerable> Errors { get; set; } + } +} diff --git a/Backend/TodoList.Api/TodoList.Api/Program.cs b/Backend/TodoList.Api/Program.cs similarity index 100% rename from Backend/TodoList.Api/TodoList.Api/Program.cs rename to Backend/TodoList.Api/Program.cs diff --git a/Backend/TodoList.Api/TodoList.Api/Properties/launchSettings.json b/Backend/TodoList.Api/Properties/launchSettings.json similarity index 84% rename from Backend/TodoList.Api/TodoList.Api/Properties/launchSettings.json rename to Backend/TodoList.Api/Properties/launchSettings.json index 43840e65..b985add6 100644 --- a/Backend/TodoList.Api/TodoList.Api/Properties/launchSettings.json +++ b/Backend/TodoList.Api/Properties/launchSettings.json @@ -12,8 +12,7 @@ "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, - //"launchUrl": "swagger", - "launchUrl": "api/todoitems", + "launchUrl": "swagger", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } @@ -22,8 +21,7 @@ "commandName": "Project", "dotnetRunMessages": "true", "launchBrowser": true, - //"launchUrl": "swagger", - "launchUrl": "api/todoitems", + "launchUrl": "swagger", "applicationUrl": "https://localhost:5001;http://localhost:5000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" diff --git a/Backend/TodoList.Api/TodoList.Api/Startup.cs b/Backend/TodoList.Api/Startup.cs similarity index 56% rename from Backend/TodoList.Api/TodoList.Api/Startup.cs rename to Backend/TodoList.Api/Startup.cs index 1db21ceb..3ad2808d 100644 --- a/Backend/TodoList.Api/TodoList.Api/Startup.cs +++ b/Backend/TodoList.Api/Startup.cs @@ -1,10 +1,9 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using Microsoft.OpenApi.Models; +using TodoList.Api.Configurations; namespace TodoList.Api { @@ -17,27 +16,15 @@ public Startup(IConfiguration 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.AddCors(options => - { - options.AddPolicy("AllowAllHeaders", - builder => - { - builder.AllowAnyOrigin() - .AllowAnyHeader() - .AllowAnyMethod(); - }); - }); - + services.AddCorsPolicies("AllowAllHeaders"); services.AddControllers(); - services.AddSwaggerGen(c => - { - c.SwaggerDoc("v1", new OpenApiInfo { Title = "TodoList.Api", Version = "v1" }); - }); - - services.AddDbContext(opt => opt.UseInMemoryDatabase("TodoItemsDB")); + services.AddApiVersioningAndSwagger(); + services.AddApplicationServices(); + services.AddEfCoreAsPersistence(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. @@ -46,22 +33,20 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); - app.UseSwagger(); - app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "TodoList.Api v1")); + app.UseSwaggerEndpoint(); } app.UseHttpsRedirection(); app.UseRouting(); - app.UseCors("AllowAllHeaders"); - - app.UseAuthorization(); + app.UseCorsPolicy("AllowAllHeaders"); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); + } } } diff --git a/Backend/TodoList.Api/TodoList.Api.UnitTests/DummyTestShould.cs b/Backend/TodoList.Api/TodoList.Api.UnitTests/DummyTestShould.cs deleted file mode 100644 index 63656951..00000000 --- a/Backend/TodoList.Api/TodoList.Api.UnitTests/DummyTestShould.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Xunit; - -namespace TodoList.Api.UnitTests -{ - public class DummyTestShould - { - [Fact] - public void Test_Should_TestSomething() - { - } - } -} diff --git a/Backend/TodoList.Api/TodoList.Api/TodoList.Api.csproj b/Backend/TodoList.Api/TodoList.Api.csproj similarity index 62% rename from Backend/TodoList.Api/TodoList.Api/TodoList.Api.csproj rename to Backend/TodoList.Api/TodoList.Api.csproj index d206ff56..a1ee1a37 100644 --- a/Backend/TodoList.Api/TodoList.Api/TodoList.Api.csproj +++ b/Backend/TodoList.Api/TodoList.Api.csproj @@ -1,4 +1,4 @@ - + net6.0 @@ -12,7 +12,7 @@ - + all @@ -20,6 +20,12 @@ + + + + + + diff --git a/Backend/TodoList.Api/TodoList.Api.sln b/Backend/TodoList.Api/TodoList.Api.sln deleted file mode 100644 index de8d1a58..00000000 --- a/Backend/TodoList.Api/TodoList.Api.sln +++ /dev/null @@ -1,31 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.31613.86 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TodoList.Api", "TodoList.Api\TodoList.Api.csproj", "{065CE954-2618-4A24-A613-9C336C2154C6}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TodoList.Api.UnitTests", "TodoList.Api.UnitTests\TodoList.Api.UnitTests.csproj", "{2BBBFEED-85B6-4361-85B2-F9E08C86C55B}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {065CE954-2618-4A24-A613-9C336C2154C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {065CE954-2618-4A24-A613-9C336C2154C6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {065CE954-2618-4A24-A613-9C336C2154C6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {065CE954-2618-4A24-A613-9C336C2154C6}.Release|Any CPU.Build.0 = Release|Any CPU - {2BBBFEED-85B6-4361-85B2-F9E08C86C55B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2BBBFEED-85B6-4361-85B2-F9E08C86C55B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2BBBFEED-85B6-4361-85B2-F9E08C86C55B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2BBBFEED-85B6-4361-85B2-F9E08C86C55B}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {061F7879-5D14-4C5E-A4A9-782C6FEB1132} - EndGlobalSection -EndGlobal diff --git a/Backend/TodoList.Api/TodoList.Api/Controllers/TodoItemsController.cs b/Backend/TodoList.Api/TodoList.Api/Controllers/TodoItemsController.cs deleted file mode 100644 index c42f2f44..00000000 --- a/Backend/TodoList.Api/TodoList.Api/Controllers/TodoItemsController.cs +++ /dev/null @@ -1,105 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; -using System; -using System.Linq; -using System.Threading.Tasks; - -namespace TodoList.Api.Controllers -{ - [Route("api/[controller]")] - [ApiController] - public class TodoItemsController : ControllerBase - { - private readonly TodoContext _context; - private readonly ILogger _logger; - - public TodoItemsController(TodoContext context, ILogger logger) - { - _context = context; - _logger = logger; - } - - // GET: api/TodoItems - [HttpGet] - public async Task GetTodoItems() - { - var results = await _context.TodoItems.Where(x => !x.IsCompleted).ToListAsync(); - return Ok(results); - } - - // GET: api/TodoItems/... - [HttpGet("{id}")] - public async Task GetTodoItem(Guid id) - { - var result = await _context.TodoItems.FindAsync(id); - - if (result == null) - { - return NotFound(); - } - - return Ok(result); - } - - // PUT: api/TodoItems/... - [HttpPut("{id}")] - public async Task PutTodoItem(Guid id, TodoItem todoItem) - { - if (id != todoItem.Id) - { - return BadRequest(); - } - - _context.Entry(todoItem).State = EntityState.Modified; - - try - { - await _context.SaveChangesAsync(); - } - catch (DbUpdateConcurrencyException) - { - if (!TodoItemIdExists(id)) - { - return NotFound(); - } - else - { - throw; - } - } - - return NoContent(); - } - - // POST: api/TodoItems - [HttpPost] - public async Task PostTodoItem(TodoItem todoItem) - { - if (string.IsNullOrEmpty(todoItem?.Description)) - { - return BadRequest("Description is required"); - } - else if (TodoItemDescriptionExists(todoItem.Description)) - { - return BadRequest("Description already exists"); - } - - _context.TodoItems.Add(todoItem); - await _context.SaveChangesAsync(); - - return CreatedAtAction(nameof(GetTodoItem), new { id = todoItem.Id }, todoItem); - } - - private bool TodoItemIdExists(Guid id) - { - return _context.TodoItems.Any(x => x.Id == id); - } - - private bool TodoItemDescriptionExists(string description) - { - return _context.TodoItems - .Any(x => x.Description.ToLowerInvariant() == description.ToLowerInvariant() && !x.IsCompleted); - } - } -} diff --git a/Backend/TodoList.Api/TodoList.Api/TodoContext.cs b/Backend/TodoList.Api/TodoList.Api/TodoContext.cs deleted file mode 100644 index 068513bf..00000000 --- a/Backend/TodoList.Api/TodoList.Api/TodoContext.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Microsoft.EntityFrameworkCore; - -namespace TodoList.Api -{ - public class TodoContext : DbContext - { - public TodoContext(DbContextOptions options) - : base(options) - { - } - - public DbSet TodoItems { get; set; } - } -} diff --git a/Backend/TodoList.Api/TodoList.Api/appsettings.Development.json b/Backend/TodoList.Api/appsettings.Development.json similarity index 100% rename from Backend/TodoList.Api/TodoList.Api/appsettings.Development.json rename to Backend/TodoList.Api/appsettings.Development.json diff --git a/Backend/TodoList.Api/TodoList.Api/appsettings.json b/Backend/TodoList.Api/appsettings.json similarity index 100% rename from Backend/TodoList.Api/TodoList.Api/appsettings.json rename to Backend/TodoList.Api/appsettings.json diff --git a/Backend/TodoList.Api/TodoList.Api/TodoItem.cs b/Backend/TodoList.Domain/Entities/TodoItem.cs similarity index 82% rename from Backend/TodoList.Api/TodoList.Api/TodoItem.cs rename to Backend/TodoList.Domain/Entities/TodoItem.cs index 75a2774e..05ecd9d7 100644 --- a/Backend/TodoList.Api/TodoList.Api/TodoItem.cs +++ b/Backend/TodoList.Domain/Entities/TodoItem.cs @@ -1,6 +1,4 @@ -using System; - -namespace TodoList.Api +namespace TodoList.Domain.Entities { public class TodoItem { diff --git a/Backend/TodoList.Domain/Repositories/ITodoListRepository.cs b/Backend/TodoList.Domain/Repositories/ITodoListRepository.cs new file mode 100644 index 00000000..a87a1870 --- /dev/null +++ b/Backend/TodoList.Domain/Repositories/ITodoListRepository.cs @@ -0,0 +1,12 @@ +using TodoList.Domain.Entities; + +namespace TodoList.Domain.Repositories +{ + public interface ITodoListRepository + { + Task CreateTodoItem(TodoItem item, CancellationToken token); + Task UpdateTodoItem(TodoItem item, CancellationToken token); + Task GetTodoItem(Guid itemId, CancellationToken token); + Task> GetTodoItems(CancellationToken token); + } +} diff --git a/Backend/TodoList.Domain/TodoList.Domain.csproj b/Backend/TodoList.Domain/TodoList.Domain.csproj new file mode 100644 index 00000000..c589380a --- /dev/null +++ b/Backend/TodoList.Domain/TodoList.Domain.csproj @@ -0,0 +1,9 @@ + + + + net6.0 + enable + disable + + + diff --git a/Backend/TodoList.Infrastructure.Application.Services.UnitTests/ModulesTests/TodoListModuleTests/TodoListServiceTests/CreateTodoItemTests.cs b/Backend/TodoList.Infrastructure.Application.Services.UnitTests/ModulesTests/TodoListModuleTests/TodoListServiceTests/CreateTodoItemTests.cs new file mode 100644 index 00000000..b58ee55f --- /dev/null +++ b/Backend/TodoList.Infrastructure.Application.Services.UnitTests/ModulesTests/TodoListModuleTests/TodoListServiceTests/CreateTodoItemTests.cs @@ -0,0 +1,76 @@ +using FakeItEasy; +using TodoList.Domain.Entities; +using TodoList.Domain.Repositories; +using TodoList.Infrastructure.Application.Services.Modules.TodoListModule; +using TodoList.Services.Modules.TodoListModule.Models; +using TodoList.Services.Shared; + +namespace TodoList.Infrastructure.Application.Services.UnitTests.ModulesTests.TodoListModuleTests.TodoListServiceTests +{ + public class CreateTodoItemTests + { + + [Fact] + public async Task ShouldReturnBadRequestWhenEmptyInputPassed() + { + // Arrange + var cancellationToken = CancellationToken.None; + CreateTodoItemModel itemModel = null; + + var fakeRepository = A.Fake(); + var sut = new TodoListService(fakeRepository); + + // Act + var result = await sut.CreateTodoItem(itemModel, cancellationToken); + + // Assert + Assert.NotNull(result); + Assert.Equal(ResultCode.BadRequest, result.ResultCode); + } + + [Fact] + public async Task ShouldReturnInternalServerErrorWhenNothingReturnedFromRepository() + { + // Arrange + var cancellationToken = CancellationToken.None; + var itemModel = new CreateTodoItemModel(); + TodoItem item = null; + + var fakeRepository = A.Fake(); + A.CallTo(() => fakeRepository.CreateTodoItem(A._, cancellationToken)) + .Returns(item); + + var sut = new TodoListService(fakeRepository); + + // Act + var result = await sut.CreateTodoItem(itemModel, cancellationToken); + + // Assert + Assert.NotNull(result); + Assert.Equal(ResultCode.InternalServerError, result.ResultCode); + } + + [Fact] + public async Task ShouldReturnResourceCreatedResultWhenValidInputPassed() + { + // Arrange + var cancellationToken = CancellationToken.None; + var description = "Test"; + var itemModel = new CreateTodoItemModel { Description = description }; + + var fakeRepository = A.Fake(); + A.CallTo(() => fakeRepository.CreateTodoItem(A._, cancellationToken)) + .Returns(Task.FromResult(new TodoItem { Description = description })); + + var service = new TodoListService(fakeRepository); + + // Act + var result = await service.CreateTodoItem(itemModel, cancellationToken); + + // Assert + Assert.NotNull(result); + Assert.NotNull(result.Result); + Assert.Equal(description, result.Result.Description); + } + } +} diff --git a/Backend/TodoList.Infrastructure.Application.Services.UnitTests/TodoList.Infrastructure.Application.Services.UnitTests.csproj b/Backend/TodoList.Infrastructure.Application.Services.UnitTests/TodoList.Infrastructure.Application.Services.UnitTests.csproj new file mode 100644 index 00000000..a3e035d7 --- /dev/null +++ b/Backend/TodoList.Infrastructure.Application.Services.UnitTests/TodoList.Infrastructure.Application.Services.UnitTests.csproj @@ -0,0 +1,30 @@ + + + + net6.0 + enable + enable + + false + true + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/Backend/TodoList.Infrastructure.Application.Services.UnitTests/Usings.cs b/Backend/TodoList.Infrastructure.Application.Services.UnitTests/Usings.cs new file mode 100644 index 00000000..8c927eb7 --- /dev/null +++ b/Backend/TodoList.Infrastructure.Application.Services.UnitTests/Usings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/Backend/TodoList.Infrastructure.Application.Services/Extensions/ServiceCollectionExtensions.cs b/Backend/TodoList.Infrastructure.Application.Services/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 00000000..b5b92dd3 --- /dev/null +++ b/Backend/TodoList.Infrastructure.Application.Services/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,27 @@ +using FluentValidation; +using FluentValidation.AspNetCore; +using Microsoft.Extensions.DependencyInjection; +using TodoList.Infrastructure.Application.Services.Modules.TodoListModule; +using TodoList.Services.Modules.TodoListModule; +using TodoList.Services.Modules.TodoListModule.Validations; + +namespace TodoList.Infrastructure.Application.Services.Extensions +{ + public static class ServiceCollectionExtensions + { + public static void AddAppServices(this IServiceCollection services) + { + services.AddValidation(); + services.AddTransient(); + } + + private static void AddValidation(this IServiceCollection services) + { + services.AddFluentValidationAutoValidation(_ => + { + _.DisableDataAnnotationsValidation = true; + }); + services.AddValidatorsFromAssemblyContaining(); + } + } +} \ No newline at end of file diff --git a/Backend/TodoList.Infrastructure.Application.Services/MappingProfiles/TodoListProfile.cs b/Backend/TodoList.Infrastructure.Application.Services/MappingProfiles/TodoListProfile.cs new file mode 100644 index 00000000..312f5613 --- /dev/null +++ b/Backend/TodoList.Infrastructure.Application.Services/MappingProfiles/TodoListProfile.cs @@ -0,0 +1,24 @@ +using Mapster; +using TodoList.Domain.Entities; +using TodoList.Services.Modules.TodoListModule.Models; +using TodoList.Services.Modules.TodoListModule.ViewModels; + +namespace TodoList.Infrastructure.Persistence.EFCore.MappingProfiles +{ + public class TodoListProfile : IRegister + { + public void Register(TypeAdapterConfig config) + { + config.ForType() + .Map(dest => dest.Id, src => Guid.NewGuid()) + .Map(dest => dest.Description, src => src.Description) + .Map(dest => dest.IsCompleted, src => false); + + + config.ForType() + .Map(dest => dest.Id, src => src.Id) + .Map(dest => dest.Description, src => src.Description) + .Map(dest => dest.IsCompleted, src => src.IsCompleted); + } + } +} diff --git a/Backend/TodoList.Infrastructure.Application.Services/Modules/TodoListModule/TodoListService.cs b/Backend/TodoList.Infrastructure.Application.Services/Modules/TodoListModule/TodoListService.cs new file mode 100644 index 00000000..ce3b3c94 --- /dev/null +++ b/Backend/TodoList.Infrastructure.Application.Services/Modules/TodoListModule/TodoListService.cs @@ -0,0 +1,73 @@ +using Mapster; +using TodoList.Domain.Entities; +using TodoList.Domain.Repositories; +using TodoList.Services.Modules.TodoListModule; +using TodoList.Services.Modules.TodoListModule.Models; +using TodoList.Services.Modules.TodoListModule.ViewModels; +using TodoList.Services.Shared; + +namespace TodoList.Infrastructure.Application.Services.Modules.TodoListModule +{ + internal class TodoListService : ITodoListService + { + private readonly ITodoListRepository todoListRepository; + public TodoListService(ITodoListRepository todoListRepository) + { + this.todoListRepository = todoListRepository; + } + public async Task> CreateTodoItem(CreateTodoItemModel item, CancellationToken cancellationToken) + { + var result = new ServiceResult(); + if (item == null) + { + result.BadRequestWithMessage("Description is required"); + return result; + } + + var newItem = await this.todoListRepository.CreateTodoItem(item.Adapt(), cancellationToken).ConfigureAwait(false); + if (newItem != null) + { + return result.ToResourceCreatedResult(newItem.Adapt()); + } + + result.WithResultCode(ResultCode.InternalServerError, "An unknown error occurred while creating TodoList Item"); + return result; + } + + public async Task> GetItem(Guid id, CancellationToken cancellationToken) + { + var result = new ServiceResult(); + var item = await this.todoListRepository.GetTodoItem(id, cancellationToken).ConfigureAwait(false); + if (item == null) + { + result.WithResultCode(ResultCode.NotFound, $"Item ({id}) not found"); + return result; + } + + return result.ToSuccessResult(item.Adapt()); + } + + public async Task> GetItems(CancellationToken token) + { + var items= await this.todoListRepository.GetTodoItems(token).ConfigureAwait(false); + return items.Adapt>(); + } + + public async Task UpdateTodoItemStatus(Guid id, UpdateTodoItemModel item, CancellationToken cancellationToken) + { + var result = new ServiceResult(); + var originalItem = await this.todoListRepository.GetTodoItem(id, cancellationToken).ConfigureAwait(false); + if (originalItem == null) + { + result.WithResultCode(ResultCode.NotFound, $"Item ({id}) not found"); + return result; + } + + originalItem.IsCompleted = item.IsCompleted; + + var isUpdated = await this.todoListRepository.UpdateTodoItem(originalItem, cancellationToken).ConfigureAwait(false); + + return result.WithResultCode(isUpdated ? ResultCode.Updated : ResultCode.InternalServerError); + } + } +} diff --git a/Backend/TodoList.Infrastructure.Application.Services/TodoList.Infrastructure.Application.Services.csproj b/Backend/TodoList.Infrastructure.Application.Services/TodoList.Infrastructure.Application.Services.csproj new file mode 100644 index 00000000..ea2377de --- /dev/null +++ b/Backend/TodoList.Infrastructure.Application.Services/TodoList.Infrastructure.Application.Services.csproj @@ -0,0 +1,25 @@ + + + + net6.0 + enable + disable + + + + + + + + + + + + + + + + <_Parameter1>TodoList.Infrastructure.Application.Services.UnitTests + + + diff --git a/Backend/TodoList.Infrastructure.Persistence.EFCore.UnitTests/RepositoriesTests/TodoListRepositoryTests/CreateTodoItemTests.cs b/Backend/TodoList.Infrastructure.Persistence.EFCore.UnitTests/RepositoriesTests/TodoListRepositoryTests/CreateTodoItemTests.cs new file mode 100644 index 00000000..9cf698ba --- /dev/null +++ b/Backend/TodoList.Infrastructure.Persistence.EFCore.UnitTests/RepositoriesTests/TodoListRepositoryTests/CreateTodoItemTests.cs @@ -0,0 +1,35 @@ +using Microsoft.EntityFrameworkCore; +using TodoList.Domain.Entities; +using TodoList.Infrastructure.Persistence.EFCore.Repositories; + +namespace TodoList.Infrastructure.Persistence.EFCore.UnitTests.RepositoriesTests.TodoListRepositoryTests +{ + public class CreateTodoItemTests + { + [Fact] + public async Task ShoudlCreateAnItemWhenCalled() + { + // Arrange + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: "CreateTodoItemDatabase") + .Options; + var id = Guid.NewGuid(); + var description = "Test"; + + using (var context = new TodoListContext(options)) + { + var repository = new TodoListRepository(context); + var item = new TodoItem { Id = id, Description = description }; + + // Act + await repository.CreateTodoItem(item, CancellationToken.None); + + // Assert + var items = context.TodoItems; + Assert.Single(items); + Assert.Equal(id, items.First().Id); + Assert.Equal(description, items.First().Description); + } + } + } +} diff --git a/Backend/TodoList.Infrastructure.Persistence.EFCore.UnitTests/TodoList.Infrastructure.Persistence.EFCore.UnitTests.csproj b/Backend/TodoList.Infrastructure.Persistence.EFCore.UnitTests/TodoList.Infrastructure.Persistence.EFCore.UnitTests.csproj new file mode 100644 index 00000000..c3b0c7f0 --- /dev/null +++ b/Backend/TodoList.Infrastructure.Persistence.EFCore.UnitTests/TodoList.Infrastructure.Persistence.EFCore.UnitTests.csproj @@ -0,0 +1,30 @@ + + + + net6.0 + enable + enable + + false + true + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/Backend/TodoList.Infrastructure.Persistence.EFCore.UnitTests/Usings.cs b/Backend/TodoList.Infrastructure.Persistence.EFCore.UnitTests/Usings.cs new file mode 100644 index 00000000..8c927eb7 --- /dev/null +++ b/Backend/TodoList.Infrastructure.Persistence.EFCore.UnitTests/Usings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/Backend/TodoList.Infrastructure.Persistence.EFCore/Configurations/TodoItemConfiguration.cs b/Backend/TodoList.Infrastructure.Persistence.EFCore/Configurations/TodoItemConfiguration.cs new file mode 100644 index 00000000..be7d4991 --- /dev/null +++ b/Backend/TodoList.Infrastructure.Persistence.EFCore/Configurations/TodoItemConfiguration.cs @@ -0,0 +1,16 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using TodoList.Domain.Entities; + +namespace TodoList.Infrastructure.Persistence.EFCore.Configurations +{ + internal class TodoItemConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(x => x.Id); + builder.Property(x => x.Id).ValueGeneratedOnAdd(); + builder.Property(x => x.Description).HasMaxLength(100); + } + } +} diff --git a/Backend/TodoList.Infrastructure.Persistence.EFCore/Extensions/ServiceCollectionExtensions.cs b/Backend/TodoList.Infrastructure.Persistence.EFCore/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 00000000..b3317066 --- /dev/null +++ b/Backend/TodoList.Infrastructure.Persistence.EFCore/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,16 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using TodoList.Domain.Repositories; +using TodoList.Infrastructure.Persistence.EFCore.Repositories; + +namespace TodoList.Infrastructure.Persistence.EFCore.Extensions +{ + public static class ServiceCollectionExtensions + { + public static void AddInMemoryPersistence(this IServiceCollection services) + { + services.AddDbContext(opt => opt.UseInMemoryDatabase("TodoItemsDB")); + services.AddTransient(); + } + } +} diff --git a/Backend/TodoList.Infrastructure.Persistence.EFCore/Repositories/TodoListRepository.cs b/Backend/TodoList.Infrastructure.Persistence.EFCore/Repositories/TodoListRepository.cs new file mode 100644 index 00000000..fc38524c --- /dev/null +++ b/Backend/TodoList.Infrastructure.Persistence.EFCore/Repositories/TodoListRepository.cs @@ -0,0 +1,39 @@ +using Microsoft.EntityFrameworkCore; +using TodoList.Domain.Entities; +using TodoList.Domain.Repositories; + +namespace TodoList.Infrastructure.Persistence.EFCore.Repositories +{ + internal class TodoListRepository : ITodoListRepository + { + private readonly TodoListContext todoListContext; + public TodoListRepository(TodoListContext todoListContext) + { + this.todoListContext = todoListContext; + } + + public async Task CreateTodoItem(TodoItem item, CancellationToken token) + { + await this.todoListContext.TodoItems.AddAsync(item, token).ConfigureAwait(false); + await this.todoListContext.SaveChangesAsync(token).ConfigureAwait(false); + return item; + } + + public async Task GetTodoItem(Guid itemId, CancellationToken token) + { + return await this.todoListContext.FindAsync(itemId, token).ConfigureAwait(false); + } + + public async Task> GetTodoItems(CancellationToken token) + { + return await this.todoListContext.TodoItems.ToArrayAsync(token).ConfigureAwait(false); + } + + public async Task UpdateTodoItem(TodoItem item, CancellationToken token) + { + this.todoListContext.Update(item); + await this.todoListContext.SaveChangesAsync(token).ConfigureAwait(false); + return true; + } + } +} diff --git a/Backend/TodoList.Infrastructure.Persistence.EFCore/TodoList.Infrastructure.Persistence.EFCore.csproj b/Backend/TodoList.Infrastructure.Persistence.EFCore/TodoList.Infrastructure.Persistence.EFCore.csproj new file mode 100644 index 00000000..05ed1a50 --- /dev/null +++ b/Backend/TodoList.Infrastructure.Persistence.EFCore/TodoList.Infrastructure.Persistence.EFCore.csproj @@ -0,0 +1,24 @@ + + + + net6.0 + enable + disable + + + + + + + + + + + + + + + <_Parameter1>TodoList.Infrastructure.Persistence.EFCore.UnitTests + + + diff --git a/Backend/TodoList.Infrastructure.Persistence.EFCore/TodoListContext.cs b/Backend/TodoList.Infrastructure.Persistence.EFCore/TodoListContext.cs new file mode 100644 index 00000000..889d9430 --- /dev/null +++ b/Backend/TodoList.Infrastructure.Persistence.EFCore/TodoListContext.cs @@ -0,0 +1,14 @@ +using Microsoft.EntityFrameworkCore; +using TodoList.Domain.Entities; + +namespace TodoList.Infrastructure.Persistence.EFCore +{ + internal class TodoListContext : DbContext + { + public TodoListContext(DbContextOptions options) : base(options) + { + } + + public DbSet TodoItems { get; set; } + } +} diff --git a/Backend/TodoList.Services/Exceptions/TodoItemNotFoundException.cs b/Backend/TodoList.Services/Exceptions/TodoItemNotFoundException.cs new file mode 100644 index 00000000..6758b731 --- /dev/null +++ b/Backend/TodoList.Services/Exceptions/TodoItemNotFoundException.cs @@ -0,0 +1,17 @@ +namespace TodoList.Services.Exceptions +{ + public class TodoItemNotFoundException : Exception + { + public TodoItemNotFoundException() + { + } + + public TodoItemNotFoundException(string? message) : base(message) + { + } + + public TodoItemNotFoundException(string? message, Exception? innerException) : base(message, innerException) + { + } + } +} diff --git a/Backend/TodoList.Services/Modules/TodoListModule/ITodoListService.cs b/Backend/TodoList.Services/Modules/TodoListModule/ITodoListService.cs new file mode 100644 index 00000000..cda6cfdb --- /dev/null +++ b/Backend/TodoList.Services/Modules/TodoListModule/ITodoListService.cs @@ -0,0 +1,14 @@ +using TodoList.Services.Modules.TodoListModule.Models; +using TodoList.Services.Modules.TodoListModule.ViewModels; +using TodoList.Services.Shared; + +namespace TodoList.Services.Modules.TodoListModule +{ + public interface ITodoListService + { + Task> CreateTodoItem(CreateTodoItemModel item, CancellationToken cancellationToken); + Task UpdateTodoItemStatus(Guid id, UpdateTodoItemModel item, CancellationToken cancellationToken); + Task> GetItem(Guid id, CancellationToken cancellationToken); + Task> GetItems(CancellationToken token); + } +} diff --git a/Backend/TodoList.Services/Modules/TodoListModule/Models/CreateTodoItemModel.cs b/Backend/TodoList.Services/Modules/TodoListModule/Models/CreateTodoItemModel.cs new file mode 100644 index 00000000..99f63984 --- /dev/null +++ b/Backend/TodoList.Services/Modules/TodoListModule/Models/CreateTodoItemModel.cs @@ -0,0 +1,7 @@ +namespace TodoList.Services.Modules.TodoListModule.Models +{ + public class CreateTodoItemModel + { + public string Description { get; set; } + } +} diff --git a/Backend/TodoList.Services/Modules/TodoListModule/Models/UpdateTodoItemModel.cs b/Backend/TodoList.Services/Modules/TodoListModule/Models/UpdateTodoItemModel.cs new file mode 100644 index 00000000..aa7aa265 --- /dev/null +++ b/Backend/TodoList.Services/Modules/TodoListModule/Models/UpdateTodoItemModel.cs @@ -0,0 +1,7 @@ +namespace TodoList.Services.Modules.TodoListModule.Models +{ + public class UpdateTodoItemModel + { + public bool IsCompleted { get; set; } + } +} diff --git a/Backend/TodoList.Services/Modules/TodoListModule/Validations/CreateTodoItemModelValidator.cs b/Backend/TodoList.Services/Modules/TodoListModule/Validations/CreateTodoItemModelValidator.cs new file mode 100644 index 00000000..46a447c4 --- /dev/null +++ b/Backend/TodoList.Services/Modules/TodoListModule/Validations/CreateTodoItemModelValidator.cs @@ -0,0 +1,13 @@ +using FluentValidation; +using TodoList.Services.Modules.TodoListModule.Models; + +namespace TodoList.Services.Modules.TodoListModule.Validations +{ + public class CreateTodoItemModelValidator : AbstractValidator + { + public CreateTodoItemModelValidator() + { + RuleFor(_ => _.Description).NotEmpty().MaximumLength(100); + } + } +} diff --git a/Backend/TodoList.Services/Modules/TodoListModule/ViewModels/GetTodoItemViewModel.cs b/Backend/TodoList.Services/Modules/TodoListModule/ViewModels/GetTodoItemViewModel.cs new file mode 100644 index 00000000..b1c0acc6 --- /dev/null +++ b/Backend/TodoList.Services/Modules/TodoListModule/ViewModels/GetTodoItemViewModel.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TodoList.Services.Modules.TodoListModule.ViewModels +{ + public class GetTodoItemViewModel + { + public Guid Id { get; set; } + public string Description { get; set; } + public bool IsCompleted { get; set; } + } +} diff --git a/Backend/TodoList.Services/Shared/ServiceResult.cs b/Backend/TodoList.Services/Shared/ServiceResult.cs new file mode 100644 index 00000000..52df0b8a --- /dev/null +++ b/Backend/TodoList.Services/Shared/ServiceResult.cs @@ -0,0 +1,57 @@ +namespace TodoList.Services.Shared +{ + public class ServiceResult + { + public ResultCode ResultCode { get; internal set; } + public string Message { get; internal set; } + public List Errors { get; internal set; } + public bool HasErrors => Errors is { Count: > 0 }; + public ServiceResult() + { + Errors = new List(); + } + } + + public class ServiceResult : ServiceResult + { + public TModel Result { get; internal set; } + } + + public enum ResultCode + { + Success, + Created, + Updated, + BadRequest, + InternalServerError, + NotFound + } + + public static class ServiceResultExtension + { + public static ServiceResult BadRequestWithMessage(this ServiceResult serviceResult, string message) + { + serviceResult.ResultCode = ResultCode.BadRequest; + serviceResult.Message = message; + return serviceResult; + } + public static ServiceResult WithResultCode(this ServiceResult serviceResult, ResultCode resultCode, string message = "") + { + serviceResult.ResultCode = resultCode; + serviceResult.Message = message; + return serviceResult; + } + public static ServiceResult ToResourceCreatedResult(this ServiceResult serviceResult, TModel result) + { + serviceResult.ResultCode = ResultCode.Created; + serviceResult.Result = result; + return serviceResult; + } + public static ServiceResult ToSuccessResult(this ServiceResult serviceResult, TModel result) + { + serviceResult.ResultCode = ResultCode.Success; + serviceResult.Result = result; + return serviceResult; + } + } +} diff --git a/Backend/TodoList.Services/TodoList.Services.csproj b/Backend/TodoList.Services/TodoList.Services.csproj new file mode 100644 index 00000000..a97d23ba --- /dev/null +++ b/Backend/TodoList.Services/TodoList.Services.csproj @@ -0,0 +1,13 @@ + + + + net6.0 + enable + disable + + + + + + + diff --git a/Backend/TodoList.Shared/TodoList.Shared.csproj b/Backend/TodoList.Shared/TodoList.Shared.csproj new file mode 100644 index 00000000..132c02c5 --- /dev/null +++ b/Backend/TodoList.Shared/TodoList.Shared.csproj @@ -0,0 +1,9 @@ + + + + net6.0 + enable + enable + + +