diff --git a/.gitignore b/.gitignore index 4ce6fdde..508afea0 100644 --- a/.gitignore +++ b/.gitignore @@ -29,7 +29,9 @@ bld/ # Visual Studio 2015/2017 cache/options directory .vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot +# Uncomment if you have tasks th + +create the project's static files in wwwroot #wwwroot/ # Visual Studio 2017 auto generated files diff --git a/Controllers/TodoItemsController.cs b/Controllers/TodoItemsController.cs deleted file mode 100644 index 0ef138e7..00000000 --- a/Controllers/TodoItemsController.cs +++ /dev/null @@ -1,116 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using TodoApi.Models; - -namespace TodoApi.Controllers -{ - [Route("api/[controller]")] - [ApiController] - public class TodoItemsController : ControllerBase - { - private readonly TodoContext _context; - - public TodoItemsController(TodoContext context) - { - _context = context; - } - - [HttpGet] - public async Task>> GetTodoItems() - { - return await _context.TodoItems - .Select(x => ItemToDTO(x)) - .ToListAsync(); - } - - [HttpGet("{id}")] - public async Task> GetTodoItem(long id) - { - var todoItem = await _context.TodoItems.FindAsync(id); - - if (todoItem == null) - { - return NotFound(); - } - - return ItemToDTO(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(); - } - - [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)); - } - - [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(); - } - - 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/TodoItem.cs b/Models/TodoItem.cs deleted file mode 100644 index 1f6e5465..00000000 --- a/Models/TodoItem.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace TodoApi.Models -{ - #region snippet - public class TodoItem - { - public long Id { get; set; } - public string Name { get; set; } - public bool IsComplete { get; set; } - public string Secret { get; set; } - } - #endregion -} \ No newline at end of file diff --git a/Models/TodoItemDTO.cs b/Models/TodoItemDTO.cs deleted file mode 100644 index e66a500a..00000000 --- a/Models/TodoItemDTO.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace TodoApi.Models -{ - #region snippet - public class TodoItemDTO - { - public long Id { get; set; } - public string Name { get; set; } - public bool IsComplete { get; set; } - } - #endregion -} diff --git a/Program.cs b/Program.cs deleted file mode 100644 index b27ac16a..00000000 --- a/Program.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; - -namespace TodoApi -{ - public class Program - { - public static void Main(string[] args) - { - CreateHostBuilder(args).Build().Run(); - } - - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - }); - } -} diff --git a/Startup.cs b/Startup.cs deleted file mode 100644 index bbfbc83d..00000000 --- a/Startup.cs +++ /dev/null @@ -1,55 +0,0 @@ -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; - -namespace TodoApi -{ - public class Startup - { - public Startup(IConfiguration configuration) - { - Configuration = 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.AddDbContext(opt => - opt.UseInMemoryDatabase("TodoList")); - 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) - { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - - app.UseHttpsRedirection(); - - app.UseRouting(); - - app.UseAuthorization(); - - app.UseEndpoints(endpoints => - { - endpoints.MapControllers(); - }); - } - } -} diff --git a/TodoApiDTO.csproj b/TodoApiDTO.csproj deleted file mode 100644 index bba6f6af..00000000 --- a/TodoApiDTO.csproj +++ /dev/null @@ -1,18 +0,0 @@ - - - - netcoreapp3.1 - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - diff --git a/TodoApiDTO.sln b/TodoApiDTO.sln index e49c182b..76460dfe 100644 --- a/TodoApiDTO.sln +++ b/TodoApiDTO.sln @@ -1,9 +1,15 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.30002.166 +# Visual Studio Version 17 +VisualStudioVersion = 17.1.32328.378 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TodoApiDTO", "TodoApiDTO.csproj", "{623124F9-F5BA-42DD-BC26-A1720774229C}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Velvetech.MyTodoApp.Application", "Velvetech.MyTodoApp.Application\Velvetech.MyTodoApp.Application.csproj", "{28CC11DB-280A-4279-9216-E6BB508662B5}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Velvetech.TodoApp.Domain", "Velvetech.TodoApp.Domain\Velvetech.TodoApp.Domain.csproj", "{0C3B04A6-DEA0-4EE7-8B54-9F8B82AC5366}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Velvetech.TodoApp.Infrastructure", "Velvetech.TodoApp.Infrastructure\Velvetech.TodoApp.Infrastructure.csproj", "{096B63B5-9459-47E8-8AE5-C85E472EBDC0}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Velvetech.MyTodoApp.WebAPI", "Velvetech.MyTodoApp.WebAPI\Velvetech.MyTodoApp.WebAPI.csproj", "{914893BF-04DF-4C3D-82D7-D33E7819422F}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -11,10 +17,22 @@ Global Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {623124F9-F5BA-42DD-BC26-A1720774229C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {623124F9-F5BA-42DD-BC26-A1720774229C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {623124F9-F5BA-42DD-BC26-A1720774229C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {623124F9-F5BA-42DD-BC26-A1720774229C}.Release|Any CPU.Build.0 = Release|Any CPU + {28CC11DB-280A-4279-9216-E6BB508662B5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {28CC11DB-280A-4279-9216-E6BB508662B5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {28CC11DB-280A-4279-9216-E6BB508662B5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {28CC11DB-280A-4279-9216-E6BB508662B5}.Release|Any CPU.Build.0 = Release|Any CPU + {0C3B04A6-DEA0-4EE7-8B54-9F8B82AC5366}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0C3B04A6-DEA0-4EE7-8B54-9F8B82AC5366}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0C3B04A6-DEA0-4EE7-8B54-9F8B82AC5366}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0C3B04A6-DEA0-4EE7-8B54-9F8B82AC5366}.Release|Any CPU.Build.0 = Release|Any CPU + {096B63B5-9459-47E8-8AE5-C85E472EBDC0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {096B63B5-9459-47E8-8AE5-C85E472EBDC0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {096B63B5-9459-47E8-8AE5-C85E472EBDC0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {096B63B5-9459-47E8-8AE5-C85E472EBDC0}.Release|Any CPU.Build.0 = Release|Any CPU + {914893BF-04DF-4C3D-82D7-D33E7819422F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {914893BF-04DF-4C3D-82D7-D33E7819422F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {914893BF-04DF-4C3D-82D7-D33E7819422F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {914893BF-04DF-4C3D-82D7-D33E7819422F}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Velvetech.MyTodoApp.Application/Config/InfrastructureDependencyRegistrations.cs b/Velvetech.MyTodoApp.Application/Config/InfrastructureDependencyRegistrations.cs new file mode 100644 index 00000000..40375242 --- /dev/null +++ b/Velvetech.MyTodoApp.Application/Config/InfrastructureDependencyRegistrations.cs @@ -0,0 +1,16 @@ +using Microsoft.Extensions.DependencyInjection; +using Velvetech.MyTodoApp.Application.Services.Abstractions; +using Velvetech.MyTodoApp.Application.Services.Implementations; + +namespace Velvetech.TodoApp.Infrastructure.Config +{ + public static class InfrastructureDependencyRegistrations + { + public static void AddApplication(this IServiceCollection services) + { + services.AddTransient(); + services.AddTransient(); + services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies()); + } + } +} diff --git a/Velvetech.MyTodoApp.Application/DTOs/TodoItemCreateDto.cs b/Velvetech.MyTodoApp.Application/DTOs/TodoItemCreateDto.cs new file mode 100644 index 00000000..0bf00f4d --- /dev/null +++ b/Velvetech.MyTodoApp.Application/DTOs/TodoItemCreateDto.cs @@ -0,0 +1,4 @@ +namespace Velvetech.MyTodoApp.Application.DTOs +{ + public record TodoItemCreateDto(string Name, bool IsComplete); +} diff --git a/Velvetech.MyTodoApp.Application/DTOs/TodoItemReadDto.cs b/Velvetech.MyTodoApp.Application/DTOs/TodoItemReadDto.cs new file mode 100644 index 00000000..a9733e87 --- /dev/null +++ b/Velvetech.MyTodoApp.Application/DTOs/TodoItemReadDto.cs @@ -0,0 +1,4 @@ +namespace Velvetech.MyTodoApp.Application.DTOs +{ + public record TodoItemReadDto(Guid Id, string Name, bool IsComplete); +} diff --git a/Velvetech.MyTodoApp.Application/DTOs/TodoItemUpdateDto.cs b/Velvetech.MyTodoApp.Application/DTOs/TodoItemUpdateDto.cs new file mode 100644 index 00000000..ee2ead67 --- /dev/null +++ b/Velvetech.MyTodoApp.Application/DTOs/TodoItemUpdateDto.cs @@ -0,0 +1,4 @@ +namespace Velvetech.MyTodoApp.Application.DTOs +{ + public record TodoItemUpdateDto(Guid Id, string Name, bool IsComplete); +} diff --git a/Velvetech.MyTodoApp.Application/Exceptions/EntityNotFoundException.cs b/Velvetech.MyTodoApp.Application/Exceptions/EntityNotFoundException.cs new file mode 100644 index 00000000..99fd0816 --- /dev/null +++ b/Velvetech.MyTodoApp.Application/Exceptions/EntityNotFoundException.cs @@ -0,0 +1,17 @@ +namespace Velvetech.MyTodoApp.Application.Exceptions +{ + public class EntityNotFoundException : Exception + { + public EntityNotFoundException() : base("Entity not found.") + { + } + + public EntityNotFoundException(string message) : base(message) + { + } + + public EntityNotFoundException(string message, Exception innerException) : base(message, innerException) + { + } + } +} diff --git a/Velvetech.MyTodoApp.Application/Profiles/TodoItemProfile.cs b/Velvetech.MyTodoApp.Application/Profiles/TodoItemProfile.cs new file mode 100644 index 00000000..597a770e --- /dev/null +++ b/Velvetech.MyTodoApp.Application/Profiles/TodoItemProfile.cs @@ -0,0 +1,17 @@ +using AutoMapper; +using Velvetech.MyTodoApp.Application.DTOs; +using Velvetech.TodoApp.Domain.Entities; + +namespace Velvetech.MyTodoApp.Application.Profiles +{ + public class TodoItemProfile : Profile + { + // Source -> Target + public TodoItemProfile() + { + CreateMap().ReverseMap(); + CreateMap().ReverseMap(); + CreateMap().ReverseMap(); + } + } +} diff --git a/Velvetech.MyTodoApp.Application/Services/Abstractions/ITodoItemService.cs b/Velvetech.MyTodoApp.Application/Services/Abstractions/ITodoItemService.cs new file mode 100644 index 00000000..3389bb28 --- /dev/null +++ b/Velvetech.MyTodoApp.Application/Services/Abstractions/ITodoItemService.cs @@ -0,0 +1,16 @@ +using System.Linq.Expressions; +using Velvetech.MyTodoApp.Application.DTOs; +using Velvetech.TodoApp.Domain.Entities; + +namespace Velvetech.MyTodoApp.Application.Services.Abstractions +{ + public interface ITodoItemService + { + Task> GetTodoItemsAsync(Expression> predicate); + Task> GetTodoItemsAsync(); + Task GetTodoItemAsync(Expression> predicate); + Task AddTodoItemAsync(TodoItemCreateDto todoItemDto); + Task UpdateTodoItemAsync(TodoItemUpdateDto todoItemDto); + Task DeleteTodoItemAsync(Guid id); + } +} diff --git a/Velvetech.MyTodoApp.Application/Services/Implementations/TodoItemService.cs b/Velvetech.MyTodoApp.Application/Services/Implementations/TodoItemService.cs new file mode 100644 index 00000000..2bb5a9d4 --- /dev/null +++ b/Velvetech.MyTodoApp.Application/Services/Implementations/TodoItemService.cs @@ -0,0 +1,76 @@ +using AutoMapper; +using System.Linq.Expressions; +using Velvetech.MyTodoApp.Application.DTOs; +using Velvetech.MyTodoApp.Application.Exceptions; +using Velvetech.MyTodoApp.Application.Services.Abstractions; +using Velvetech.TodoApp.Domain.Entities; +using Velvetech.TodoApp.Infrastructure.Repositories.Abstractions.Custom; + +namespace Velvetech.MyTodoApp.Application.Services.Implementations +{ + public class TodoItemService : ITodoItemService + { + private readonly ITodoItemRepository _repository; + private readonly IMapper _mapper; + + public TodoItemService(ITodoItemRepository repository, IMapper mapper) + { + _repository = repository; + _mapper = mapper; + } + + public async Task> GetTodoItemsAsync(Expression> predicate) + { + return _mapper.Map>(await _repository.GetAsync(predicate)); + } + + public async Task> GetTodoItemsAsync() + { + return _mapper.Map>(await _repository.GetAsync()); + } + + public async Task GetTodoItemAsync(Expression> predicate) + { + TodoItemEntity entity = await _repository.GetFirstOrDefaultAsync(predicate); + if (entity is null) + { + throw new EntityNotFoundException(); + } + + return _mapper.Map(entity); + } + + public async Task AddTodoItemAsync(TodoItemCreateDto todoItemDto) + { + TodoItemEntity todoItemEntity = _mapper.Map(todoItemDto); + + return _mapper.Map(await _repository.AddAsync(todoItemEntity)); + } + + public async Task UpdateTodoItemAsync(TodoItemUpdateDto todoItemDto) + { + await ValidateId(todoItemDto.Id); + + TodoItemEntity todoItemEntity = _mapper.Map(todoItemDto); + + return _mapper.Map(await _repository.UpdateAsync(todoItemEntity)); + } + + public async Task DeleteTodoItemAsync(Guid id) + { + await ValidateId(id); + + TodoItemEntity todoItemEntity = await _repository.GetFirstOrDefaultAsync(o => o.Id == id); + + return await _repository.DeleteAsync(todoItemEntity); + } + + private async Task ValidateId(Guid id) + { + if (!await _repository.AnyAsync(o => o.Id == id)) + { + throw new EntityNotFoundException(); + } + } + } +} diff --git a/Velvetech.MyTodoApp.Application/Velvetech.MyTodoApp.Application.csproj b/Velvetech.MyTodoApp.Application/Velvetech.MyTodoApp.Application.csproj new file mode 100644 index 00000000..f02492a1 --- /dev/null +++ b/Velvetech.MyTodoApp.Application/Velvetech.MyTodoApp.Application.csproj @@ -0,0 +1,18 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + + diff --git a/Velvetech.MyTodoApp.WebAPI/Controllers/TodoItemsController.cs b/Velvetech.MyTodoApp.WebAPI/Controllers/TodoItemsController.cs new file mode 100644 index 00000000..56ccae61 --- /dev/null +++ b/Velvetech.MyTodoApp.WebAPI/Controllers/TodoItemsController.cs @@ -0,0 +1,79 @@ +using Microsoft.AspNetCore.Mvc; +using Velvetech.MyTodoApp.Application.DTOs; +using Velvetech.MyTodoApp.Application.Services.Abstractions; + +namespace Velvetech.MyTodoApp.WebAPI.Controllers +{ + [Route("api/v1/[controller]")] + [ApiController] + public class TodoItemsController : ControllerBase + { + private readonly ITodoItemService _todoItemService; + + public ITodoItemService TodoItemService => _todoItemService; + + public TodoItemsController(ITodoItemService todoItemService) + { + _todoItemService = todoItemService; + } + + [HttpGet] + public async Task>> GetTodoItems() + { + IEnumerable todoItems = await TodoItemService.GetTodoItemsAsync(); + return Ok(todoItems); + } + + [HttpGet("{id}", Name = "GetTodoItem")] + public async Task> GetTodoItem(Guid id) + { + TodoItemReadDto todoItem = await TodoItemService.GetTodoItemAsync(o => o.Id == id); + + if (todoItem is null) + { + return NotFound(); + } + + return Ok(todoItem); + } + + [HttpPut("{id}")] + public async Task UpdateTodoItem(Guid id, TodoItemUpdateDto todoItemDto) + { + if (id != todoItemDto.Id) + { + return BadRequest(); + } + + TodoItemReadDto updatedTodoItem = await TodoItemService.UpdateTodoItemAsync(todoItemDto); + + if (updatedTodoItem is null) + { + return NotFound(); + } + + return Ok(updatedTodoItem); + } + + [HttpPost] + public async Task> CreateTodoItem(TodoItemCreateDto todoItemDto) + { + TodoItemReadDto createdTodoItem = await TodoItemService.AddTodoItemAsync(todoItemDto); + + return CreatedAtRoute("GetTodoItem", new { id = createdTodoItem.Id }, createdTodoItem); + } + + [HttpDelete("{id}")] + public async Task DeleteTodoItem(Guid id) + { + bool result = await TodoItemService.DeleteTodoItemAsync(id); + + if (!result) + { + return NotFound(); + } + + return NoContent(); + } + } +} diff --git a/Velvetech.MyTodoApp.WebAPI/Logs/log-20230523.txt b/Velvetech.MyTodoApp.WebAPI/Logs/log-20230523.txt new file mode 100644 index 00000000..d802c237 --- /dev/null +++ b/Velvetech.MyTodoApp.WebAPI/Logs/log-20230523.txt @@ -0,0 +1 @@ +2023-05-23 18:31:52.287 +04:00 [Error] HTTP "DELETE" "/api/v1/TodoItems/3fa85f64-5717-4562-b3fc-2c963f66afa6" responded 500 in 1438.2005 ms diff --git a/Velvetech.MyTodoApp.WebAPI/Middlewares/GlobalExceptionHandlingMiddleware.cs b/Velvetech.MyTodoApp.WebAPI/Middlewares/GlobalExceptionHandlingMiddleware.cs new file mode 100644 index 00000000..37c6c8c5 --- /dev/null +++ b/Velvetech.MyTodoApp.WebAPI/Middlewares/GlobalExceptionHandlingMiddleware.cs @@ -0,0 +1,44 @@ +using Microsoft.AspNetCore.Mvc; +using System.Net; +using System.Text.Json; + +namespace Velvetech.MyTodoApp.WebAPI.Middlewares +{ + public class GlobalExceptionHandlingMiddleware : IMiddleware + { + private readonly ILogger _logger; + + public GlobalExceptionHandlingMiddleware(ILogger logger) + { + _logger = logger; + } + + public async Task InvokeAsync(HttpContext context, RequestDelegate next) + { + try + { + await next(context); + } + catch (Exception) + { + //_logger.LogInformation($"Exception happened whith the message \"{ex.Message}\" in the endpoint \"{context.Request.Path}\""); + + context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; + + ProblemDetails problem = new() + { + Status = (int)HttpStatusCode.InternalServerError, + Type = "Server error", + Title = "Server error", + Detail = $"An internal server error has occured" + }; + + var json = JsonSerializer.Serialize(problem); + + context.Response.ContentType = "application/json"; + + await context.Response.WriteAsync(json); + } + } + } +} diff --git a/Velvetech.MyTodoApp.WebAPI/Models/ErrorDetails.cs b/Velvetech.MyTodoApp.WebAPI/Models/ErrorDetails.cs new file mode 100644 index 00000000..9a770fa1 --- /dev/null +++ b/Velvetech.MyTodoApp.WebAPI/Models/ErrorDetails.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; + +namespace Velvetech.MyTodoApp.WebAPI.Models +{ + public class ErrorDetails + { + public int StatusCode { get; set; } + public string Message { get; set; } + public override string ToString() + { + return JsonConvert.SerializeObject(this); + } + } +} diff --git a/Velvetech.MyTodoApp.WebAPI/Program.cs b/Velvetech.MyTodoApp.WebAPI/Program.cs new file mode 100644 index 00000000..c2c17b06 --- /dev/null +++ b/Velvetech.MyTodoApp.WebAPI/Program.cs @@ -0,0 +1,48 @@ +using Velvetech.MyTodoApp.WebAPI.Middlewares; +using Velvetech.TodoApp.Infrastructure.Config; +using Serilog; +using Serilog.Filters; + +var builder = WebApplication.CreateBuilder(args); + + +// Add services to the container. + +builder.Services.AddInfrastructure(builder.Configuration); // Adding services from Infrastructure layer +builder.Services.AddApplication(); // Adding services from Application layer + +builder.Services.AddControllers(); + +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + + +builder.Host.UseSerilog((context, configuration) => + configuration.ReadFrom.Configuration(context.Configuration)); + +builder.Services.AddTransient(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + + + +app.UseSerilogRequestLogging(); + +app.UseHttpsRedirection(); + +app.UseAuthorization(); + +app.UseMiddleware(); + +app.ApplyDbMigrations(); + +app.MapControllers(); + +app.Run(); diff --git a/Properties/launchSettings.json b/Velvetech.MyTodoApp.WebAPI/Properties/launchSettings.json similarity index 57% rename from Properties/launchSettings.json rename to Velvetech.MyTodoApp.WebAPI/Properties/launchSettings.json index 6766196a..b1a2b82f 100644 --- a/Properties/launchSettings.json +++ b/Velvetech.MyTodoApp.WebAPI/Properties/launchSettings.json @@ -1,27 +1,31 @@ -{ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", "iisSettings": { "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { - "applicationUrl": "http://localhost:56416/", - "sslPort": 44331 + "applicationUrl": "http://localhost:58218", + "sslPort": 44387 } }, "profiles": { - "IIS Express": { - "commandName": "IISExpress", + "Velvetech.MyTodoApp.WebAPI": { + "commandName": "Project", + "dotnetRunMessages": true, "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7265;http://localhost:5265", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, - "TodoApiDTO": { - "commandName": "Project", + "IIS Express": { + "commandName": "IISExpress", "launchBrowser": true, + "launchUrl": "swagger", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" - }, - "applicationUrl": "https://localhost:5001;http://localhost:5000" + } } } -} \ No newline at end of file +} diff --git a/README.md b/Velvetech.MyTodoApp.WebAPI/README.md similarity index 100% rename from README.md rename to Velvetech.MyTodoApp.WebAPI/README.md diff --git a/Velvetech.MyTodoApp.WebAPI/Velvetech.MyTodoApp.WebAPI.csproj b/Velvetech.MyTodoApp.WebAPI/Velvetech.MyTodoApp.WebAPI.csproj new file mode 100644 index 00000000..e8eb0765 --- /dev/null +++ b/Velvetech.MyTodoApp.WebAPI/Velvetech.MyTodoApp.WebAPI.csproj @@ -0,0 +1,35 @@ + + + + net6.0 + enable + Linux + ..\VelvetechTestTask + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + diff --git a/Velvetech.MyTodoApp.WebAPI/appsettings.Development.json b/Velvetech.MyTodoApp.WebAPI/appsettings.Development.json new file mode 100644 index 00000000..191fa714 --- /dev/null +++ b/Velvetech.MyTodoApp.WebAPI/appsettings.Development.json @@ -0,0 +1,25 @@ +{ + "Serilog": { + "Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File" ], + "MinimumLevel": "Error", + "WriteTo": [ + { + "Name": "File", + "Args": { + "path": "Logs/log-.txt", + "rollingInterval": "Day", + "rollOnFileSizeLimit": true, + "formatter": "Serilog.Formatting.Json.JsonFormatter", + "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level}] {Message}{NewLine}{Exception}" + } + } + ], + "Enrich": [ "FromLogCOntext", "WithMachineName", "WithThreadId" ], + "Filter": { + "NotExceptionFilter": "Serilog.Filters.ByExcluding+FilterExpressionMatching, Serilog" + } + }, + "ConnectionStrings": { + "TodoItemsConn": "Data Source=.;Initial Catalog=TodoItemsDB;MultipleActiveResultSets=true;TrustServerCertificate=Yes;Integrated Security=True" + } +} diff --git a/appsettings.json b/Velvetech.MyTodoApp.WebAPI/appsettings.json similarity index 56% rename from appsettings.json rename to Velvetech.MyTodoApp.WebAPI/appsettings.json index d9d9a9bf..10f68b8c 100644 --- a/appsettings.json +++ b/Velvetech.MyTodoApp.WebAPI/appsettings.json @@ -2,8 +2,7 @@ "Logging": { "LogLevel": { "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" + "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*" diff --git a/Velvetech.TodoApp.Domain/Entities/BaseEntity.cs b/Velvetech.TodoApp.Domain/Entities/BaseEntity.cs new file mode 100644 index 00000000..5a7e47d3 --- /dev/null +++ b/Velvetech.TodoApp.Domain/Entities/BaseEntity.cs @@ -0,0 +1,11 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace Velvetech.TodoApp.Domain.Entities +{ + public class BaseEntity + { + [Key] + public Guid Id { get; set; } + } +} diff --git a/Velvetech.TodoApp.Domain/Entities/TodoItemEntity.cs b/Velvetech.TodoApp.Domain/Entities/TodoItemEntity.cs new file mode 100644 index 00000000..e75f2625 --- /dev/null +++ b/Velvetech.TodoApp.Domain/Entities/TodoItemEntity.cs @@ -0,0 +1,12 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace Velvetech.TodoApp.Domain.Entities +{ + [Table("TodoItem")] + public class TodoItemEntity : BaseEntity + { + public string Name { get; set; } + public bool IsComplete { get; set; } + public string Secret { get; set; } + } +} \ No newline at end of file diff --git a/Velvetech.TodoApp.Domain/Velvetech.TodoApp.Domain.csproj b/Velvetech.TodoApp.Domain/Velvetech.TodoApp.Domain.csproj new file mode 100644 index 00000000..dbc15171 --- /dev/null +++ b/Velvetech.TodoApp.Domain/Velvetech.TodoApp.Domain.csproj @@ -0,0 +1,7 @@ + + + + net6.0 + + + diff --git a/Velvetech.TodoApp.Infrastructure/Config/InfrastructureDependencyRegistrations.cs b/Velvetech.TodoApp.Infrastructure/Config/InfrastructureDependencyRegistrations.cs new file mode 100644 index 00000000..ab1e5c66 --- /dev/null +++ b/Velvetech.TodoApp.Infrastructure/Config/InfrastructureDependencyRegistrations.cs @@ -0,0 +1,31 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Velvetech.TodoApp.Infrastructure.Data; +using Velvetech.TodoApp.Infrastructure.Repositories.Abstractions.Custom; +using Velvetech.TodoApp.Infrastructure.Repositories.Implementations.Custom; + +namespace Velvetech.TodoApp.Infrastructure.Config +{ + public static class InfrastructureDependencyRegistrations + { + public static void AddInfrastructure(this IServiceCollection services, IConfiguration configuration) // Todo Add Sql Server from appsettings + { + var connectionString = configuration.GetConnectionString("TodoItemsConn"); + services.AddDbContext(opt => opt + .UseSqlServer(connectionString) + .UseQueryTrackingBehavior(QueryTrackingBehavior.TrackAll), ServiceLifetime.Scoped); + + services.AddScoped(); + } + + public static IApplicationBuilder ApplyDbMigrations(this IApplicationBuilder app) + { + IServiceScope serviceScope = app.ApplicationServices.CreateScope(); + TodoContext dbContext = serviceScope.ServiceProvider.GetRequiredService(); + dbContext.Database.Migrate(); + return app; + } + } +} diff --git a/Models/TodoContext.cs b/Velvetech.TodoApp.Infrastructure/Data/TodoContext.cs similarity index 58% rename from Models/TodoContext.cs rename to Velvetech.TodoApp.Infrastructure/Data/TodoContext.cs index 6e59e363..a8c86aa9 100644 --- a/Models/TodoContext.cs +++ b/Velvetech.TodoApp.Infrastructure/Data/TodoContext.cs @@ -1,6 +1,7 @@ using Microsoft.EntityFrameworkCore; +using Velvetech.TodoApp.Domain.Entities; -namespace TodoApi.Models +namespace Velvetech.TodoApp.Infrastructure.Data { public class TodoContext : DbContext { @@ -9,6 +10,6 @@ public TodoContext(DbContextOptions options) { } - public DbSet TodoItems { get; set; } + public DbSet TodoItems { get; set; } } -} \ No newline at end of file +} diff --git a/Velvetech.TodoApp.Infrastructure/Migrations/20230523133924_initialMigration.Designer.cs b/Velvetech.TodoApp.Infrastructure/Migrations/20230523133924_initialMigration.Designer.cs new file mode 100644 index 00000000..109d2a36 --- /dev/null +++ b/Velvetech.TodoApp.Infrastructure/Migrations/20230523133924_initialMigration.Designer.cs @@ -0,0 +1,50 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Velvetech.TodoApp.Infrastructure.Data; + +#nullable disable + +namespace Velvetech.TodoApp.Infrastructure.Migrations +{ + [DbContext(typeof(TodoContext))] + [Migration("20230523133924_initialMigration")] + partial class initialMigration + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Velvetech.TodoApp.Domain.Entities.TodoItemEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("IsComplete") + .HasColumnType("bit"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Secret") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("TodoItem"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Velvetech.TodoApp.Infrastructure/Migrations/20230523133924_initialMigration.cs b/Velvetech.TodoApp.Infrastructure/Migrations/20230523133924_initialMigration.cs new file mode 100644 index 00000000..90c19063 --- /dev/null +++ b/Velvetech.TodoApp.Infrastructure/Migrations/20230523133924_initialMigration.cs @@ -0,0 +1,36 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Velvetech.TodoApp.Infrastructure.Migrations +{ + /// + public partial class initialMigration : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "TodoItem", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + Name = table.Column(type: "nvarchar(max)", nullable: true), + IsComplete = table.Column(type: "bit", nullable: false), + Secret = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_TodoItem", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "TodoItem"); + } + } +} diff --git a/Velvetech.TodoApp.Infrastructure/Migrations/TodoContextModelSnapshot.cs b/Velvetech.TodoApp.Infrastructure/Migrations/TodoContextModelSnapshot.cs new file mode 100644 index 00000000..b463aa44 --- /dev/null +++ b/Velvetech.TodoApp.Infrastructure/Migrations/TodoContextModelSnapshot.cs @@ -0,0 +1,47 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Velvetech.TodoApp.Infrastructure.Data; + +#nullable disable + +namespace Velvetech.TodoApp.Infrastructure.Migrations +{ + [DbContext(typeof(TodoContext))] + partial class TodoContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Velvetech.TodoApp.Domain.Entities.TodoItemEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("IsComplete") + .HasColumnType("bit"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Secret") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("TodoItem"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Velvetech.TodoApp.Infrastructure/Repositories/Abstractions/Base/IBaseRepository.cs b/Velvetech.TodoApp.Infrastructure/Repositories/Abstractions/Base/IBaseRepository.cs new file mode 100644 index 00000000..ed0c79c0 --- /dev/null +++ b/Velvetech.TodoApp.Infrastructure/Repositories/Abstractions/Base/IBaseRepository.cs @@ -0,0 +1,24 @@ +using System.Linq.Expressions; +using Velvetech.TodoApp.Domain.Entities; + +namespace DataAccessLayer.Repository.Abstractions.Base +{ + public interface IBaseRepository where TEntity : BaseEntity, new() + { + Task AnyAsync(Expression> predicate); + + Task CountAsync(); + + Task> GetAsync(); + + Task> GetAsync(Expression> predicate); + + Task GetFirstOrDefaultAsync(Expression> predicate); + + Task AddAsync(TEntity entity); + + Task UpdateAsync(TEntity entity); + + Task DeleteAsync(TEntity entity); + } +} \ No newline at end of file diff --git a/Velvetech.TodoApp.Infrastructure/Repositories/Abstractions/Custom/ITodoItemRepository.cs b/Velvetech.TodoApp.Infrastructure/Repositories/Abstractions/Custom/ITodoItemRepository.cs new file mode 100644 index 00000000..f92cfd29 --- /dev/null +++ b/Velvetech.TodoApp.Infrastructure/Repositories/Abstractions/Custom/ITodoItemRepository.cs @@ -0,0 +1,10 @@ +using DataAccessLayer.Repository.Abstractions.Base; +using Velvetech.TodoApp.Domain.Entities; + +namespace Velvetech.TodoApp.Infrastructure.Repositories.Abstractions.Custom +{ + public interface ITodoItemRepository : IBaseRepository + { + + } +} diff --git a/Velvetech.TodoApp.Infrastructure/Repositories/Implementations/Base/BaseRepository.cs b/Velvetech.TodoApp.Infrastructure/Repositories/Implementations/Base/BaseRepository.cs new file mode 100644 index 00000000..4e34602c --- /dev/null +++ b/Velvetech.TodoApp.Infrastructure/Repositories/Implementations/Base/BaseRepository.cs @@ -0,0 +1,67 @@ +using DataAccessLayer.Repository.Abstractions.Base; +using Microsoft.EntityFrameworkCore; +using System.Linq.Expressions; +using Velvetech.TodoApp.Domain.Entities; + +namespace DataAccessLayer.Repository.Concrete +{ + public abstract class BaseRepository : IBaseRepository where TEntity : BaseEntity, new() + { + private readonly DbContext _dbContext; + private readonly DbSet _dbSet; + + protected BaseRepository(DbContext dbContext) + { + _dbContext = dbContext; + _dbSet = _dbContext.Set(); + } + + public async Task AnyAsync(Expression> predicate) + { + return await _dbSet.AnyAsync(predicate); + } + + public async Task CountAsync() + { + return await _dbSet.CountAsync(); + } + + public async Task> GetAsync() + { + return await _dbSet.ToListAsync(); + } + + public async Task> GetAsync(Expression> predicate) + { + return await _dbSet.Where(predicate).ToListAsync(); + } + + public async Task GetFirstOrDefaultAsync(Expression> predicate) + { +#pragma warning disable CS8603 // Possible null reference return. + return await _dbSet.FirstOrDefaultAsync(predicate); +#pragma warning restore CS8603 // Possible null reference return. + } + + public async Task AddAsync(TEntity entity) + { + entity.Id = Guid.NewGuid(); + await _dbSet.AddAsync(entity); + await _dbContext.SaveChangesAsync(); + return entity; + } + + public async Task UpdateAsync(TEntity entity) + { + _dbSet.Update(entity); + await _dbContext.SaveChangesAsync(); + return entity; + } + + public async Task DeleteAsync(TEntity entity) + { + _dbSet.Remove(entity); + return await _dbContext.SaveChangesAsync() > 0; + } + } +} \ No newline at end of file diff --git a/Velvetech.TodoApp.Infrastructure/Repositories/Implementations/Custom/TodoItemRepository.cs b/Velvetech.TodoApp.Infrastructure/Repositories/Implementations/Custom/TodoItemRepository.cs new file mode 100644 index 00000000..e4de8cf9 --- /dev/null +++ b/Velvetech.TodoApp.Infrastructure/Repositories/Implementations/Custom/TodoItemRepository.cs @@ -0,0 +1,14 @@ +using DataAccessLayer.Repository.Concrete; +using Velvetech.TodoApp.Domain.Entities; +using Velvetech.TodoApp.Infrastructure.Data; +using Velvetech.TodoApp.Infrastructure.Repositories.Abstractions.Custom; + +namespace Velvetech.TodoApp.Infrastructure.Repositories.Implementations.Custom +{ + public class TodoItemRepository : BaseRepository, ITodoItemRepository + { + public TodoItemRepository(TodoContext dbContext) : base(dbContext) + { + } + } +} diff --git a/Velvetech.TodoApp.Infrastructure/Velvetech.TodoApp.Infrastructure.csproj b/Velvetech.TodoApp.Infrastructure/Velvetech.TodoApp.Infrastructure.csproj new file mode 100644 index 00000000..95d1efad --- /dev/null +++ b/Velvetech.TodoApp.Infrastructure/Velvetech.TodoApp.Infrastructure.csproj @@ -0,0 +1,28 @@ + + + + net6.0 + enable + enable + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/appsettings.Development.json b/appsettings.Development.json deleted file mode 100644 index 8983e0fc..00000000 --- a/appsettings.Development.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" - } - } -}