diff --git a/.gitignore b/.gitignore index 4ce6fdde..4177634f 100644 --- a/.gitignore +++ b/.gitignore @@ -337,4 +337,7 @@ ASALocalRun/ .localhistory/ # BeatPulse healthcheck temp database -healthchecksdb \ No newline at end of file +healthchecksdb + +#Logs +Log.txt \ No newline at end of file diff --git a/Applications/TodoApi/Controllers/Configuration/AutoMapper/AutoMapperConfiguration.cs b/Applications/TodoApi/Controllers/Configuration/AutoMapper/AutoMapperConfiguration.cs new file mode 100644 index 00000000..db81d2d7 --- /dev/null +++ b/Applications/TodoApi/Controllers/Configuration/AutoMapper/AutoMapperConfiguration.cs @@ -0,0 +1,14 @@ +using Microsoft.Extensions.DependencyInjection; +using TodoApi.Controllers.Configuration.AutoMapper.AutoMapperProfiles; + +namespace TodoApi.Controllers.Configuration.AutoMapper +{ + public static class AutoMapperConfiguration + { + public static void Configure(IServiceCollection services) + { + services.AddAutoMapper(typeof(TodoItemApiDataProfile)); + services.AddAutoMapper(typeof(TodoItemServiceDataProfile)); + } + } +} \ No newline at end of file diff --git a/Applications/TodoApi/Controllers/Configuration/AutoMapper/AutoMapperProfiles/TodoItemApiDataProfile.cs b/Applications/TodoApi/Controllers/Configuration/AutoMapper/AutoMapperProfiles/TodoItemApiDataProfile.cs new file mode 100644 index 00000000..f3de78b7 --- /dev/null +++ b/Applications/TodoApi/Controllers/Configuration/AutoMapper/AutoMapperProfiles/TodoItemApiDataProfile.cs @@ -0,0 +1,17 @@ +using AutoMapper; +using ApiData = TodoApiDto.Shared.Api.Data; +using ServiceData = TodoApiDto.Services.Data; + +namespace TodoApi.Controllers.Configuration.AutoMapper.AutoMapperProfiles +{ + public class TodoItemApiDataProfile : Profile + { + public TodoItemApiDataProfile() + { + CreateMap(); + CreateMap(); + CreateMap() + .ForMember(todoItem => todoItem.Id, opt => opt.MapFrom(todoItem => todoItem.Id.ObjectId)); + } + } +} \ No newline at end of file diff --git a/Applications/TodoApi/Controllers/Configuration/AutoMapper/AutoMapperProfiles/TodoItemServiceDataProfile.cs b/Applications/TodoApi/Controllers/Configuration/AutoMapper/AutoMapperProfiles/TodoItemServiceDataProfile.cs new file mode 100644 index 00000000..4b24444c --- /dev/null +++ b/Applications/TodoApi/Controllers/Configuration/AutoMapper/AutoMapperProfiles/TodoItemServiceDataProfile.cs @@ -0,0 +1,21 @@ +using AutoMapper; +using TodoApiDto.StrongId; +using DbData = TodoApiDto.Repositories.Data; +using ServiceData = TodoApiDto.Services.Data; + +namespace TodoApi.Controllers.Configuration.AutoMapper.AutoMapperProfiles +{ + public class TodoItemServiceDataProfile : Profile + { + public TodoItemServiceDataProfile() + { + CreateMap(); + + CreateMap() + .ForMember(todoItem => todoItem.Id, opt => opt.MapFrom(todoItem => todoItem.Id.ObjectId)); + + CreateMap() + .ForMember(todoItem => todoItem.Id, opt => opt.MapFrom(todoItem => new TodoId(todoItem.Id))); + } + } +} \ No newline at end of file diff --git a/Applications/TodoApi/Controllers/Configuration/Middleware/MiddlewareConfiguration.cs b/Applications/TodoApi/Controllers/Configuration/Middleware/MiddlewareConfiguration.cs new file mode 100644 index 00000000..f3433fc6 --- /dev/null +++ b/Applications/TodoApi/Controllers/Configuration/Middleware/MiddlewareConfiguration.cs @@ -0,0 +1,13 @@ +using Microsoft.AspNetCore.Builder; +using TodoApi.Middleware; + +namespace TodoApi.Controllers.Configuration.Middleware +{ + public static class MiddlewareConfiguration + { + public static void Configure(IApplicationBuilder app) + { + app.UseMiddleware(); + } + } +} \ No newline at end of file diff --git a/Applications/TodoApi/Controllers/Configuration/RepositoriesConfiguration.cs b/Applications/TodoApi/Controllers/Configuration/RepositoriesConfiguration.cs new file mode 100644 index 00000000..458c304e --- /dev/null +++ b/Applications/TodoApi/Controllers/Configuration/RepositoriesConfiguration.cs @@ -0,0 +1,14 @@ +using Microsoft.Extensions.DependencyInjection; +using TodoApiDto.Repositories; +using TodoApiDto.Repositories.Interfaces; + +namespace TodoApi.Controllers.Configuration +{ + public static class RepositoriesConfiguration + { + public static void Configure(IServiceCollection services) + { + services.AddTransient(); + } + } +} \ No newline at end of file diff --git a/Applications/TodoApi/Controllers/Configuration/ServicesConfiguration.cs b/Applications/TodoApi/Controllers/Configuration/ServicesConfiguration.cs new file mode 100644 index 00000000..eeebd32a --- /dev/null +++ b/Applications/TodoApi/Controllers/Configuration/ServicesConfiguration.cs @@ -0,0 +1,14 @@ +using Microsoft.Extensions.DependencyInjection; +using TodoApiDto.Services; +using TodoApiDto.Services.Interfaces; + +namespace TodoApi.Controllers.Configuration +{ + public class ServicesConfiguration + { + public static void Configure(IServiceCollection services) + { + services.AddTransient(); + } + } +} \ No newline at end of file diff --git a/Applications/TodoApi/Controllers/TodoItemsController.cs b/Applications/TodoApi/Controllers/TodoItemsController.cs new file mode 100644 index 00000000..af86190a --- /dev/null +++ b/Applications/TodoApi/Controllers/TodoItemsController.cs @@ -0,0 +1,159 @@ +using AutoMapper; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using System.Collections.Generic; +using System.Threading.Tasks; +using TodoApiDto.Services.Interfaces; +using TodoApiDto.StrongId; +using ApiData = TodoApiDto.Shared.Api.Data; +using ServiceData = TodoApiDto.Services.Data; + +namespace TodoApi.Controllers +{ + [Route("api/[controller]"), ApiController, Produces("application/json")] + public class TodoItemsController : ControllerBase + { + private readonly ITodoService _todoService; + private readonly IMapper _mapper; + + public TodoItemsController( + ITodoService todoService, + IMapper mapper) + { + _todoService = todoService; + _mapper = mapper; + } + + /// + /// Get all TodoItems + /// + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task>> GetTodoItems() + { + var serviceResult = await _todoService.GetAllAsync(); + + if (serviceResult.IsError) + { + return StatusCode(StatusCodes.Status500InternalServerError, new { message = "Internal server error" }); + } + + var apiData = _mapper.Map>(serviceResult.Result); + + return Ok(apiData); + } + + /// + /// Get TodoItem by + /// + /// Record id + [HttpGet("{id:long}")] + public async Task> GetTodoItem(long id) + { + var serviceResult = await _todoService.GetByIdAsync(new TodoId(id)); + + if (serviceResult.IsError) + { + return StatusCode(StatusCodes.Status500InternalServerError, new { message = "Internal server error" }); + } + + if (serviceResult.IsNotFound) + { + return NotFound(); + } + + var apiData = _mapper.Map(serviceResult.Result); + + return Ok(apiData); + } + + /// + /// Update TodoItem + /// + /// Record id + /// Update request + [HttpPut("{id:long}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task UpdateTodoItem( + long id, + [FromBody] ApiData.Requests.TodoItemUpdateRequest request) + { + if (id != request?.Id) + { + return BadRequest(); + } + + var serviceUpdateModel = _mapper.Map(request); + var serviceResult = await _todoService.UpdateAsync(serviceUpdateModel); + + if (serviceResult.IsError) + { + return StatusCode(StatusCodes.Status500InternalServerError, new { message = "Internal server error" }); + } + + if (serviceResult.IsNotFound) + { + return NotFound(); + } + + return Ok(); + } + + /// + /// Create new TodoItem + /// + /// Create request + [HttpPost] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + [ProducesResponseType(StatusCodes.Status201Created)] + public async Task> CreateTodoItem( + [FromBody] ApiData.Requests.TodoItemCreateRequest request) + { + if (request is null) + { + return BadRequest(); + } + + var serviceCreateModel = _mapper.Map(request); + var serviceResult = await _todoService.CreateAsync(serviceCreateModel); + + if (serviceResult.IsError) + { + return StatusCode(StatusCodes.Status500InternalServerError, new { message = "Internal server error" }); + } + + var apiData = _mapper.Map(serviceResult.Result); + + return CreatedAtAction(nameof(GetTodoItem), new { id = apiData.Id }, apiData); + } + + /// + /// Delete TodoItem by + /// + /// Record id + [HttpDelete("{id:long}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task DeleteTodoItem(long id) + { + var serviceResult = await _todoService.RemoveAsync(new TodoId(id)); + + if (serviceResult.IsError) + { + return StatusCode(StatusCodes.Status500InternalServerError, new { message = "Internal server error" }); + } + + if (serviceResult.IsNotFound) + { + return NotFound(); + } + + return Ok(); + } + } +} \ No newline at end of file diff --git a/Applications/TodoApi/Middleware/ExceptionHandlerMiddleware.cs b/Applications/TodoApi/Middleware/ExceptionHandlerMiddleware.cs new file mode 100644 index 00000000..dc7befeb --- /dev/null +++ b/Applications/TodoApi/Middleware/ExceptionHandlerMiddleware.cs @@ -0,0 +1,51 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using System; +using System.Net; +using System.Threading.Tasks; + +namespace TodoApi.Middleware +{ + public class ExceptionHandlerMiddleware + { + private readonly RequestDelegate _next; + private readonly ILogger _logger; + + public ExceptionHandlerMiddleware( + RequestDelegate next, + ILogger logger) + { + _logger = logger; + _next = next; + } + + public async Task Invoke(HttpContext context) + { + try + { + await _next.Invoke(context); + } + catch (Exception ex) + { + _logger.LogError(ex, "Unhandled exception"); + + await HandleExceptionMessageAsync(context); + } + } + + private static Task HandleExceptionMessageAsync(HttpContext context) + { + context.Response.ContentType = "application/json"; + const int statusCode = (int)HttpStatusCode.InternalServerError; + var result = JsonConvert.SerializeObject(new + { + StatusCode = statusCode, + ErrorMessage = "Internal server error", + }); + context.Response.ContentType = "application/json"; + context.Response.StatusCode = statusCode; + return context.Response.WriteAsync(result); + } + } +} \ No newline at end of file diff --git a/Applications/TodoApi/Program.cs b/Applications/TodoApi/Program.cs new file mode 100644 index 00000000..8a463a78 --- /dev/null +++ b/Applications/TodoApi/Program.cs @@ -0,0 +1,30 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; +using Serilog; +using System.IO; + +namespace TodoApi +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) + { + return Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); }) + .UseSerilog((context, services, configuration) => + { + configuration + .ReadFrom.Configuration(context.Configuration) + .ReadFrom.Services(services) + .Enrich.FromLogContext() + .MinimumLevel.Information() + .WriteTo.File(Path.Combine(Directory.GetCurrentDirectory(), "Log.txt")); + }); + } + } +} \ No newline at end of file diff --git a/Applications/TodoApi/Properties/launchSettings.json b/Applications/TodoApi/Properties/launchSettings.json new file mode 100644 index 00000000..2f401077 --- /dev/null +++ b/Applications/TodoApi/Properties/launchSettings.json @@ -0,0 +1,27 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:56416/", + "sslPort": 44331 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "TodoApiDTO": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:5001;http://localhost:5000" + } + } +} \ No newline at end of file diff --git a/Startup.cs b/Applications/TodoApi/Startup.cs similarity index 53% rename from Startup.cs rename to Applications/TodoApi/Startup.cs index bbfbc83d..81690308 100644 --- a/Startup.cs +++ b/Applications/TodoApi/Startup.cs @@ -1,17 +1,14 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.HttpsPolicy; -using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using TodoApi.Models; +using System.IO; +using TodoApi.Controllers.Configuration; +using TodoApi.Controllers.Configuration.AutoMapper; +using TodoApi.Controllers.Configuration.Middleware; +using TodoApiDto.Repositories.Context; namespace TodoApi { @@ -24,32 +21,41 @@ 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.AddDbContext(opt => - opt.UseInMemoryDatabase("TodoList")); + services.AddDbContext(options => + options.UseSqlServer(Configuration.GetConnectionString("TodoTask"))); + services.AddControllers(); + services.AddSwaggerGen(options => + { + var xmlFile = Path.ChangeExtension(typeof(Startup).Assembly.Location, ".xml"); + options.IncludeXmlComments(xmlFile); + }); + + AutoMapperConfiguration.Configure(services); + RepositoriesConfiguration.Configure(services); + ServicesConfiguration.Configure(services); } - // 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(); - } + MiddlewareConfiguration.Configure(app); app.UseHttpsRedirection(); - app.UseRouting(); - app.UseAuthorization(); + if (env.IsDevelopment()) + { + app.UseSwagger(); + app.UseSwaggerUI(); + } + app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); } } -} +} \ No newline at end of file diff --git a/Applications/TodoApi/TodoApi.csproj b/Applications/TodoApi/TodoApi.csproj new file mode 100644 index 00000000..ede7f130 --- /dev/null +++ b/Applications/TodoApi/TodoApi.csproj @@ -0,0 +1,31 @@ + + + + netcoreapp3.1 + True + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/appsettings.Development.json b/Applications/TodoApi/appsettings.Development.json similarity index 98% rename from appsettings.Development.json rename to Applications/TodoApi/appsettings.Development.json index 8983e0fc..45fe774a 100644 --- a/appsettings.Development.json +++ b/Applications/TodoApi/appsettings.Development.json @@ -6,4 +6,4 @@ "Microsoft.Hosting.Lifetime": "Information" } } -} +} \ No newline at end of file diff --git a/Applications/TodoApi/appsettings.json b/Applications/TodoApi/appsettings.json new file mode 100644 index 00000000..28e80243 --- /dev/null +++ b/Applications/TodoApi/appsettings.json @@ -0,0 +1,13 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*", + "ConnectionStrings": { + "TodoTask": "Server=(localdb)\\mssqllocaldb;Database=TodoTask;Trusted_Connection=True" + } +} \ No newline at end of file diff --git a/Controllers/Configuration/AutoMapper/AutoMapperConfiguration.cs b/Controllers/Configuration/AutoMapper/AutoMapperConfiguration.cs new file mode 100644 index 00000000..b5171a00 --- /dev/null +++ b/Controllers/Configuration/AutoMapper/AutoMapperConfiguration.cs @@ -0,0 +1,14 @@ +using Microsoft.Extensions.DependencyInjection; +using TodoApiDTO.Controllers.Configuration.AutoMapper.AutoMapperProfiles; + +namespace TodoApiDTO.Controllers.Configuration.AutoMapper +{ + public static class AutoMapperConfiguration + { + public static void Configure(IServiceCollection services) + { + services.AddAutoMapper(typeof(TodoItemApiDataProfile)); + services.AddAutoMapper(typeof(TodoItemServiceDataProfile)); + } + } +} \ No newline at end of file diff --git a/Controllers/Configuration/AutoMapper/AutoMapperProfiles/TodoItemApiDataProfile.cs b/Controllers/Configuration/AutoMapper/AutoMapperProfiles/TodoItemApiDataProfile.cs new file mode 100644 index 00000000..929522c5 --- /dev/null +++ b/Controllers/Configuration/AutoMapper/AutoMapperProfiles/TodoItemApiDataProfile.cs @@ -0,0 +1,17 @@ +using AutoMapper; +using ApiData = TodoApiDto.Shared.Api.Data; +using ServiceData = TodoApiDto.Services.Data; + +namespace TodoApiDTO.Controllers.Configuration.AutoMapper.AutoMapperProfiles +{ + public class TodoItemApiDataProfile : Profile + { + public TodoItemApiDataProfile() + { + CreateMap(); + CreateMap(); + CreateMap() + .ForMember(todoItem => todoItem.Id, opt => opt.MapFrom(todoItem => todoItem.Id.ObjectId)); + } + } +} \ No newline at end of file diff --git a/Controllers/Configuration/AutoMapper/AutoMapperProfiles/TodoItemServiceDataProfile.cs b/Controllers/Configuration/AutoMapper/AutoMapperProfiles/TodoItemServiceDataProfile.cs new file mode 100644 index 00000000..6a5a8cc6 --- /dev/null +++ b/Controllers/Configuration/AutoMapper/AutoMapperProfiles/TodoItemServiceDataProfile.cs @@ -0,0 +1,21 @@ +using AutoMapper; +using TodoApiDto.StrongId; +using DbData = TodoApiDto.Repositories.Data; +using ServiceData = TodoApiDto.Services.Data; + +namespace TodoApiDTO.Controllers.Configuration.AutoMapper.AutoMapperProfiles +{ + public class TodoItemServiceDataProfile : Profile + { + public TodoItemServiceDataProfile() + { + CreateMap(); + + CreateMap() + .ForMember(todoItem => todoItem.Id, opt => opt.MapFrom(todoItem => todoItem.Id.ObjectId)); + + CreateMap() + .ForMember(todoItem => todoItem.Id, opt => opt.MapFrom(todoItem => new TodoId(todoItem.Id))); + } + } +} \ No newline at end of file diff --git a/Controllers/Configuration/Middleware/MiddlewareConfiguration.cs b/Controllers/Configuration/Middleware/MiddlewareConfiguration.cs new file mode 100644 index 00000000..6f3b4f28 --- /dev/null +++ b/Controllers/Configuration/Middleware/MiddlewareConfiguration.cs @@ -0,0 +1,13 @@ +using Microsoft.AspNetCore.Builder; +using TodoApiDTO.Middleware; + +namespace TodoApiDTO.Controllers.Configuration.Middleware +{ + public static class MiddlewareConfiguration + { + public static void Configure(IApplicationBuilder app) + { + app.UseMiddleware(); + } + } +} \ No newline at end of file diff --git a/Controllers/Configuration/RepositoriesConfiguration.cs b/Controllers/Configuration/RepositoriesConfiguration.cs new file mode 100644 index 00000000..956e5526 --- /dev/null +++ b/Controllers/Configuration/RepositoriesConfiguration.cs @@ -0,0 +1,14 @@ +using Microsoft.Extensions.DependencyInjection; +using TodoApiDto.Repositories; +using TodoApiDto.Repositories.Interfaces; + +namespace TodoApiDTO.Controllers.Configuration +{ + public static class RepositoriesConfiguration + { + public static void Configure(IServiceCollection services) + { + services.AddTransient(); + } + } +} \ No newline at end of file diff --git a/Controllers/Configuration/ServicesConfiguration.cs b/Controllers/Configuration/ServicesConfiguration.cs new file mode 100644 index 00000000..2f2e5369 --- /dev/null +++ b/Controllers/Configuration/ServicesConfiguration.cs @@ -0,0 +1,14 @@ +using Microsoft.Extensions.DependencyInjection; +using TodoApiDto.Services; +using TodoApiDto.Services.Interfaces; + +namespace TodoApiDTO.Controllers.Configuration +{ + public class ServicesConfiguration + { + public static void Configure(IServiceCollection services) + { + services.AddTransient(); + } + } +} \ No newline at end of file diff --git a/Controllers/TodoItemsController.cs b/Controllers/TodoItemsController.cs index 0ef138e7..efb8c6e9 100644 --- a/Controllers/TodoItemsController.cs +++ b/Controllers/TodoItemsController.cs @@ -1,116 +1,159 @@ +using AutoMapper; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; -using TodoApi.Models; +using TodoApiDto.Services.Interfaces; +using TodoApiDto.StrongId; +using ApiData = TodoApiDto.Shared.Api.Data; +using ServiceData = TodoApiDto.Services.Data; -namespace TodoApi.Controllers +namespace TodoApiDTO.Controllers { - [Route("api/[controller]")] - [ApiController] + [Route("api/[controller]"), ApiController, Produces("application/json")] public class TodoItemsController : ControllerBase { - private readonly TodoContext _context; + private readonly ITodoService _todoService; + private readonly IMapper _mapper; - public TodoItemsController(TodoContext context) + public TodoItemsController( + ITodoService todoService, + IMapper mapper) { - _context = context; + _todoService = todoService; + _mapper = mapper; } + /// + /// Get all TodoItems + /// [HttpGet] - public async Task>> GetTodoItems() + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task>> GetTodoItems() { - return await _context.TodoItems - .Select(x => ItemToDTO(x)) - .ToListAsync(); + var serviceResult = await _todoService.GetAllAsync(); + + if (serviceResult.IsError) + { + return StatusCode(StatusCodes.Status500InternalServerError, new { message = "Internal server error" }); + } + + var apiData = _mapper.Map>(serviceResult.Result); + + return Ok(apiData); } - [HttpGet("{id}")] - public async Task> GetTodoItem(long id) + /// + /// Get TodoItem by + /// + /// Record id + [HttpGet("{id:long}")] + public async Task> GetTodoItem(long id) { - var todoItem = await _context.TodoItems.FindAsync(id); + var serviceResult = await _todoService.GetByIdAsync(new TodoId(id)); + + if (serviceResult.IsError) + { + return StatusCode(StatusCodes.Status500InternalServerError, new { message = "Internal server error" }); + } - if (todoItem == null) + if (serviceResult.IsNotFound) { return NotFound(); } - return ItemToDTO(todoItem); + var apiData = _mapper.Map(serviceResult.Result); + + return Ok(apiData); } - [HttpPut("{id}")] - public async Task UpdateTodoItem(long id, TodoItemDTO todoItemDTO) + /// + /// Update TodoItem + /// + /// Record id + /// Update request + [HttpPut("{id:long}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task UpdateTodoItem( + long id, + [FromBody] ApiData.Requests.TodoItemUpdateRequest request) { - if (id != todoItemDTO.Id) + if (id != request?.Id) { return BadRequest(); } - var todoItem = await _context.TodoItems.FindAsync(id); - if (todoItem == null) - { - return NotFound(); - } - - todoItem.Name = todoItemDTO.Name; - todoItem.IsComplete = todoItemDTO.IsComplete; + var serviceUpdateModel = _mapper.Map(request); + var serviceResult = await _todoService.UpdateAsync(serviceUpdateModel); - try + if (serviceResult.IsError) { - await _context.SaveChangesAsync(); + return StatusCode(StatusCodes.Status500InternalServerError, new { message = "Internal server error" }); } - catch (DbUpdateConcurrencyException) when (!TodoItemExists(id)) + + if (serviceResult.IsNotFound) { return NotFound(); } - return NoContent(); + return Ok(); } + /// + /// Create new TodoItem + /// + /// Create request [HttpPost] - public async Task> CreateTodoItem(TodoItemDTO todoItemDTO) + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + [ProducesResponseType(StatusCodes.Status201Created)] + public async Task> CreateTodoItem( + [FromBody] ApiData.Requests.TodoItemCreateRequest request) { - var todoItem = new TodoItem + if (request is null) { - IsComplete = todoItemDTO.IsComplete, - Name = todoItemDTO.Name - }; + return BadRequest(); + } - _context.TodoItems.Add(todoItem); - await _context.SaveChangesAsync(); + var serviceCreateModel = _mapper.Map(request); + var serviceResult = await _todoService.CreateAsync(serviceCreateModel); - return CreatedAtAction( - nameof(GetTodoItem), - new { id = todoItem.Id }, - ItemToDTO(todoItem)); + if (serviceResult.IsError) + { + return StatusCode(StatusCodes.Status500InternalServerError, new { message = "Internal server error" }); + } + + var apiData = _mapper.Map(serviceResult.Result); + + return CreatedAtAction(nameof(GetTodoItem), new { id = apiData.Id }, apiData); } - [HttpDelete("{id}")] + /// + /// Delete TodoItem by + /// + /// Record id + [HttpDelete("{id:long}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task DeleteTodoItem(long id) { - var todoItem = await _context.TodoItems.FindAsync(id); + var serviceResult = await _todoService.RemoveAsync(new TodoId(id)); - if (todoItem == null) + if (serviceResult.IsError) { - return NotFound(); + return StatusCode(StatusCodes.Status500InternalServerError, new { message = "Internal server error" }); } - _context.TodoItems.Remove(todoItem); - await _context.SaveChangesAsync(); + if (serviceResult.IsNotFound) + { + return NotFound(); + } - return NoContent(); + return Ok(); } - - 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 - }; } -} +} \ No newline at end of file diff --git a/Infrastructure/TodoApiDto.StrongId/TodoApiDto.StrongId.csproj b/Infrastructure/TodoApiDto.StrongId/TodoApiDto.StrongId.csproj new file mode 100644 index 00000000..cb631906 --- /dev/null +++ b/Infrastructure/TodoApiDto.StrongId/TodoApiDto.StrongId.csproj @@ -0,0 +1,7 @@ + + + + netcoreapp3.1 + + + diff --git a/Infrastructure/TodoApiDto.StrongId/TodoId.cs b/Infrastructure/TodoApiDto.StrongId/TodoId.cs new file mode 100644 index 00000000..337f553c --- /dev/null +++ b/Infrastructure/TodoApiDto.StrongId/TodoId.cs @@ -0,0 +1,12 @@ +namespace TodoApiDto.StrongId +{ + public class TodoId + { + public long ObjectId { get; set; } + + public TodoId(long objectId) + { + ObjectId = objectId; + } + } +} \ No newline at end of file diff --git a/Middleware/ExceptionHandlerMiddleware.cs b/Middleware/ExceptionHandlerMiddleware.cs new file mode 100644 index 00000000..d9167ba0 --- /dev/null +++ b/Middleware/ExceptionHandlerMiddleware.cs @@ -0,0 +1,51 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using System; +using System.Net; +using System.Threading.Tasks; + +namespace TodoApiDTO.Middleware +{ + public class ExceptionHandlerMiddleware + { + private readonly RequestDelegate _next; + private readonly ILogger _logger; + + public ExceptionHandlerMiddleware( + RequestDelegate next, + ILogger logger) + { + _logger = logger; + _next = next; + } + + public async Task Invoke(HttpContext context) + { + try + { + await _next.Invoke(context); + } + catch (Exception ex) + { + _logger.LogError(ex, "Unhandled exception"); + + await HandleExceptionMessageAsync(context); + } + } + + private static Task HandleExceptionMessageAsync(HttpContext context) + { + context.Response.ContentType = "application/json"; + const int statusCode = (int)HttpStatusCode.InternalServerError; + var result = JsonConvert.SerializeObject(new + { + StatusCode = statusCode, + ErrorMessage = "Internal server error", + }); + context.Response.ContentType = "application/json"; + context.Response.StatusCode = statusCode; + return context.Response.WriteAsync(result); + } + } +} \ No newline at end of file diff --git a/Models/TodoContext.cs b/Models/TodoContext.cs deleted file mode 100644 index 6e59e363..00000000 --- a/Models/TodoContext.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Microsoft.EntityFrameworkCore; - -namespace TodoApi.Models -{ - public class TodoContext : DbContext - { - public TodoContext(DbContextOptions options) - : base(options) - { - } - - public DbSet TodoItems { get; set; } - } -} \ No newline at end of file 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/Properties/launchSettings.json b/Properties/launchSettings.json index 6766196a..a5ca2600 100644 --- a/Properties/launchSettings.json +++ b/Properties/launchSettings.json @@ -1,27 +1,27 @@ { - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:56416/", - "sslPort": 44331 - } - }, - "profiles": { - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:56416/", + "sslPort": 44331 + } }, - "TodoApiDTO": { - "commandName": "Project", - "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - }, - "applicationUrl": "https://localhost:5001;http://localhost:5000" + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "TodoApiDTO": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:5001;http://localhost:5000" + } } - } } \ No newline at end of file diff --git a/Repositories/TodoApiDto.Repositories.Data/TodoApiDto.Repositories.Data.csproj b/Repositories/TodoApiDto.Repositories.Data/TodoApiDto.Repositories.Data.csproj new file mode 100644 index 00000000..cb631906 --- /dev/null +++ b/Repositories/TodoApiDto.Repositories.Data/TodoApiDto.Repositories.Data.csproj @@ -0,0 +1,7 @@ + + + + netcoreapp3.1 + + + diff --git a/Repositories/TodoApiDto.Repositories.Data/TodoItem.cs b/Repositories/TodoApiDto.Repositories.Data/TodoItem.cs new file mode 100644 index 00000000..c2f6ac92 --- /dev/null +++ b/Repositories/TodoApiDto.Repositories.Data/TodoItem.cs @@ -0,0 +1,31 @@ +using System; + +namespace TodoApiDto.Repositories.Data +{ + /// + /// Db TodoItem dto + /// + [Serializable] + public class TodoItem + { + /// + /// Record id + /// + public long Id { get; set; } + + /// + /// Content + /// + public string Name { get; set; } + + /// + /// Status + /// + public bool IsComplete { get; set; } + + /// + /// Secret + /// + public string Secret { get; set; } + } +} \ No newline at end of file diff --git a/Repositories/TodoApiDto.Repositories.Data/TodoItemCreateModel.cs b/Repositories/TodoApiDto.Repositories.Data/TodoItemCreateModel.cs new file mode 100644 index 00000000..fb590e78 --- /dev/null +++ b/Repositories/TodoApiDto.Repositories.Data/TodoItemCreateModel.cs @@ -0,0 +1,18 @@ +namespace TodoApiDto.Repositories.Data +{ + /// + /// Model for creating TODOItem + /// + public class TodoItemCreateModel + { + /// + /// Content + /// + public string Name { get; set; } + + /// + /// Status + /// + public bool IsComplete { get; set; } + } +} \ No newline at end of file diff --git a/Repositories/TodoApiDto.Repositories.Data/TodoItemUpdateModel.cs b/Repositories/TodoApiDto.Repositories.Data/TodoItemUpdateModel.cs new file mode 100644 index 00000000..6d2c9c79 --- /dev/null +++ b/Repositories/TodoApiDto.Repositories.Data/TodoItemUpdateModel.cs @@ -0,0 +1,23 @@ +namespace TodoApiDto.Repositories.Data +{ + /// + /// Model for updating TODOItem + /// + public class TodoItemUpdateModel + { + /// + /// record id + /// + public long Id { get; set; } + + /// + /// Content + /// + public string Name { get; set; } + + /// + /// Status + /// + public bool IsComplete { get; set; } + } +} \ No newline at end of file diff --git a/Repositories/TodoApiDto.Repositories.Interfaces/ITodoRepository.cs b/Repositories/TodoApiDto.Repositories.Interfaces/ITodoRepository.cs new file mode 100644 index 00000000..cb93017e --- /dev/null +++ b/Repositories/TodoApiDto.Repositories.Interfaces/ITodoRepository.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using TodoApiDto.Repositories.Data; +using TodoApiDto.StrongId; + +namespace TodoApiDto.Repositories.Interfaces +{ + /// + /// Repository for working with TodoItem + /// + public interface ITodoRepository + { + /// + /// Get all TodoItems + /// + Task> GetAllAsync(); + + /// + /// Get TodoItem by + /// + /// Record id + Task GetByIdAsync(TodoId id); + + /// + /// Remove TodoItem by + /// + /// Record id + Task RemoveAsync(TodoId id); + + /// + /// Update TodoItem + /// + /// Update model + Task UpdateAsync(TodoItemUpdateModel updateModel); + + /// + /// Create new TodoItem + /// + /// Create model + Task CreateAsync(TodoItemCreateModel createModel); + } +} \ No newline at end of file diff --git a/Repositories/TodoApiDto.Repositories.Interfaces/TodoApiDto.Repositories.Interfaces.csproj b/Repositories/TodoApiDto.Repositories.Interfaces/TodoApiDto.Repositories.Interfaces.csproj new file mode 100644 index 00000000..000dd245 --- /dev/null +++ b/Repositories/TodoApiDto.Repositories.Interfaces/TodoApiDto.Repositories.Interfaces.csproj @@ -0,0 +1,12 @@ + + + + netcoreapp3.1 + + + + + + + + diff --git a/Repositories/TodoApiDto.Repositories/Context/TodoContext.cs b/Repositories/TodoApiDto.Repositories/Context/TodoContext.cs new file mode 100644 index 00000000..ada9d909 --- /dev/null +++ b/Repositories/TodoApiDto.Repositories/Context/TodoContext.cs @@ -0,0 +1,25 @@ +using Microsoft.EntityFrameworkCore; +using TodoApiDto.Repositories.Data; + +namespace TodoApiDto.Repositories.Context +{ + public sealed class TodoContext : DbContext + { + public TodoContext(DbContextOptions options) + : base(options) + { + Database.EnsureCreated(); + } + + public DbSet TodoItems { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity().ToTable("todo_item"); + modelBuilder.Entity().HasKey(todoItem => todoItem.Id).HasName("id"); + modelBuilder.Entity().Property(todoItem => todoItem.Name).HasColumnName("name"); + modelBuilder.Entity().Property(todoItem => todoItem.Secret).HasColumnName("secret"); + modelBuilder.Entity().Property(todoItem => todoItem.IsComplete).HasColumnName("is_complete").IsRequired(); + } + } +} \ No newline at end of file diff --git a/Repositories/TodoApiDto.Repositories/TodoApiDto.Repositories.csproj b/Repositories/TodoApiDto.Repositories/TodoApiDto.Repositories.csproj new file mode 100644 index 00000000..2b26c935 --- /dev/null +++ b/Repositories/TodoApiDto.Repositories/TodoApiDto.Repositories.csproj @@ -0,0 +1,19 @@ + + + + netcoreapp3.1 + + + + + + + + + + + + + + + diff --git a/Repositories/TodoApiDto.Repositories/TodoRepository.cs b/Repositories/TodoApiDto.Repositories/TodoRepository.cs new file mode 100644 index 00000000..089d831d --- /dev/null +++ b/Repositories/TodoApiDto.Repositories/TodoRepository.cs @@ -0,0 +1,90 @@ +using Microsoft.EntityFrameworkCore; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using TodoApiDto.Repositories.Context; +using TodoApiDto.Repositories.Data; +using TodoApiDto.Repositories.Interfaces; +using TodoApiDto.Shared.Helpers; +using TodoApiDto.StrongId; + +namespace TodoApiDto.Repositories +{ + public class TodoRepository : ITodoRepository + { + private readonly TodoContext _context; + + public TodoRepository(TodoContext context) + { + _context = context; + } + + public async Task> GetAllAsync() + => await _context.TodoItems.AsNoTracking().ToListAsync(); + + public async Task GetByIdAsync(TodoId id) + { + id.ThrowIfNull(nameof(id)); + + return await _context.TodoItems + .Where(todoItem => todoItem.Id == id.ObjectId) + .AsNoTracking() + .FirstOrDefaultAsync(); + } + + public async Task RemoveAsync(TodoId id) + { + id.ThrowIfNull(nameof(id)); + + try + { + var item = new TodoItem + { + Id = id.ObjectId, + }; + + _context.TodoItems.Remove(item); + await _context.SaveChangesAsync(); + } + catch + { + if (await _context.TodoItems.AnyAsync(i => i.Id == id.ObjectId)) + { + throw; + } + } + } + + public async Task UpdateAsync(TodoItemUpdateModel updateModel) + { + updateModel.ThrowIfNull(nameof(updateModel)); + + var dbTodoItem = await _context.TodoItems + .FirstOrDefaultAsync(todoItem => todoItem.Id == updateModel.Id); + + dbTodoItem.Name = updateModel.Name; + dbTodoItem.IsComplete = updateModel.IsComplete; + + await _context.SaveChangesAsync(); + + return dbTodoItem; + } + + public async Task CreateAsync(TodoItemCreateModel createModel) + { + createModel.ThrowIfNull(nameof(createModel)); + + var todoItem = new TodoItem + { + IsComplete = createModel.IsComplete, + Name = createModel.Name, + }; + + var dbTodoItem = _context.TodoItems.Add(todoItem); + + await _context.SaveChangesAsync(); + + return dbTodoItem.Entity; + } + } +} \ No newline at end of file diff --git a/Services/TodoApiDto.Services.Data/ServiceResult.cs b/Services/TodoApiDto.Services.Data/ServiceResult.cs new file mode 100644 index 00000000..538b8a10 --- /dev/null +++ b/Services/TodoApiDto.Services.Data/ServiceResult.cs @@ -0,0 +1,31 @@ +namespace TodoApiDto.Services.Data +{ + public class ServiceResult + { + /// + /// + /// + public bool IsSuccess { get; set; } + + /// + /// + /// + public bool IsError { get; set; } = false; + + /// + /// + /// + public bool IsNotFound { get; set; } = false; + } + + /// + /// + /// + public class ServiceResult : ServiceResult + { + /// + /// + /// + public T Result { get; set; } + } +} \ No newline at end of file diff --git a/Services/TodoApiDto.Services.Data/TodoApiDto.Services.Data.csproj b/Services/TodoApiDto.Services.Data/TodoApiDto.Services.Data.csproj new file mode 100644 index 00000000..7e7fb806 --- /dev/null +++ b/Services/TodoApiDto.Services.Data/TodoApiDto.Services.Data.csproj @@ -0,0 +1,11 @@ + + + + netcoreapp3.1 + + + + + + + diff --git a/Services/TodoApiDto.Services.Data/TodoItem.cs b/Services/TodoApiDto.Services.Data/TodoItem.cs new file mode 100644 index 00000000..acc9f4be --- /dev/null +++ b/Services/TodoApiDto.Services.Data/TodoItem.cs @@ -0,0 +1,30 @@ +using TodoApiDto.StrongId; + +namespace TodoApiDto.Services.Data +{ + /// + /// Service TodoItem dto + /// + public class TodoItem + { + /// + /// Record id + /// + public TodoId Id { get; set; } + + /// + /// Content + /// + public string Name { get; set; } + + /// + /// Status + /// + public bool IsComplete { get; set; } + + /// + /// Secret + /// + public string Secret { get; set; } + } +} \ No newline at end of file diff --git a/Services/TodoApiDto.Services.Data/TodoItemCreateModel.cs b/Services/TodoApiDto.Services.Data/TodoItemCreateModel.cs new file mode 100644 index 00000000..6820127e --- /dev/null +++ b/Services/TodoApiDto.Services.Data/TodoItemCreateModel.cs @@ -0,0 +1,18 @@ +namespace TodoApiDto.Services.Data +{ + /// + /// Model for creating TODOItem + /// + public class TodoItemCreateModel + { + /// + /// Content + /// + public string Name { get; set; } + + /// + /// Status + /// + public bool IsComplete { get; set; } + } +} \ No newline at end of file diff --git a/Services/TodoApiDto.Services.Data/TodoItemUpdateModel.cs b/Services/TodoApiDto.Services.Data/TodoItemUpdateModel.cs new file mode 100644 index 00000000..2ded24e5 --- /dev/null +++ b/Services/TodoApiDto.Services.Data/TodoItemUpdateModel.cs @@ -0,0 +1,25 @@ +using TodoApiDto.StrongId; + +namespace TodoApiDto.Services.Data +{ + /// + /// Model for updating TODOItem + /// + public class TodoItemUpdateModel + { + /// + /// Record id + /// + public TodoId Id { get; set; } + + /// + /// Content + /// + public string Name { get; set; } + + /// + /// Status + /// + public bool IsComplete { get; set; } + } +} \ No newline at end of file diff --git a/Services/TodoApiDto.Services.Interfaces/ITodoService.cs b/Services/TodoApiDto.Services.Interfaces/ITodoService.cs new file mode 100644 index 00000000..06423745 --- /dev/null +++ b/Services/TodoApiDto.Services.Interfaces/ITodoService.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using TodoApiDto.Services.Data; +using TodoApiDto.StrongId; + +namespace TodoApiDto.Services.Interfaces +{ + /// + /// Service for working with TodoItem + /// + public interface ITodoService + { + /// + /// Get all TodoItems + /// + Task>> GetAllAsync(); + + /// + /// Get TodoItem by + /// + /// Record id + Task> GetByIdAsync(TodoId id); + + /// + /// Remove TodoItem by + /// + /// Record id + Task RemoveAsync(TodoId id); + + /// + /// Update TodoItem + /// + /// Update model + Task> UpdateAsync(TodoItemUpdateModel updateModel); + + /// + /// Create new TodoItem + /// + /// Create model + Task> CreateAsync(TodoItemCreateModel createModel); + } +} \ No newline at end of file diff --git a/Services/TodoApiDto.Services.Interfaces/TodoApiDto.Services.Interfaces.csproj b/Services/TodoApiDto.Services.Interfaces/TodoApiDto.Services.Interfaces.csproj new file mode 100644 index 00000000..8fd6419e --- /dev/null +++ b/Services/TodoApiDto.Services.Interfaces/TodoApiDto.Services.Interfaces.csproj @@ -0,0 +1,12 @@ + + + + netcoreapp3.1 + + + + + + + + diff --git a/Services/TodoApiDto.Services/TodoApiDto.Services.csproj b/Services/TodoApiDto.Services/TodoApiDto.Services.csproj new file mode 100644 index 00000000..0c13ccd6 --- /dev/null +++ b/Services/TodoApiDto.Services/TodoApiDto.Services.csproj @@ -0,0 +1,17 @@ + + + + netcoreapp3.1 + + + + + + + + + + + + + diff --git a/Services/TodoApiDto.Services/TodoService.cs b/Services/TodoApiDto.Services/TodoService.cs new file mode 100644 index 00000000..e128f8f4 --- /dev/null +++ b/Services/TodoApiDto.Services/TodoService.cs @@ -0,0 +1,121 @@ +using AutoMapper; +using System.Collections.Generic; +using System.Threading.Tasks; +using TodoApiDto.Repositories.Interfaces; +using TodoApiDto.Services.Interfaces; +using TodoApiDto.Shared.Helpers; +using TodoApiDto.StrongId; +using DbData = TodoApiDto.Repositories.Data; +using ServiceData = TodoApiDto.Services.Data; + +namespace TodoApiDto.Services +{ + public class TodoService : ITodoService + { + private readonly ITodoRepository _todoRepository; + private readonly IMapper _mapper; + + public TodoService( + ITodoRepository todoRepository, + IMapper mapper) + { + _todoRepository = todoRepository; + _mapper = mapper; + } + + public async Task>> GetAllAsync() + { + var dbData = await _todoRepository.GetAllAsync(); + + var serviceData = _mapper.Map>(dbData); + + return new ServiceData.ServiceResult> + { + Result = serviceData ?? new List(), + IsSuccess = true, + }; + } + + public async Task> GetByIdAsync(TodoId id) + { + id.ThrowIfNull(nameof(id)); + + var dbTodoItem = await _todoRepository.GetByIdAsync(id); + + return new ServiceData.ServiceResult + { + Result = _mapper.Map(dbTodoItem), + IsSuccess = dbTodoItem is null, + IsNotFound = dbTodoItem is null, + }; + } + + public async Task RemoveAsync(TodoId id) + { + id.ThrowIfNull(nameof(id)); + + var dbTodoItem = await _todoRepository.GetByIdAsync(id); + + if (dbTodoItem is null) + { + return new ServiceData.ServiceResult + { + IsSuccess = false, + IsError = false, + IsNotFound = true, + }; + } + + await _todoRepository.RemoveAsync(id); + + return new ServiceData.ServiceResult + { + IsSuccess = true, + }; + } + + public async Task> UpdateAsync( + ServiceData.TodoItemUpdateModel updateModel) + { + updateModel.ThrowIfNull(nameof(updateModel)); + + var dbTodoItem = await _todoRepository.GetByIdAsync(updateModel.Id); + + if (dbTodoItem is null) + { + return new ServiceData.ServiceResult + { + IsSuccess = false, + IsError = false, + IsNotFound = true, + }; + } + + var dbUpdateModel = _mapper.Map(updateModel); + + var updatedDbTodoItem = await _todoRepository.UpdateAsync(dbUpdateModel); + + return new ServiceData.ServiceResult + { + Result = _mapper.Map(updatedDbTodoItem), + IsSuccess = true, + }; + } + + public async Task> CreateAsync( + ServiceData.TodoItemCreateModel createModel) + { + createModel.ThrowIfNull(nameof(createModel)); + + var dbCreateModel = _mapper.Map(createModel); + + var createdDbTodoItem = await _todoRepository.CreateAsync(dbCreateModel); + + return new ServiceData.ServiceResult + { + Result = _mapper.Map(createdDbTodoItem), + IsSuccess = true, + }; + } + } +} \ No newline at end of file diff --git a/Shared/TodoApiDto.Shared.Api/Data/Requests/TodoItemCreateRequest.cs b/Shared/TodoApiDto.Shared.Api/Data/Requests/TodoItemCreateRequest.cs new file mode 100644 index 00000000..a945a1d4 --- /dev/null +++ b/Shared/TodoApiDto.Shared.Api/Data/Requests/TodoItemCreateRequest.cs @@ -0,0 +1,18 @@ +namespace TodoApiDto.Shared.Api.Data.Requests +{ + /// + /// Model of request to create TODOItem + /// + public class TodoItemCreateRequest + { + /// + /// Content + /// + public string Name { get; set; } + + /// + /// Status + /// + public bool IsComplete { get; set; } + } +} \ No newline at end of file diff --git a/Shared/TodoApiDto.Shared.Api/Data/Requests/TodoItemUpdateRequest.cs b/Shared/TodoApiDto.Shared.Api/Data/Requests/TodoItemUpdateRequest.cs new file mode 100644 index 00000000..171c828f --- /dev/null +++ b/Shared/TodoApiDto.Shared.Api/Data/Requests/TodoItemUpdateRequest.cs @@ -0,0 +1,23 @@ +namespace TodoApiDto.Shared.Api.Data.Requests +{ + /// + /// Model of request to update TODOItem + /// + public class TodoItemUpdateRequest + { + /// + /// Record id + /// + public long Id { get; set; } + + /// + /// Content + /// + public string Name { get; set; } + + /// + /// Status + /// + public bool IsComplete { get; set; } + } +} \ No newline at end of file diff --git a/Shared/TodoApiDto.Shared.Api/Data/TodoItemViewModel.cs b/Shared/TodoApiDto.Shared.Api/Data/TodoItemViewModel.cs new file mode 100644 index 00000000..7ceae9bf --- /dev/null +++ b/Shared/TodoApiDto.Shared.Api/Data/TodoItemViewModel.cs @@ -0,0 +1,23 @@ +namespace TodoApiDto.Shared.Api.Data +{ + /// + /// TODOItem Model + /// + public class TodoItemViewModel + { + /// + /// Record id + /// + public long Id { get; set; } + + /// + /// Content + /// + public string Name { get; set; } + + /// + /// Status + /// + public bool IsComplete { get; set; } + } +} \ No newline at end of file diff --git a/Shared/TodoApiDto.Shared.Api/TodoApiDto.Shared.Api.csproj b/Shared/TodoApiDto.Shared.Api/TodoApiDto.Shared.Api.csproj new file mode 100644 index 00000000..cb631906 --- /dev/null +++ b/Shared/TodoApiDto.Shared.Api/TodoApiDto.Shared.Api.csproj @@ -0,0 +1,7 @@ + + + + netcoreapp3.1 + + + diff --git a/Shared/TodoApiDto.Shared/Helpers/ObjectHelper.cs b/Shared/TodoApiDto.Shared/Helpers/ObjectHelper.cs new file mode 100644 index 00000000..fd4b0a26 --- /dev/null +++ b/Shared/TodoApiDto.Shared/Helpers/ObjectHelper.cs @@ -0,0 +1,18 @@ +using System; + +namespace TodoApiDto.Shared.Helpers +{ + public static class ObjectHelper + { + /// + /// Throwing an if the is null + /// + /// Argument type + /// Argument + /// Argument name + /// + /// + public static T ThrowIfNull(this T obj, string paramName) where T : class + => obj ?? throw new ArgumentNullException(paramName); + } +} \ No newline at end of file diff --git a/Shared/TodoApiDto.Shared/TodoApiDto.Shared.csproj b/Shared/TodoApiDto.Shared/TodoApiDto.Shared.csproj new file mode 100644 index 00000000..cb631906 --- /dev/null +++ b/Shared/TodoApiDto.Shared/TodoApiDto.Shared.csproj @@ -0,0 +1,7 @@ + + + + netcoreapp3.1 + + + 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..a941d48a 100644 --- a/TodoApiDTO.sln +++ b/TodoApiDTO.sln @@ -1,9 +1,37 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.30002.166 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.33530.505 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TodoApiDTO", "TodoApiDTO.csproj", "{623124F9-F5BA-42DD-BC26-A1720774229C}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Services", "Services", "{8D353DB9-38BB-4EFB-B4B8-116C58F62FE2}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Repositories", "Repositories", "{BE4D3342-1A9B-4367-AADD-5DD8587BFA59}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{82D7280B-C6A6-4769-975E-F2F735E3916C}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Infrastructure", "Infrastructure", "{E4FEAD28-1AD6-4B4B-A2FB-16F16F554EF2}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Applications", "Applications", "{53FEEEC7-E256-4E19-86D7-D989AF6F30D7}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TodoApiDto.StrongId", "Infrastructure\TodoApiDto.StrongId\TodoApiDto.StrongId.csproj", "{0A61B9D7-C377-483C-8A4A-666D76C15B81}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TodoApiDto.Repositories", "Repositories\TodoApiDto.Repositories\TodoApiDto.Repositories.csproj", "{A2F66901-F0DD-4B85-8B4D-D48BA86E9EAC}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TodoApiDto.Repositories.Data", "Repositories\TodoApiDto.Repositories.Data\TodoApiDto.Repositories.Data.csproj", "{918244C2-0637-41D0-93E2-917FDFECA7A6}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TodoApiDto.Repositories.Interfaces", "Repositories\TodoApiDto.Repositories.Interfaces\TodoApiDto.Repositories.Interfaces.csproj", "{4A65D8B2-CDC7-498B-9C3F-7BEDEA5EAF40}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TodoApiDto.Services", "Services\TodoApiDto.Services\TodoApiDto.Services.csproj", "{A8520D88-5688-429D-ABAF-343BB8EF0BFF}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TodoApiDto.Services.Data", "Services\TodoApiDto.Services.Data\TodoApiDto.Services.Data.csproj", "{8927E5B1-08DF-4E27-87BC-E8D7E8F19B13}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TodoApiDto.Services.Interfaces", "Services\TodoApiDto.Services.Interfaces\TodoApiDto.Services.Interfaces.csproj", "{AAB9A123-AF43-47EC-B35D-6DC5327DA064}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TodoApiDto.Shared", "Shared\TodoApiDto.Shared\TodoApiDto.Shared.csproj", "{3BFE1F28-FE17-486D-9237-C7696FB42CFE}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TodoApiDto.Shared.Api", "Shared\TodoApiDto.Shared.Api\TodoApiDto.Shared.Api.csproj", "{BE90DCC1-A5B5-4BF4-99A7-B6564F84DBEC}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TodoApi", "Applications\TodoApi\TodoApi.csproj", "{3D598E1F-35FF-4582-A1CC-C50C87F6C861}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -11,14 +39,62 @@ 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 + {0A61B9D7-C377-483C-8A4A-666D76C15B81}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0A61B9D7-C377-483C-8A4A-666D76C15B81}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0A61B9D7-C377-483C-8A4A-666D76C15B81}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0A61B9D7-C377-483C-8A4A-666D76C15B81}.Release|Any CPU.Build.0 = Release|Any CPU + {A2F66901-F0DD-4B85-8B4D-D48BA86E9EAC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A2F66901-F0DD-4B85-8B4D-D48BA86E9EAC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A2F66901-F0DD-4B85-8B4D-D48BA86E9EAC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A2F66901-F0DD-4B85-8B4D-D48BA86E9EAC}.Release|Any CPU.Build.0 = Release|Any CPU + {918244C2-0637-41D0-93E2-917FDFECA7A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {918244C2-0637-41D0-93E2-917FDFECA7A6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {918244C2-0637-41D0-93E2-917FDFECA7A6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {918244C2-0637-41D0-93E2-917FDFECA7A6}.Release|Any CPU.Build.0 = Release|Any CPU + {4A65D8B2-CDC7-498B-9C3F-7BEDEA5EAF40}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4A65D8B2-CDC7-498B-9C3F-7BEDEA5EAF40}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4A65D8B2-CDC7-498B-9C3F-7BEDEA5EAF40}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4A65D8B2-CDC7-498B-9C3F-7BEDEA5EAF40}.Release|Any CPU.Build.0 = Release|Any CPU + {A8520D88-5688-429D-ABAF-343BB8EF0BFF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A8520D88-5688-429D-ABAF-343BB8EF0BFF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A8520D88-5688-429D-ABAF-343BB8EF0BFF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A8520D88-5688-429D-ABAF-343BB8EF0BFF}.Release|Any CPU.Build.0 = Release|Any CPU + {8927E5B1-08DF-4E27-87BC-E8D7E8F19B13}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8927E5B1-08DF-4E27-87BC-E8D7E8F19B13}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8927E5B1-08DF-4E27-87BC-E8D7E8F19B13}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8927E5B1-08DF-4E27-87BC-E8D7E8F19B13}.Release|Any CPU.Build.0 = Release|Any CPU + {AAB9A123-AF43-47EC-B35D-6DC5327DA064}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AAB9A123-AF43-47EC-B35D-6DC5327DA064}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AAB9A123-AF43-47EC-B35D-6DC5327DA064}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AAB9A123-AF43-47EC-B35D-6DC5327DA064}.Release|Any CPU.Build.0 = Release|Any CPU + {3BFE1F28-FE17-486D-9237-C7696FB42CFE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3BFE1F28-FE17-486D-9237-C7696FB42CFE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3BFE1F28-FE17-486D-9237-C7696FB42CFE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3BFE1F28-FE17-486D-9237-C7696FB42CFE}.Release|Any CPU.Build.0 = Release|Any CPU + {BE90DCC1-A5B5-4BF4-99A7-B6564F84DBEC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BE90DCC1-A5B5-4BF4-99A7-B6564F84DBEC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BE90DCC1-A5B5-4BF4-99A7-B6564F84DBEC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BE90DCC1-A5B5-4BF4-99A7-B6564F84DBEC}.Release|Any CPU.Build.0 = Release|Any CPU + {3D598E1F-35FF-4582-A1CC-C50C87F6C861}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3D598E1F-35FF-4582-A1CC-C50C87F6C861}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3D598E1F-35FF-4582-A1CC-C50C87F6C861}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3D598E1F-35FF-4582-A1CC-C50C87F6C861}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {0A61B9D7-C377-483C-8A4A-666D76C15B81} = {E4FEAD28-1AD6-4B4B-A2FB-16F16F554EF2} + {A2F66901-F0DD-4B85-8B4D-D48BA86E9EAC} = {BE4D3342-1A9B-4367-AADD-5DD8587BFA59} + {918244C2-0637-41D0-93E2-917FDFECA7A6} = {BE4D3342-1A9B-4367-AADD-5DD8587BFA59} + {4A65D8B2-CDC7-498B-9C3F-7BEDEA5EAF40} = {BE4D3342-1A9B-4367-AADD-5DD8587BFA59} + {A8520D88-5688-429D-ABAF-343BB8EF0BFF} = {8D353DB9-38BB-4EFB-B4B8-116C58F62FE2} + {8927E5B1-08DF-4E27-87BC-E8D7E8F19B13} = {8D353DB9-38BB-4EFB-B4B8-116C58F62FE2} + {AAB9A123-AF43-47EC-B35D-6DC5327DA064} = {8D353DB9-38BB-4EFB-B4B8-116C58F62FE2} + {3BFE1F28-FE17-486D-9237-C7696FB42CFE} = {82D7280B-C6A6-4769-975E-F2F735E3916C} + {BE90DCC1-A5B5-4BF4-99A7-B6564F84DBEC} = {82D7280B-C6A6-4769-975E-F2F735E3916C} + {3D598E1F-35FF-4582-A1CC-C50C87F6C861} = {53FEEEC7-E256-4E19-86D7-D989AF6F30D7} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {60984BC9-01D1-4B40-9733-E459B1231F96} EndGlobalSection diff --git a/appsettings.json b/appsettings.json deleted file mode 100644 index d9d9a9bf..00000000 --- a/appsettings.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" - } - }, - "AllowedHosts": "*" -}