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/Todo.Api/Configuration/DatabaseConfiguration.cs b/Todo.Api/Configuration/DatabaseConfiguration.cs new file mode 100644 index 00000000..d943aaf7 --- /dev/null +++ b/Todo.Api/Configuration/DatabaseConfiguration.cs @@ -0,0 +1,16 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Todo.DAL; + +namespace Todo.Api.Configuration +{ + public static class DatabaseConfiguration + { + public static void ConfigureDatabase(this IServiceCollection services, IConfiguration configuration) + { + services.AddDbContext( + options => options.UseSqlServer(configuration.GetConnectionString("TodoDB"))); + } + } +} diff --git a/Todo.Api/Configuration/LoggingConfiguration.cs b/Todo.Api/Configuration/LoggingConfiguration.cs new file mode 100644 index 00000000..289c8402 --- /dev/null +++ b/Todo.Api/Configuration/LoggingConfiguration.cs @@ -0,0 +1,27 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Serilog; +using Serilog.Exceptions; + +namespace Todo.Api.Configuration +{ + public static class LoggingConfiguration + { + public static void ConfigureLogging(this IServiceCollection services, IConfiguration configuration) + { + Log.Logger = new LoggerConfiguration() + .Enrich.FromLogContext() + .Enrich.WithExceptionDetails() + .MinimumLevel.Error() + .ReadFrom.Configuration(configuration) + .CreateLogger(); + + services.AddLogging(loggingBuilder => + { + loggingBuilder.ClearProviders(); + loggingBuilder.AddSerilog(); + }); + } + } +} diff --git a/Todo.Api/Configuration/MappingConfiguration.cs b/Todo.Api/Configuration/MappingConfiguration.cs new file mode 100644 index 00000000..3e43f9b1 --- /dev/null +++ b/Todo.Api/Configuration/MappingConfiguration.cs @@ -0,0 +1,14 @@ +using FastExpressionCompiler; +using Mapster; +using Microsoft.Extensions.DependencyInjection; + +namespace Todo.Api.Configuration +{ + public static class MappingConfiguration + { + public static void ConfigureMapping(this IServiceCollection _) + { + TypeAdapterConfig.GlobalSettings.Compiler = exp => exp.CompileFast(); + } + } +} diff --git a/Todo.Api/Configuration/ServicesConfiguration.cs b/Todo.Api/Configuration/ServicesConfiguration.cs new file mode 100644 index 00000000..9b81ca2d --- /dev/null +++ b/Todo.Api/Configuration/ServicesConfiguration.cs @@ -0,0 +1,18 @@ +using MediatR; +using Microsoft.Extensions.DependencyInjection; +using Todo.BLL.Pipelines; + +namespace Todo.Api.Configuration +{ + public static class ServicesConfiguration + { + public static void ConfigureServices(this IServiceCollection services) + { + services.AddMediatR(cfg => { + cfg.RegisterServicesFromAssembly(typeof(ErrorLoggingPipeline<,>).Assembly); + cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(ErrorLoggingPipeline<,>)); + }); + services.AddControllers(); + } + } +} diff --git a/Todo.Api/Configuration/SwaggerConfiguration.cs b/Todo.Api/Configuration/SwaggerConfiguration.cs new file mode 100644 index 00000000..895e208d --- /dev/null +++ b/Todo.Api/Configuration/SwaggerConfiguration.cs @@ -0,0 +1,26 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.OpenApi.Models; + +namespace Todo.Api.Configuration +{ + public static class SwaggerConfiguration + { + public static void ConfigureSwagger(this IServiceCollection services) + { + services.AddSwaggerGen(c => + { + c.SwaggerDoc("v1", new OpenApiInfo { Title = "TODO API", Version = "v1" }); + }); + } + + public static void AddSwagger(this IApplicationBuilder app) + { + app.UseSwagger(); + app.UseSwaggerUI(c => + { + c.SwaggerEndpoint("/swagger/v1/swagger.json", "TODO API"); + }); + } + } +} diff --git a/Todo.Api/Controllers/TodoItemsController.cs b/Todo.Api/Controllers/TodoItemsController.cs new file mode 100644 index 00000000..d049d175 --- /dev/null +++ b/Todo.Api/Controllers/TodoItemsController.cs @@ -0,0 +1,64 @@ +using MediatR; +using Microsoft.AspNetCore.Mvc; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Todo.Common.Commands; +using Todo.Common.Dto; +using Todo.Common.Queries; + +namespace Todo.Api.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class TodoItemsController : ControllerBase + { + private readonly IMediator _mediator; + + public TodoItemsController(IMediator mediator) + { + _mediator = mediator; + } + + [HttpGet] + public async Task>> GetTodoItems(CancellationToken ct) + { + return Ok(await _mediator.Send(new GetAllTodosQuery(), ct)); + } + + [HttpGet("{id}")] + public async Task> GetTodoItem(long id, CancellationToken ct) + { + return Ok(await _mediator.Send(new GetTodoQuery(id), ct)); + } + + [HttpPut("{id}")] + public async Task UpdateTodoItem(long id, TodoItemDto todoItemDto, CancellationToken ct) + { + if (id != todoItemDto.Id) return BadRequest(); + + await _mediator.Send(new UpdateTodoCommand(todoItemDto), ct); + + return NoContent(); + } + + [HttpPost] + public async Task> CreateTodoItem(TodoItemDto todoItemDto, CancellationToken ct) + { + var result = await _mediator.Send(new CreateTodoCommand(todoItemDto), ct); + + return CreatedAtAction( + nameof(GetTodoItem), + new { id = result.Id }, + result); + } + + [HttpDelete("{id}")] + public async Task DeleteTodoItem(long id, CancellationToken ct) + { + await _mediator.Send(new DeleteTodoCommand(id), ct); + + return NoContent(); + } + } +} diff --git a/Todo.Api/Middleware/ErrorHandlingMiddleware.cs b/Todo.Api/Middleware/ErrorHandlingMiddleware.cs new file mode 100644 index 00000000..64aacbca --- /dev/null +++ b/Todo.Api/Middleware/ErrorHandlingMiddleware.cs @@ -0,0 +1,28 @@ +using Microsoft.AspNetCore.Http; +using System.Threading.Tasks; +using Todo.Common.Exceptions; + +namespace Todo.Api.Middleware +{ + public class ErrorHandlingMiddleware + { + private readonly RequestDelegate _next; + + public ErrorHandlingMiddleware(RequestDelegate next) + { + _next = next; + } + + public async Task InvokeAsync(HttpContext context) + { + try + { + await _next(context); + } + catch (NotFoundException) + { + context.Response.StatusCode = StatusCodes.Status404NotFound; + } + } + } +} diff --git a/Program.cs b/Todo.Api/Program.cs similarity index 71% rename from Program.cs rename to Todo.Api/Program.cs index b27ac16a..78e28c25 100644 --- a/Program.cs +++ b/Todo.Api/Program.cs @@ -1,13 +1,7 @@ -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 +namespace Todo.Api { public class Program { diff --git a/Properties/launchSettings.json b/Todo.Api/Properties/launchSettings.json similarity index 80% rename from Properties/launchSettings.json rename to Todo.Api/Properties/launchSettings.json index 6766196a..45bdcf86 100644 --- a/Properties/launchSettings.json +++ b/Todo.Api/Properties/launchSettings.json @@ -11,13 +11,15 @@ "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, + "launchUrl": "https://localhost:44331/swagger/index.html", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, - "TodoApiDTO": { + "Todo.ApiDTO": { "commandName": "Project", "launchBrowser": true, + "launchUrl": "https://localhost:44331/swagger/index.html", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, diff --git a/Startup.cs b/Todo.Api/Startup.cs similarity index 70% rename from Startup.cs rename to Todo.Api/Startup.cs index bbfbc83d..3049b080 100644 --- a/Startup.cs +++ b/Todo.Api/Startup.cs @@ -1,19 +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; - -namespace TodoApi +using Todo.Api.Configuration; +using Todo.Api.Middleware; +using Todo.DAL; + +namespace Todo.Api { public class Startup { @@ -27,29 +22,35 @@ public Startup(IConfiguration configuration) // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { - services.AddDbContext(opt => - opt.UseInMemoryDatabase("TodoList")); - services.AddControllers(); + services.ConfigureDatabase(Configuration); + services.ConfigureLogging(Configuration); + services.ConfigureServices(); + services.ConfigureSwagger(); + services.ConfigureMapping(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + public void Configure(IApplicationBuilder app, IWebHostEnvironment env, TodoContext context) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } - app.UseHttpsRedirection(); + app.UseMiddleware(); + app.UseHttpsRedirection(); app.UseRouting(); - app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); + + app.AddSwagger(); + + context.Database.Migrate(); } } } diff --git a/Todo.Api/Todo.Api.csproj b/Todo.Api/Todo.Api.csproj new file mode 100644 index 00000000..1ab3b191 --- /dev/null +++ b/Todo.Api/Todo.Api.csproj @@ -0,0 +1,30 @@ + + + + netcoreapp3.1 + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/appsettings.Development.json b/Todo.Api/appsettings.Development.json similarity index 100% rename from appsettings.Development.json rename to Todo.Api/appsettings.Development.json diff --git a/Todo.Api/appsettings.json b/Todo.Api/appsettings.json new file mode 100644 index 00000000..b085ed3e --- /dev/null +++ b/Todo.Api/appsettings.json @@ -0,0 +1,24 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "Serilog": { + "WriteTo": [ + { + "Name": "File", + "Args": { + "path": "logs/log-.txt", + "rollingInterval": "Day" + } + } + ] + }, + "ConnectionStrings": { + "TodoDB": "Server=localhost\\SQLEXPRESS;Database=TodoDB;Trusted_Connection=True;MultipleActiveResultSets=true" + }, + "AllowedHosts": "*" +} diff --git a/Todo.BLL/CommandHandlers/CreateTodoCommandHandler.cs b/Todo.BLL/CommandHandlers/CreateTodoCommandHandler.cs new file mode 100644 index 00000000..df5047ce --- /dev/null +++ b/Todo.BLL/CommandHandlers/CreateTodoCommandHandler.cs @@ -0,0 +1,34 @@ +using Mapster; +using MediatR; +using System.Threading; +using System.Threading.Tasks; +using Todo.Common.Commands; +using Todo.Common.Dto; +using Todo.DAL; +using Todo.DAL.Models; + +namespace Todo.BLL.CommandHandlers +{ + public class CreateTodoCommandHandler : IRequestHandler + { + private readonly TodoContext _context; + + public CreateTodoCommandHandler(TodoContext todoContext) + { + _context = todoContext; + } + + public async Task Handle(CreateTodoCommand request, CancellationToken ct) + { + var todoItem = request.Item.Adapt(); + + todoItem.Id = default; + + _context.Set().Add(todoItem); + + await _context.SaveChangesAsync(ct); + + return todoItem.Adapt(); + } + } +} diff --git a/Todo.BLL/CommandHandlers/DeleteTodoCommandHandler.cs b/Todo.BLL/CommandHandlers/DeleteTodoCommandHandler.cs new file mode 100644 index 00000000..2730607c --- /dev/null +++ b/Todo.BLL/CommandHandlers/DeleteTodoCommandHandler.cs @@ -0,0 +1,44 @@ +using MediatR; +using Microsoft.EntityFrameworkCore; +using System.Threading; +using System.Threading.Tasks; +using Todo.BLL.Queries; +using Todo.Common.Commands; +using Todo.Common.Exceptions; +using Todo.DAL; +using Todo.DAL.Models; + +namespace Todo.BLL.CommandHandlers +{ + public class DeleteTodoCommandHandler : IRequestHandler + { + private readonly IMediator _mediator; + private readonly TodoContext _context; + + public DeleteTodoCommandHandler(IMediator mediator, TodoContext context) + { + _mediator = mediator; + _context = context; + } + + public async Task Handle(DeleteTodoCommand request, CancellationToken ct) + { + var todoItem = await _mediator.Send(new GetTodoEntityQuery(request.Id), ct) ?? throw new NotFoundException(); + + _context.Set().Remove(todoItem); + + try + { + await _context.SaveChangesAsync(ct); + } + catch (DbUpdateConcurrencyException) + { + await _mediator.Send(new GetTodoEntityQuery(request.Id)); + + if (todoItem == null) throw new NotFoundException(); + + throw; + } + } + } +} diff --git a/Todo.BLL/CommandHandlers/UpdateTodoCommandHandler.cs b/Todo.BLL/CommandHandlers/UpdateTodoCommandHandler.cs new file mode 100644 index 00000000..d8ffd58a --- /dev/null +++ b/Todo.BLL/CommandHandlers/UpdateTodoCommandHandler.cs @@ -0,0 +1,44 @@ +using Mapster; +using MediatR; +using Microsoft.EntityFrameworkCore; +using System.Threading; +using System.Threading.Tasks; +using Todo.BLL.Queries; +using Todo.Common.Commands; +using Todo.Common.Exceptions; +using Todo.DAL; + +namespace Todo.BLL.CommandHandlers +{ + public class UpdateTodoCommandHandler : IRequestHandler + { + private readonly IMediator _mediator; + private readonly TodoContext _context; + + public UpdateTodoCommandHandler(IMediator mediator, TodoContext context) + { + _mediator = mediator; + _context = context; + } + + public async Task Handle(UpdateTodoCommand request, CancellationToken ct) + { + var todoItem = await _mediator.Send(new GetTodoEntityQuery(request.Item.Id)) ?? throw new NotFoundException(); + + request.Item.Adapt(todoItem); + + try + { + await _context.SaveChangesAsync(ct); + } + catch(DbUpdateConcurrencyException) + { + await _mediator.Send(new GetTodoEntityQuery(request.Item.Id)); + + if (todoItem == null) throw new NotFoundException(); + + throw; + } + } + } +} diff --git a/Todo.BLL/Pipelines/ErrorLoggingPipeline.cs b/Todo.BLL/Pipelines/ErrorLoggingPipeline.cs new file mode 100644 index 00000000..83e5d73c --- /dev/null +++ b/Todo.BLL/Pipelines/ErrorLoggingPipeline.cs @@ -0,0 +1,38 @@ +using MediatR; +using Microsoft.Extensions.Logging; +using System; +using System.Data; +using System.Threading; +using System.Threading.Tasks; +using Todo.Common.Exceptions; + +namespace Todo.BLL.Pipelines +{ + public class ErrorLoggingPipeline : IPipelineBehavior + { + private readonly ILogger> _logger; + + public ErrorLoggingPipeline(ILogger> logger) + { + _logger = logger; + } + + public async Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken ct) + { + try + { + return await next.Invoke(); + } + catch(NotFoundException notFoundException) + { + _logger.LogError(notFoundException, "An attempt to use non-existent entity."); + throw; + } + catch(Exception ex) + { + _logger.LogError(ex, "An unexpected error occured while executing request."); + throw; + } + } + } +} diff --git a/Todo.BLL/Queries/GetTodoEntityQuery.cs b/Todo.BLL/Queries/GetTodoEntityQuery.cs new file mode 100644 index 00000000..2b6565b2 --- /dev/null +++ b/Todo.BLL/Queries/GetTodoEntityQuery.cs @@ -0,0 +1,15 @@ +using MediatR; +using Todo.DAL.Models; + +namespace Todo.BLL.Queries +{ + public class GetTodoEntityQuery : IRequest + { + public long Id { get; } + + public GetTodoEntityQuery(long id) + { + Id = id; + } + } +} diff --git a/Todo.BLL/QueryHandlers/GetAllTodosQueryHandler.cs b/Todo.BLL/QueryHandlers/GetAllTodosQueryHandler.cs new file mode 100644 index 00000000..452cbb10 --- /dev/null +++ b/Todo.BLL/QueryHandlers/GetAllTodosQueryHandler.cs @@ -0,0 +1,30 @@ +using Mapster; +using MediatR; +using Microsoft.EntityFrameworkCore; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Todo.Common.Dto; +using Todo.Common.Queries; +using Todo.DAL; + +namespace Todo.BLL.QueryHandlers +{ + public class GetAllTodosQueryHandler : IRequestHandler> + { + private readonly TodoContext _context; + + public GetAllTodosQueryHandler(TodoContext context) + { + _context = context; + } + + public async Task> Handle(GetAllTodosQuery request, CancellationToken ct) + { + return await _context.TodoItems + .Select(item => item.Adapt()) + .ToListAsync(ct); + } + } +} diff --git a/Todo.BLL/QueryHandlers/GetTodoEntityQueryHandler.cs b/Todo.BLL/QueryHandlers/GetTodoEntityQueryHandler.cs new file mode 100644 index 00000000..ddbf6903 --- /dev/null +++ b/Todo.BLL/QueryHandlers/GetTodoEntityQueryHandler.cs @@ -0,0 +1,29 @@ +using MediatR; +using System.Threading; +using System.Threading.Tasks; +using Todo.BLL.Queries; +using Todo.Common.Exceptions; +using Todo.DAL; +using Todo.DAL.Models; + +namespace Todo.BLL.QueryHandlers +{ + public class GetTodoEntityQueryHandler : IRequestHandler + { + private readonly TodoContext _context; + + public GetTodoEntityQueryHandler(TodoContext context) + { + _context = context; + } + + public async Task Handle(GetTodoEntityQuery request, CancellationToken ct) + { + var todoItem = await _context + .Set() + .FindAsync(new object[] { request.Id }, ct); + + return todoItem; + } + } +} diff --git a/Todo.BLL/QueryHandlers/GetTodoQueryHandler.cs b/Todo.BLL/QueryHandlers/GetTodoQueryHandler.cs new file mode 100644 index 00000000..d66e262f --- /dev/null +++ b/Todo.BLL/QueryHandlers/GetTodoQueryHandler.cs @@ -0,0 +1,30 @@ +using Mapster; +using MediatR; +using System.Threading; +using System.Threading.Tasks; +using Todo.BLL.Queries; +using Todo.Common.Dto; +using Todo.Common.Exceptions; +using Todo.Common.Queries; + +namespace Todo.BLL.QueryHandlers +{ + public class GetTodoQueryHandler : IRequestHandler + { + private readonly IMediator _mediator; + + public GetTodoQueryHandler(IMediator mediator) + { + _mediator = mediator; + } + + public async Task Handle(GetTodoQuery request, CancellationToken cancellationToken) + { + var todoItem = await _mediator.Send(new GetTodoEntityQuery(request.Id)); + + if (todoItem == null) throw new NotFoundException(); + + return todoItem.Adapt(); + } + } +} diff --git a/Todo.BLL/Todo.BLL.csproj b/Todo.BLL/Todo.BLL.csproj new file mode 100644 index 00000000..cd0a106a --- /dev/null +++ b/Todo.BLL/Todo.BLL.csproj @@ -0,0 +1,17 @@ + + + + netstandard2.0 + + + + + + + + + + + + + diff --git a/Todo.Common/Commands/CreateTodoCommand.cs b/Todo.Common/Commands/CreateTodoCommand.cs new file mode 100644 index 00000000..8f27e9d0 --- /dev/null +++ b/Todo.Common/Commands/CreateTodoCommand.cs @@ -0,0 +1,15 @@ +using MediatR; +using Todo.Common.Dto; + +namespace Todo.Common.Commands +{ + public class CreateTodoCommand : IRequest + { + public CreateTodoCommand(TodoItemDto item) + { + Item = item; + } + + public TodoItemDto Item { get; set; } + } +} diff --git a/Todo.Common/Commands/DeleteTodoCommand.cs b/Todo.Common/Commands/DeleteTodoCommand.cs new file mode 100644 index 00000000..b86f9893 --- /dev/null +++ b/Todo.Common/Commands/DeleteTodoCommand.cs @@ -0,0 +1,14 @@ +using MediatR; + +namespace Todo.Common.Commands +{ + public class DeleteTodoCommand : IRequest + { + public long Id { get; } + + public DeleteTodoCommand(long id) + { + Id = id; + } + } +} diff --git a/Todo.Common/Commands/UpdateTodoCommand.cs b/Todo.Common/Commands/UpdateTodoCommand.cs new file mode 100644 index 00000000..0f5bddda --- /dev/null +++ b/Todo.Common/Commands/UpdateTodoCommand.cs @@ -0,0 +1,15 @@ +using MediatR; +using Todo.Common.Dto; + +namespace Todo.Common.Commands +{ + public class UpdateTodoCommand : IRequest + { + public UpdateTodoCommand(TodoItemDto item) + { + Item = item; + } + + public TodoItemDto Item { get; } + } +} diff --git a/Models/TodoItemDTO.cs b/Todo.Common/Dto/TodoItemDto.cs similarity index 75% rename from Models/TodoItemDTO.cs rename to Todo.Common/Dto/TodoItemDto.cs index e66a500a..ed5c50c2 100644 --- a/Models/TodoItemDTO.cs +++ b/Todo.Common/Dto/TodoItemDto.cs @@ -1,7 +1,7 @@ -namespace TodoApi.Models +namespace Todo.Common.Dto { #region snippet - public class TodoItemDTO + public class TodoItemDto { public long Id { get; set; } public string Name { get; set; } diff --git a/Todo.Common/Exceptions/NotFoundException.cs b/Todo.Common/Exceptions/NotFoundException.cs new file mode 100644 index 00000000..2d3f477f --- /dev/null +++ b/Todo.Common/Exceptions/NotFoundException.cs @@ -0,0 +1,8 @@ +using System; + +namespace Todo.Common.Exceptions +{ + public class NotFoundException : Exception + { + } +} diff --git a/Todo.Common/Queries/GetAllTodosQuery.cs b/Todo.Common/Queries/GetAllTodosQuery.cs new file mode 100644 index 00000000..2ba65f85 --- /dev/null +++ b/Todo.Common/Queries/GetAllTodosQuery.cs @@ -0,0 +1,10 @@ +using MediatR; +using System.Collections.Generic; +using Todo.Common.Dto; + +namespace Todo.Common.Queries +{ + public class GetAllTodosQuery : IRequest> + { + } +} diff --git a/Todo.Common/Queries/GetTodoQuery.cs b/Todo.Common/Queries/GetTodoQuery.cs new file mode 100644 index 00000000..81dae39a --- /dev/null +++ b/Todo.Common/Queries/GetTodoQuery.cs @@ -0,0 +1,15 @@ +using MediatR; +using Todo.Common.Dto; + +namespace Todo.Common.Queries +{ + public class GetTodoQuery : IRequest + { + public long Id { get; } + + public GetTodoQuery(long id) + { + Id = id; + } + } +} diff --git a/Todo.Common/Todo.Common.csproj b/Todo.Common/Todo.Common.csproj new file mode 100644 index 00000000..457a6acf --- /dev/null +++ b/Todo.Common/Todo.Common.csproj @@ -0,0 +1,11 @@ + + + + netstandard2.0 + + + + + + + diff --git a/Todo.DAL/Migrations/20230512121731_InitialMigration.Designer.cs b/Todo.DAL/Migrations/20230512121731_InitialMigration.Designer.cs new file mode 100644 index 00000000..aac21e7e --- /dev/null +++ b/Todo.DAL/Migrations/20230512121731_InitialMigration.Designer.cs @@ -0,0 +1,46 @@ +// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Todo.DAL; + +namespace Todo.DAL.Migrations +{ + [DbContext(typeof(TodoContext))] + [Migration("20230512121731_InitialMigration")] + partial class InitialMigration + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.32") + .HasAnnotation("Relational:MaxIdentifierLength", 128) + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("Todo.DAL.Models.TodoItemEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("IsComplete") + .HasColumnType("bit"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Secret") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("TodoItems"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Todo.DAL/Migrations/20230512121731_InitialMigration.cs b/Todo.DAL/Migrations/20230512121731_InitialMigration.cs new file mode 100644 index 00000000..151b3259 --- /dev/null +++ b/Todo.DAL/Migrations/20230512121731_InitialMigration.cs @@ -0,0 +1,31 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Todo.DAL.Migrations +{ + public partial class InitialMigration : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "TodoItems", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Name = table.Column(nullable: true), + IsComplete = table.Column(nullable: false), + Secret = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_TodoItems", x => x.Id); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "TodoItems"); + } + } +} diff --git a/Todo.DAL/Migrations/TodoContextModelSnapshot.cs b/Todo.DAL/Migrations/TodoContextModelSnapshot.cs new file mode 100644 index 00000000..6614978a --- /dev/null +++ b/Todo.DAL/Migrations/TodoContextModelSnapshot.cs @@ -0,0 +1,44 @@ +// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Todo.DAL; + +namespace Todo.DAL.Migrations +{ + [DbContext(typeof(TodoContext))] + partial class TodoContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.32") + .HasAnnotation("Relational:MaxIdentifierLength", 128) + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("Todo.DAL.Models.TodoItemEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("IsComplete") + .HasColumnType("bit"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Secret") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("TodoItems"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Models/TodoItem.cs b/Todo.DAL/Models/TodoItemEntity.cs similarity index 77% rename from Models/TodoItem.cs rename to Todo.DAL/Models/TodoItemEntity.cs index 1f6e5465..49c2d9c7 100644 --- a/Models/TodoItem.cs +++ b/Todo.DAL/Models/TodoItemEntity.cs @@ -1,7 +1,7 @@ -namespace TodoApi.Models +namespace Todo.DAL.Models { #region snippet - public class TodoItem + public class TodoItemEntity { public long Id { get; set; } public string Name { get; set; } diff --git a/Todo.DAL/Todo.DAL.csproj b/Todo.DAL/Todo.DAL.csproj new file mode 100644 index 00000000..fa1b8cea --- /dev/null +++ b/Todo.DAL/Todo.DAL.csproj @@ -0,0 +1,26 @@ + + + + netstandard2.0 + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + diff --git a/Models/TodoContext.cs b/Todo.DAL/TodoContext.cs similarity index 66% rename from Models/TodoContext.cs rename to Todo.DAL/TodoContext.cs index 6e59e363..043843ec 100644 --- a/Models/TodoContext.cs +++ b/Todo.DAL/TodoContext.cs @@ -1,6 +1,7 @@ using Microsoft.EntityFrameworkCore; - -namespace TodoApi.Models +using Todo.DAL.Models; + +namespace Todo.DAL { 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/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..53d24ae2 100644 --- a/TodoApiDTO.sln +++ b/TodoApiDTO.sln @@ -1,25 +1,43 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.30002.166 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TodoApiDTO", "TodoApiDTO.csproj", "{623124F9-F5BA-42DD-BC26-A1720774229C}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - 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 - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {60984BC9-01D1-4B40-9733-E459B1231F96} - EndGlobalSection -EndGlobal + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.33502.453 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Todo.Api", "Todo.Api\Todo.Api.csproj", "{623124F9-F5BA-42DD-BC26-A1720774229C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Todo.DAL", "Todo.DAL\Todo.DAL.csproj", "{23311B65-C65D-4512-8E12-4B349A285F2C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Todo.BLL", "Todo.BLL\Todo.BLL.csproj", "{94317018-4D43-4CFC-AE6D-E9AFC7AE5FE8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Todo.Common", "Todo.Common\Todo.Common.csproj", "{8257E296-C746-4ACB-A78D-48ED6CF11172}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + 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 + {23311B65-C65D-4512-8E12-4B349A285F2C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {23311B65-C65D-4512-8E12-4B349A285F2C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {23311B65-C65D-4512-8E12-4B349A285F2C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {23311B65-C65D-4512-8E12-4B349A285F2C}.Release|Any CPU.Build.0 = Release|Any CPU + {94317018-4D43-4CFC-AE6D-E9AFC7AE5FE8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {94317018-4D43-4CFC-AE6D-E9AFC7AE5FE8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {94317018-4D43-4CFC-AE6D-E9AFC7AE5FE8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {94317018-4D43-4CFC-AE6D-E9AFC7AE5FE8}.Release|Any CPU.Build.0 = Release|Any CPU + {8257E296-C746-4ACB-A78D-48ED6CF11172}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8257E296-C746-4ACB-A78D-48ED6CF11172}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8257E296-C746-4ACB-A78D-48ED6CF11172}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8257E296-C746-4ACB-A78D-48ED6CF11172}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {60984BC9-01D1-4B40-9733-E459B1231F96} + EndGlobalSection +EndGlobal 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": "*" -}