diff --git a/.gitignore b/.gitignore index 4ce6fdde..f6660769 100644 --- a/.gitignore +++ b/.gitignore @@ -337,4 +337,5 @@ ASALocalRun/ .localhistory/ # BeatPulse healthcheck temp database -healthchecksdb \ No newline at end of file +healthchecksdb +Logs/logs.txt diff --git a/Application/Application.csproj b/Application/Application.csproj new file mode 100644 index 00000000..25878756 --- /dev/null +++ b/Application/Application.csproj @@ -0,0 +1,22 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + + + + + + diff --git a/Application/DependencyInjection.cs b/Application/DependencyInjection.cs new file mode 100644 index 00000000..920f9695 --- /dev/null +++ b/Application/DependencyInjection.cs @@ -0,0 +1,19 @@ +using FluentValidation; +using Microsoft.Extensions.DependencyInjection; + +namespace Application; + +public static class DependencyInjection +{ + public static IServiceCollection AddApplication(this IServiceCollection services) + { + var assembly = typeof(DependencyInjection).Assembly; + + services.AddMediatR(configuration => + configuration.RegisterServicesFromAssembly(assembly)); + + services.AddValidatorsFromAssembly(assembly); + + return services; + } +} \ No newline at end of file diff --git a/Application/Exceptions/EntityNotFoundException.cs b/Application/Exceptions/EntityNotFoundException.cs new file mode 100644 index 00000000..a7de0629 --- /dev/null +++ b/Application/Exceptions/EntityNotFoundException.cs @@ -0,0 +1,17 @@ +using System.Net; + +namespace Application.Exceptions; + +public class EntityNotFoundException : Exception +{ + public static HttpStatusCode StatusCode = HttpStatusCode.NotFound; + + public EntityNotFoundException() : base() + {} + + public EntityNotFoundException(string message) : base(message) + {} + + public EntityNotFoundException(string message, Exception innerException) : base(message, innerException) + {} +} \ No newline at end of file diff --git a/Application/Exceptions/ErrorDetails.cs b/Application/Exceptions/ErrorDetails.cs new file mode 100644 index 00000000..a18d4cf5 --- /dev/null +++ b/Application/Exceptions/ErrorDetails.cs @@ -0,0 +1,15 @@ +using System.Text.Json; + +namespace Application.Exceptions; + +public class ErrorDetails +{ + public int StatusCode { get; set; } + + public string Message { get; set; } + + public override string ToString() + { + return JsonSerializer.Serialize(this); + } +} \ No newline at end of file diff --git a/Application/Interfaces/ITodoItemsRepository.cs b/Application/Interfaces/ITodoItemsRepository.cs new file mode 100644 index 00000000..c502151f --- /dev/null +++ b/Application/Interfaces/ITodoItemsRepository.cs @@ -0,0 +1,57 @@ +using Domain.Entities; + +namespace Application.Interfaces; + +public interface ITodoItemsRepository +{ + /// + /// Gets every TodoItem + /// + /// Collection of TodoItems + Task> GetAll(); + + /// + /// Gets every TodoItem and separates them by pages + /// + /// Page + /// Items per page + /// Collection of TodoItems + Task> GetAllPaginated(int page = 1, int pageSize = 10); + + /// + /// Gets a specific TodoItem by its ID + /// + /// TodoItem ID + /// TodoItem + Task GetById(long todoItemId); + + /// + /// Creates a new TodoItem + /// + /// New TodoItem + /// Created TodoItem + Task CreateTodoItem(TodoItem todoItem); + + /// + /// Updates an existing TodoItem + /// + /// TodoItem ID + /// Updated TodoItem data + /// Updated TodoItem + Task UpdateTodoItem(long todoItemId, TodoItem todoItem); + + /// + /// Updates TodoItem's secret + /// + /// TodoItem ID + /// TodoItem Secret + /// Updated TodoItem + Task UpdateTodoItemSecret(long todoItemId, string secret); + + /// + /// Deletes an existing TodoItem by ID + /// + /// TodoItem ID + /// + Task DeleteTodoItem(long todoItemId); +} \ No newline at end of file diff --git a/Application/TodoItems/CommandHandlers/CreateTodoItemHandler.cs b/Application/TodoItems/CommandHandlers/CreateTodoItemHandler.cs new file mode 100644 index 00000000..b331173c --- /dev/null +++ b/Application/TodoItems/CommandHandlers/CreateTodoItemHandler.cs @@ -0,0 +1,28 @@ +using Application.Interfaces; +using Application.TodoItems.Commands; +using Domain.Entities; +using MediatR; + +namespace Application.TodoItems.CommandHandlers; + +public class CreateTodoItemHandler : IRequestHandler +{ + private readonly ITodoItemsRepository _todoItemsRepository; + + public CreateTodoItemHandler(ITodoItemsRepository todoItemsRepository) + { + _todoItemsRepository = todoItemsRepository; + } + + public async Task Handle(CreateTodoItem request, CancellationToken cancellationToken) + { + var newTodoItem = new TodoItem + { + Name = request.Name, + IsComplete = request.IsComplete, + Secret = request.Secret + }; + + return await _todoItemsRepository.CreateTodoItem(newTodoItem); + } +} \ No newline at end of file diff --git a/Application/TodoItems/CommandHandlers/DeleteTodoItemHandler.cs b/Application/TodoItems/CommandHandlers/DeleteTodoItemHandler.cs new file mode 100644 index 00000000..9302754e --- /dev/null +++ b/Application/TodoItems/CommandHandlers/DeleteTodoItemHandler.cs @@ -0,0 +1,21 @@ +using Application.Interfaces; +using Application.TodoItems.Commands; +using Domain.Entities; +using MediatR; + +namespace Application.TodoItems.CommandHandlers; + +public class DeleteTodoItemHandler : IRequestHandler +{ + private readonly ITodoItemsRepository _todoItemsRepository; + + public DeleteTodoItemHandler(ITodoItemsRepository todoItemsRepository) + { + _todoItemsRepository = todoItemsRepository; + } + + public async Task Handle(DeleteTodoItem request, CancellationToken cancellationToken) + { + await _todoItemsRepository.DeleteTodoItem(request.Id); + } +} \ No newline at end of file diff --git a/Application/TodoItems/CommandHandlers/UpdateTodoItemHandler.cs b/Application/TodoItems/CommandHandlers/UpdateTodoItemHandler.cs new file mode 100644 index 00000000..775f60b9 --- /dev/null +++ b/Application/TodoItems/CommandHandlers/UpdateTodoItemHandler.cs @@ -0,0 +1,26 @@ +using Application.Interfaces; +using Application.TodoItems.Commands; +using Domain.Entities; +using MediatR; + +namespace Application.TodoItems.CommandHandlers; + +public class UpdateTodoItemHandler : IRequestHandler +{ + private readonly ITodoItemsRepository _todoItemsRepository; + + public UpdateTodoItemHandler(ITodoItemsRepository todoItemsRepository) + { + _todoItemsRepository = todoItemsRepository; + } + + public async Task Handle(UpdateTodoItem request, CancellationToken cancellationToken) + { + var updatedTodoItem = new TodoItem + { + Name = request.Name, + IsComplete = request.IsComplete + }; + return await _todoItemsRepository.UpdateTodoItem(request.Id, updatedTodoItem); + } +} \ No newline at end of file diff --git a/Application/TodoItems/Commands/CreateTodoItem.cs b/Application/TodoItems/Commands/CreateTodoItem.cs new file mode 100644 index 00000000..9b1b8eb0 --- /dev/null +++ b/Application/TodoItems/Commands/CreateTodoItem.cs @@ -0,0 +1,23 @@ +using Application.TodoItems.Models; +using Domain.Entities; +using MediatR; + +namespace Application.TodoItems.Commands; + +public class CreateTodoItem : IRequest +{ + public string Name { get; set; } + + public bool IsComplete { get; set; } + + public string Secret { get; set; } + + public CreateTodoItem() + {} + + public CreateTodoItem(CreateTodoItemRequest request) + { + Name = request.Name; + IsComplete = request.IsComplete; + } +} \ No newline at end of file diff --git a/Application/TodoItems/Commands/DeleteTodoItem.cs b/Application/TodoItems/Commands/DeleteTodoItem.cs new file mode 100644 index 00000000..9fed2a74 --- /dev/null +++ b/Application/TodoItems/Commands/DeleteTodoItem.cs @@ -0,0 +1,16 @@ +using MediatR; + +namespace Application.TodoItems.Commands; + +public class DeleteTodoItem : IRequest +{ + public long Id { get; set; } + + public DeleteTodoItem() + {} + + public DeleteTodoItem(long id) + { + Id = id; + } +} \ No newline at end of file diff --git a/Application/TodoItems/Commands/UpdateTodoItem.cs b/Application/TodoItems/Commands/UpdateTodoItem.cs new file mode 100644 index 00000000..49e8d875 --- /dev/null +++ b/Application/TodoItems/Commands/UpdateTodoItem.cs @@ -0,0 +1,24 @@ +using Application.TodoItems.Models; +using Domain.Entities; +using MediatR; + +namespace Application.TodoItems.Commands; + +public class UpdateTodoItem : IRequest +{ + public long Id { get; set; } + + public string Name { get; set; } + + public bool IsComplete { get; set; } + + public UpdateTodoItem() + {} + + public UpdateTodoItem(UpdateTodoItemRequest request) + { + Id = request.Id; + Name = request.Name; + IsComplete = request.IsComplete; + } +} \ No newline at end of file diff --git a/Application/TodoItems/Models/CreateTodoItemRequest.cs b/Application/TodoItems/Models/CreateTodoItemRequest.cs new file mode 100644 index 00000000..ba579b63 --- /dev/null +++ b/Application/TodoItems/Models/CreateTodoItemRequest.cs @@ -0,0 +1,8 @@ +namespace Application.TodoItems.Models; + +public class CreateTodoItemRequest +{ + public string Name { get; set; } + + public bool IsComplete { get; set; } +} \ No newline at end of file diff --git a/Application/TodoItems/Models/TodoItemFullModel.cs b/Application/TodoItems/Models/TodoItemFullModel.cs new file mode 100644 index 00000000..2f53f561 --- /dev/null +++ b/Application/TodoItems/Models/TodoItemFullModel.cs @@ -0,0 +1,25 @@ +using Domain.Entities; + +namespace Application.TodoItems.Models; + +public class TodoItemFullModel +{ + public long Id { get; set; } + + public string Name { get; set; } + + public bool IsComplete { get; set; } + + public string Secret { get; set; } + + public TodoItemFullModel() + {} + + public TodoItemFullModel(TodoItem todoItem) + { + Id = todoItem.Id; + Name = todoItem.Name; + IsComplete = todoItem.IsComplete; + Secret = todoItem.Secret; + } +} \ No newline at end of file diff --git a/Application/TodoItems/Models/TodoItemShortModel.cs b/Application/TodoItems/Models/TodoItemShortModel.cs new file mode 100644 index 00000000..d5760a57 --- /dev/null +++ b/Application/TodoItems/Models/TodoItemShortModel.cs @@ -0,0 +1,22 @@ +using Domain.Entities; + +namespace Application.TodoItems.Models; + +public class TodoItemShortModel +{ + public long Id { get; set; } + + public string Name { get; set; } + + public bool IsComplete { get; set; } + + public TodoItemShortModel() + {} + + public TodoItemShortModel(TodoItem todoItem) + { + Id = todoItem.Id; + Name = todoItem.Name; + IsComplete = todoItem.IsComplete; + } +} \ No newline at end of file diff --git a/Application/TodoItems/Models/UpdateTodoItemRequest.cs b/Application/TodoItems/Models/UpdateTodoItemRequest.cs new file mode 100644 index 00000000..db4d6deb --- /dev/null +++ b/Application/TodoItems/Models/UpdateTodoItemRequest.cs @@ -0,0 +1,10 @@ +namespace Application.TodoItems.Models; + +public class UpdateTodoItemRequest +{ + public long Id { get; set; } + + public string Name { get; set; } + + public bool IsComplete { get; set; } +} \ No newline at end of file diff --git a/Application/TodoItems/Queries/GetAllTodoItems.cs b/Application/TodoItems/Queries/GetAllTodoItems.cs new file mode 100644 index 00000000..f23be12e --- /dev/null +++ b/Application/TodoItems/Queries/GetAllTodoItems.cs @@ -0,0 +1,9 @@ +using Domain.Entities; +using MediatR; + +namespace Application.TodoItems.Queries; + +public class GetAllTodoItems : IRequest> +{ + +} \ No newline at end of file diff --git a/Application/TodoItems/Queries/GetTodoItemById.cs b/Application/TodoItems/Queries/GetTodoItemById.cs new file mode 100644 index 00000000..f87ef588 --- /dev/null +++ b/Application/TodoItems/Queries/GetTodoItemById.cs @@ -0,0 +1,17 @@ +using Domain.Entities; +using MediatR; + +namespace Application.TodoItems.Queries; + +public class GetTodoItemById : IRequest +{ + public long Id { get; set; } + + public GetTodoItemById() + {} + + public GetTodoItemById(long id) + { + Id = id; + } +} \ No newline at end of file diff --git a/Application/TodoItems/Queries/GetTodoItemsPaginated.cs b/Application/TodoItems/Queries/GetTodoItemsPaginated.cs new file mode 100644 index 00000000..765096bc --- /dev/null +++ b/Application/TodoItems/Queries/GetTodoItemsPaginated.cs @@ -0,0 +1,20 @@ +using Domain.Entities; +using MediatR; + +namespace Application.TodoItems.Queries; + +public class GetTodoItemsPaginated : IRequest> +{ + public int Page { get; set; } + + public int PageSize { get; set; } + + public GetTodoItemsPaginated() + {} + + public GetTodoItemsPaginated(int page, int pageSize) + { + Page = page; + PageSize = pageSize; + } +} \ No newline at end of file diff --git a/Application/TodoItems/QueryHandlers/GetAllTodoItemsHandler.cs b/Application/TodoItems/QueryHandlers/GetAllTodoItemsHandler.cs new file mode 100644 index 00000000..1256a397 --- /dev/null +++ b/Application/TodoItems/QueryHandlers/GetAllTodoItemsHandler.cs @@ -0,0 +1,21 @@ +using Application.Interfaces; +using Application.TodoItems.Queries; +using Domain.Entities; +using MediatR; + +namespace Application.TodoItems.QueryHandlers; + +public class GetAllTodoItemsHandler : IRequestHandler> +{ + private readonly ITodoItemsRepository _todoItemsRepository; + + public GetAllTodoItemsHandler(ITodoItemsRepository todoItemsRepository) + { + _todoItemsRepository = todoItemsRepository; + } + + public async Task> Handle(GetAllTodoItems request, CancellationToken cancellationToken) + { + return await _todoItemsRepository.GetAll(); + } +} \ No newline at end of file diff --git a/Application/TodoItems/QueryHandlers/GetTodoItemByIdHandler.cs b/Application/TodoItems/QueryHandlers/GetTodoItemByIdHandler.cs new file mode 100644 index 00000000..11d6ae64 --- /dev/null +++ b/Application/TodoItems/QueryHandlers/GetTodoItemByIdHandler.cs @@ -0,0 +1,21 @@ +using Application.Interfaces; +using Application.TodoItems.Queries; +using Domain.Entities; +using MediatR; + +namespace Application.TodoItems.QueryHandlers; + +public class GetTodoItemByIdHandler : IRequestHandler +{ + private readonly ITodoItemsRepository _todoItemsRepository; + + public GetTodoItemByIdHandler(ITodoItemsRepository todoItemsRepository) + { + _todoItemsRepository = todoItemsRepository; + } + + public async Task Handle(GetTodoItemById request, CancellationToken cancellationToken) + { + return await _todoItemsRepository.GetById(request.Id); + } +} \ No newline at end of file diff --git a/Application/TodoItems/QueryHandlers/GetTodoItemsPaginatedHandler.cs b/Application/TodoItems/QueryHandlers/GetTodoItemsPaginatedHandler.cs new file mode 100644 index 00000000..602f45ed --- /dev/null +++ b/Application/TodoItems/QueryHandlers/GetTodoItemsPaginatedHandler.cs @@ -0,0 +1,26 @@ +using Application.Interfaces; +using Application.TodoItems.Queries; +using Domain.Entities; +using MediatR; + +namespace Application.TodoItems.QueryHandlers; + +public class GetTodoItemsPaginatedHandler : IRequestHandler> +{ + private readonly ITodoItemsRepository _todoItemsRepository; + + public GetTodoItemsPaginatedHandler(ITodoItemsRepository todoItemsRepository) + { + _todoItemsRepository = todoItemsRepository; + } + + public async Task> Handle(GetTodoItemsPaginated request, CancellationToken cancellationToken) + { + if (request.Page < 1 || request.PageSize < 1) + { + throw new ArgumentException("Page and Page Size cannot be less than 1"); + } + + return await _todoItemsRepository.GetAllPaginated(request.Page, request.PageSize); + } +} \ No newline at end of file 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/Domain/Domain.csproj b/Domain/Domain.csproj new file mode 100644 index 00000000..4810a795 --- /dev/null +++ b/Domain/Domain.csproj @@ -0,0 +1,12 @@ + + + + net6.0 + enable + enable + + + + + + diff --git a/Domain/Entities/TodoItem.cs b/Domain/Entities/TodoItem.cs new file mode 100644 index 00000000..2a79ac56 --- /dev/null +++ b/Domain/Entities/TodoItem.cs @@ -0,0 +1,12 @@ +namespace Domain.Entities; + +public class TodoItem +{ + public long Id { get; set; } + + public string Name { get; set; } + + public bool IsComplete { get; set; } + + public string? Secret { get; set; } +} \ No newline at end of file diff --git a/Infrastructure/DependencyInjection.cs b/Infrastructure/DependencyInjection.cs new file mode 100644 index 00000000..ae3a1b15 --- /dev/null +++ b/Infrastructure/DependencyInjection.cs @@ -0,0 +1,11 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace Infrastructure; + +public static class DependencyInjection +{ + public static IServiceCollection AddInfrastructure(this IServiceCollection services) + { + return services; + } +} \ No newline at end of file diff --git a/Infrastructure/Infrastructure.csproj b/Infrastructure/Infrastructure.csproj new file mode 100644 index 00000000..a65da9cd --- /dev/null +++ b/Infrastructure/Infrastructure.csproj @@ -0,0 +1,22 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + + + + + + diff --git a/Infrastructure/Migrations/20230817192939_InitialMigration.Designer.cs b/Infrastructure/Migrations/20230817192939_InitialMigration.Designer.cs new file mode 100644 index 00000000..1b2eae7a --- /dev/null +++ b/Infrastructure/Migrations/20230817192939_InitialMigration.Designer.cs @@ -0,0 +1,53 @@ +// +using Infrastructure; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Infrastructure.Migrations +{ + [DbContext(typeof(TodoAppDbContext))] + [Migration("20230817192939_InitialMigration")] + partial class InitialMigration + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Domain.Entities.TodoItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("IsComplete") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Secret") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("TodoItems"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Infrastructure/Migrations/20230817192939_InitialMigration.cs b/Infrastructure/Migrations/20230817192939_InitialMigration.cs new file mode 100644 index 00000000..6c71919d --- /dev/null +++ b/Infrastructure/Migrations/20230817192939_InitialMigration.cs @@ -0,0 +1,36 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Infrastructure.Migrations +{ + /// + public partial class InitialMigration : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "TodoItems", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + Name = table.Column(type: "nvarchar(max)", nullable: false), + IsComplete = table.Column(type: "bit", nullable: false), + Secret = table.Column(type: "nvarchar(max)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_TodoItems", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "TodoItems"); + } + } +} diff --git a/Infrastructure/Migrations/20230818085851_ChangedSecretToNullable.Designer.cs b/Infrastructure/Migrations/20230818085851_ChangedSecretToNullable.Designer.cs new file mode 100644 index 00000000..00783372 --- /dev/null +++ b/Infrastructure/Migrations/20230818085851_ChangedSecretToNullable.Designer.cs @@ -0,0 +1,52 @@ +// +using Infrastructure; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Infrastructure.Migrations +{ + [DbContext(typeof(TodoAppDbContext))] + [Migration("20230818085851_ChangedSecretToNullable")] + partial class ChangedSecretToNullable + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Domain.Entities.TodoItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("IsComplete") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Secret") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("TodoItems"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Infrastructure/Migrations/20230818085851_ChangedSecretToNullable.cs b/Infrastructure/Migrations/20230818085851_ChangedSecretToNullable.cs new file mode 100644 index 00000000..c575d39f --- /dev/null +++ b/Infrastructure/Migrations/20230818085851_ChangedSecretToNullable.cs @@ -0,0 +1,36 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Infrastructure.Migrations +{ + /// + public partial class ChangedSecretToNullable : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Secret", + table: "TodoItems", + type: "nvarchar(max)", + nullable: true, + oldClrType: typeof(string), + oldType: "nvarchar(max)"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Secret", + table: "TodoItems", + type: "nvarchar(max)", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "nvarchar(max)", + oldNullable: true); + } + } +} diff --git a/Infrastructure/Migrations/TodoAppDbContextModelSnapshot.cs b/Infrastructure/Migrations/TodoAppDbContextModelSnapshot.cs new file mode 100644 index 00000000..02268507 --- /dev/null +++ b/Infrastructure/Migrations/TodoAppDbContextModelSnapshot.cs @@ -0,0 +1,49 @@ +// +using Infrastructure; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Infrastructure.Migrations +{ + [DbContext(typeof(TodoAppDbContext))] + partial class TodoAppDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.10") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Domain.Entities.TodoItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("IsComplete") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Secret") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("TodoItems"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Infrastructure/Repositories/TodoItemsRepository.cs b/Infrastructure/Repositories/TodoItemsRepository.cs new file mode 100644 index 00000000..a9963284 --- /dev/null +++ b/Infrastructure/Repositories/TodoItemsRepository.cs @@ -0,0 +1,108 @@ +using Application.Exceptions; +using Application.Interfaces; +using Domain.Entities; +using Microsoft.EntityFrameworkCore; + +namespace Infrastructure.Repositories; + +public class TodoItemsRepository : ITodoItemsRepository +{ + private readonly TodoAppDbContext _context; + + public TodoItemsRepository(TodoAppDbContext context) + { + _context = context; + } + + /// + /// Gets every TodoItem + /// + /// Collection of TodoItems + public async Task> GetAll() + { + return await _context.TodoItems.ToListAsync(); + } + + /// + /// Gets every TodoItem + /// + /// Page + /// Items per page + /// Collection of TodoItems + public async Task> GetAllPaginated(int page = 1, int pageSize = 10) + { + var skip = (page - 1) * pageSize; + return await _context.TodoItems + .OrderBy(x => x.Id) + .Skip(skip) + .Take(pageSize) + .ToListAsync(); + } + + /// + /// Gets a specific TodoItem by its ID + /// + /// TodoItem ID + /// TodoItem + public async Task GetById(long todoItemId) + { + return await _context.TodoItems.FindAsync(todoItemId) + ?? throw new EntityNotFoundException($"Todo Item with ID [{todoItemId}] does not exist."); + } + + /// + /// Creates a new TodoItem + /// + /// New TodoItem + /// Created TodoItem + public async Task CreateTodoItem(TodoItem todoItem) + { + await _context.TodoItems.AddAsync(todoItem); + await _context.SaveChangesAsync(); + + return todoItem; + } + + /// + /// Updates an existing TodoItem + /// + /// TodoItem ID + /// Updated TodoItem data + /// Updated TodoItem + public async Task UpdateTodoItem(long todoItemId, TodoItem todoItem) + { + var existingTodoItem = await _context.TodoItems.FindAsync(todoItemId) + ?? throw new EntityNotFoundException($"Todo Item with ID [{todoItemId}] does not exist."); + + existingTodoItem.Name = todoItem.Name; + existingTodoItem.IsComplete = todoItem.IsComplete; + + await _context.SaveChangesAsync(); + return existingTodoItem; + } + + public async Task UpdateTodoItemSecret(long todoItemId, string secret) + { + var existingTodoItem = await _context.TodoItems.FindAsync(todoItemId) + ?? throw new EntityNotFoundException($"Todo Item with ID [{todoItemId}] does not exist."); + + existingTodoItem.Secret = secret; + + await _context.SaveChangesAsync(); + return existingTodoItem; + } + + /// + /// Deletes an existing TodoItem by ID + /// + /// TodoItem ID + /// + public async Task DeleteTodoItem(long todoItemId) + { + var todoItem = await _context.TodoItems.FindAsync(todoItemId) + ?? throw new EntityNotFoundException($"Todo Item with ID [{todoItemId}] does not exist."); + + _context.TodoItems.Remove(todoItem); + await _context.SaveChangesAsync(); + } +} \ No newline at end of file diff --git a/Infrastructure/TodoAppDbContext.cs b/Infrastructure/TodoAppDbContext.cs new file mode 100644 index 00000000..dd44cafc --- /dev/null +++ b/Infrastructure/TodoAppDbContext.cs @@ -0,0 +1,12 @@ +using Domain.Entities; +using Microsoft.EntityFrameworkCore; + +namespace Infrastructure; + +public class TodoAppDbContext : DbContext +{ + public TodoAppDbContext(DbContextOptions options) : base(options) + {} + + public DbSet TodoItems { get; set; } +} \ 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/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/Tests/Mocks/MockTodoItemsRepository.cs b/Tests/Mocks/MockTodoItemsRepository.cs new file mode 100644 index 00000000..88d0b59c --- /dev/null +++ b/Tests/Mocks/MockTodoItemsRepository.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Application.Interfaces; +using Domain.Entities; +using Moq; + +namespace Tests.Mocks; + +internal class MockTodoItemsRepository +{ + public static Mock GetMock() + { + var mock = new Mock(); + IEnumerable todoItems = new List + { + new TodoItem + { + Id = 1, + Name = "First Todo", + IsComplete = false, + Secret = "Secret" + }, + new TodoItem + { + Id = 2, + Name = "Second Todo", + IsComplete = false, + Secret = "Secret" + }, + new TodoItem + { + Id = 3, + Name = "Third Todo", + IsComplete = false, + Secret = "Secret" + }, + }; + + mock.Setup(m => m.GetAll()) + .Returns(() => Task.FromResult(todoItems)); + + mock.Setup(m => m.GetAllPaginated(It.IsAny(), It.IsAny())) + .Returns((int page, int pageSize) => + Task.FromResult(todoItems.OrderBy(x => x.Id).Skip((page - 1) * pageSize).Take(pageSize))); + + mock.Setup(m => m.GetById(It.IsAny())) + .Returns((long id) => Task.FromResult(todoItems.First(x => x.Id == id))); + + mock.Setup(m => m.CreateTodoItem(It.IsAny())) + .Callback(() => { }); + + mock.Setup(m => m.UpdateTodoItem(It.IsAny(), It.IsAny())) + .Callback(() => { }); + + mock.Setup(m => m.DeleteTodoItem(It.IsAny())) + .Callback(() => { }); + + return mock; + } +} \ No newline at end of file diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj new file mode 100644 index 00000000..731ca908 --- /dev/null +++ b/Tests/Tests.csproj @@ -0,0 +1,29 @@ + + + + net6.0 + enable + + false + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + diff --git a/Tests/TodoItemsTests.cs b/Tests/TodoItemsTests.cs new file mode 100644 index 00000000..99648a85 --- /dev/null +++ b/Tests/TodoItemsTests.cs @@ -0,0 +1,65 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Application.TodoItems.Queries; +using Application.TodoItems.QueryHandlers; +using Domain.Entities; +using MediatR; +using Moq; +using Tests.Mocks; +using Xunit; + +namespace Tests; + +public class TodoItemsTests +{ + [Fact] + public async void WhenGettingAllTodoItems_ThenAllTodoItemsReturn() + { + var mockTodoItemsRepository = MockTodoItemsRepository.GetMock(); + + var query = new GetAllTodoItems(); + var handler = new GetAllTodoItemsHandler(mockTodoItemsRepository.Object); + + var result = await handler.Handle(query, new CancellationToken()); + + Assert.NotNull(result); + Assert.IsAssignableFrom>(result); + } + + [Fact] + public async void WhenGettingTodoItemById_ThenTodoItemWithMatchingIdReturns() + { + var mockTodoItemsRepository = MockTodoItemsRepository.GetMock(); + + var query = new GetTodoItemById(1); + var handler = new GetTodoItemByIdHandler(mockTodoItemsRepository.Object); + + var result = await handler.Handle(query, new CancellationToken()); + + Assert.NotNull(result); + Assert.IsAssignableFrom(result); + Assert.Equal(1, result.Id); + } + + [Fact] + public async void WhenGettingTodoItemsPaginated_ThenTodoItemsPaginatedReturn() + { + var mockTodoItemsRepository = MockTodoItemsRepository.GetMock(); + + var query = new GetTodoItemsPaginated(2, 2); + var handler = new GetTodoItemsPaginatedHandler(mockTodoItemsRepository.Object); + + var result = await handler.Handle(query, new CancellationToken()); + + Assert.NotNull(result); + Assert.IsAssignableFrom>(result); + + var todoItems = result.ToList(); + Assert.Single(todoItems.ToList()); + Assert.Equal(3, todoItems.First().Id); + } + + +} \ 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..ded5c8cc 100644 --- a/TodoApiDTO.sln +++ b/TodoApiDTO.sln @@ -3,7 +3,15 @@ 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}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Domain", "Domain\Domain.csproj", "{110A165C-E0E1-428E-84A6-52918C221F93}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Application", "Application\Application.csproj", "{5B4AC66F-59AC-4C4C-AF55-B7221FDE71AE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Infrastructure", "Infrastructure\Infrastructure.csproj", "{C5167AB8-38D4-4A40-99CC-641CA0197777}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApi", "WebApi\WebApi.csproj", "{C4B0BAC3-D4B4-42BF-B3D1-937FAB7B8C39}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests.csproj", "{6A29A3CC-BFF5-423C-98A4-B697D21BC12D}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -11,10 +19,26 @@ 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 + {110A165C-E0E1-428E-84A6-52918C221F93}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {110A165C-E0E1-428E-84A6-52918C221F93}.Debug|Any CPU.Build.0 = Debug|Any CPU + {110A165C-E0E1-428E-84A6-52918C221F93}.Release|Any CPU.ActiveCfg = Release|Any CPU + {110A165C-E0E1-428E-84A6-52918C221F93}.Release|Any CPU.Build.0 = Release|Any CPU + {5B4AC66F-59AC-4C4C-AF55-B7221FDE71AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5B4AC66F-59AC-4C4C-AF55-B7221FDE71AE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5B4AC66F-59AC-4C4C-AF55-B7221FDE71AE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5B4AC66F-59AC-4C4C-AF55-B7221FDE71AE}.Release|Any CPU.Build.0 = Release|Any CPU + {C5167AB8-38D4-4A40-99CC-641CA0197777}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C5167AB8-38D4-4A40-99CC-641CA0197777}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C5167AB8-38D4-4A40-99CC-641CA0197777}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C5167AB8-38D4-4A40-99CC-641CA0197777}.Release|Any CPU.Build.0 = Release|Any CPU + {C4B0BAC3-D4B4-42BF-B3D1-937FAB7B8C39}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C4B0BAC3-D4B4-42BF-B3D1-937FAB7B8C39}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C4B0BAC3-D4B4-42BF-B3D1-937FAB7B8C39}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C4B0BAC3-D4B4-42BF-B3D1-937FAB7B8C39}.Release|Any CPU.Build.0 = Release|Any CPU + {6A29A3CC-BFF5-423C-98A4-B697D21BC12D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6A29A3CC-BFF5-423C-98A4-B697D21BC12D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6A29A3CC-BFF5-423C-98A4-B697D21BC12D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6A29A3CC-BFF5-423C-98A4-B697D21BC12D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/WebApi/Controllers/TodoItemsController.cs b/WebApi/Controllers/TodoItemsController.cs new file mode 100644 index 00000000..655eec6c --- /dev/null +++ b/WebApi/Controllers/TodoItemsController.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using Application.TodoItems.Commands; +using Application.TodoItems.Models; +using Application.TodoItems.Queries; +using Domain.Entities; +using MediatR; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace WebApi.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class TodoItemsController : ControllerBase + { + private readonly IMediator _mediator; + + public TodoItemsController(IMediator mediator) + { + _mediator = mediator; + } + + [HttpGet] + public async Task>> GetAllTodoItems() + { + var query = new GetAllTodoItems(); + + var todoItems = await _mediator.Send(query); + return Ok(todoItems.Select(todoItem => new TodoItemShortModel(todoItem))); + } + + [HttpGet("paginated")] + public async Task>> GetTodoItemsPaginated([FromQuery] int page = 1, [FromQuery] int pageSize = 10) + { + var query = new GetTodoItemsPaginated(page, pageSize); + + var todoItems = await _mediator.Send(query); + return Ok(todoItems.Select(todoItem => new TodoItemShortModel(todoItem))); + } + + [HttpGet("{todoItemId}")] + public async Task> GetTodoItem(long todoItemId) + { + var query = new GetTodoItemById(todoItemId); + + var todoItem = await _mediator.Send(query); + return Ok(new TodoItemShortModel(todoItem)); + } + + [HttpPost] + public async Task> CreateTodoItem([FromBody] CreateTodoItemRequest request) + { + var command = new CreateTodoItem(request); + var todo = await _mediator.Send(command); + return Ok(new TodoItemShortModel(todo)); + } + + [HttpPut("{todoItemId}")] + public async Task UpdateTodoItem(long todoItemId, [FromBody] UpdateTodoItemRequest request) + { + if (todoItemId != request.Id) + { + throw new ArgumentException("Todo Item ID does not match with the updating object"); + } + + var command = new UpdateTodoItem(request); + await _mediator.Send(command); + + return NoContent(); + } + + [HttpDelete("{todoItemId}")] + public async Task DeleteTodoItem(long todoItemId) + { + var command = new DeleteTodoItem(todoItemId); + await _mediator.Send(command); + + return NoContent(); + } + } +} \ No newline at end of file diff --git a/WebApi/Middleware/ExceptionMiddleware.cs b/WebApi/Middleware/ExceptionMiddleware.cs new file mode 100644 index 00000000..e6a7e46d --- /dev/null +++ b/WebApi/Middleware/ExceptionMiddleware.cs @@ -0,0 +1,64 @@ +using System.Net; +using Application.Exceptions; +using Microsoft.AspNetCore.Diagnostics; +using Serilog; + +namespace WebApi.Middleware; + +public class ExceptionMiddleware +{ + private readonly RequestDelegate _next; + + public ExceptionMiddleware(RequestDelegate next) + { + _next = next; + } + + public async Task InvokeAsync(HttpContext httpContext) + { + try + { + await _next(httpContext); + } + catch (Exception ex) + { + Log.Error($"Something went wrong: {ex}"); + await HandleExceptionAsync(httpContext, ex); + } + } + private async Task HandleExceptionAsync(HttpContext context, Exception exception) + { + context.Response.ContentType = "application/json"; + + switch (exception) + { + case EntityNotFoundException e: + context.Response.StatusCode = (int)EntityNotFoundException.StatusCode; + await context.Response.WriteAsync(new ErrorDetails + { + StatusCode = context.Response.StatusCode, + Message = e.Message + }.ToString()); + break; + + case ArgumentException e: + context.Response.StatusCode = (int)HttpStatusCode.BadRequest; + await context.Response.WriteAsync(new ErrorDetails + { + StatusCode = context.Response.StatusCode, + Message = e.Message + }.ToString()); + break; + + default: + context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; + await context.Response.WriteAsync(new ErrorDetails + { + StatusCode = context.Response.StatusCode, + Message = "Internal Server Error" + }.ToString()); + + break; + } + } +} \ No newline at end of file diff --git a/WebApi/Middleware/ExceptionMiddlewareExtensions.cs b/WebApi/Middleware/ExceptionMiddlewareExtensions.cs new file mode 100644 index 00000000..47e791af --- /dev/null +++ b/WebApi/Middleware/ExceptionMiddlewareExtensions.cs @@ -0,0 +1,9 @@ +namespace WebApi.Middleware; + +public static class ExceptionMiddlewareExtensions +{ + public static void ConfigureExceptionMiddleware(this WebApplication app) + { + app.UseMiddleware(); + } +} \ No newline at end of file diff --git a/WebApi/Program.cs b/WebApi/Program.cs new file mode 100644 index 00000000..b9601373 --- /dev/null +++ b/WebApi/Program.cs @@ -0,0 +1,57 @@ +using System.Configuration; +using Application; +using Application.Interfaces; +using Infrastructure; +using Infrastructure.Repositories; +using Microsoft.EntityFrameworkCore; +using Serilog; +using Serilog.Exceptions; +using WebApi.Middleware; + +var builder = WebApplication.CreateBuilder(args); + +Log.Logger = new LoggerConfiguration() + .ReadFrom.Configuration(builder.Configuration) + .Enrich.FromLogContext() + .Enrich.WithExceptionDetails() + .Enrich.WithMachineName() + .CreateBootstrapLogger(); + +Log.Information("Starting up..."); + +// Setting up logger +builder.Host.UseSerilog(); + +// Adding database context +var connectionString = builder.Configuration.GetConnectionString("TodoAppDB"); +builder.Services.AddDbContext(opt => opt.UseSqlServer(connectionString)); + +// Adding traditional controllers +builder.Services.AddControllers(); + +// Adding Swagger documentation +builder.Services.AddSwaggerGen(); + +// Adding inner layers +builder.Services + .AddApplication() + .AddInfrastructure(); + +// Adding repositories +builder.Services.AddScoped(); + +var app = builder.Build(); +app.UseSerilogRequestLogging(); + +// Adding Global Exception Handler +app.ConfigureExceptionMiddleware(); + +// Adding Swagger only to Development Environment +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.MapControllers(); +app.Run(); \ No newline at end of file diff --git a/Properties/launchSettings.json b/WebApi/Properties/launchSettings.json similarity index 69% rename from Properties/launchSettings.json rename to WebApi/Properties/launchSettings.json index 6766196a..37eae6a0 100644 --- a/Properties/launchSettings.json +++ b/WebApi/Properties/launchSettings.json @@ -1,27 +1,28 @@ -{ +{ "iisSettings": { "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { - "applicationUrl": "http://localhost:56416/", - "sslPort": 44331 + "applicationUrl": "http://localhost:25408", + "sslPort": 44392 } }, "profiles": { - "IIS Express": { - "commandName": "IISExpress", + "WebApi": { + "commandName": "Project", + "dotnetRunMessages": true, "launchBrowser": true, + "applicationUrl": "https://localhost:7144;http://localhost:5223", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, - "TodoApiDTO": { - "commandName": "Project", + "IIS Express": { + "commandName": "IISExpress", "launchBrowser": true, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" - }, - "applicationUrl": "https://localhost:5001;http://localhost:5000" + } } } -} \ No newline at end of file +} diff --git a/WebApi/WebApi.csproj b/WebApi/WebApi.csproj new file mode 100644 index 00000000..4f9b830b --- /dev/null +++ b/WebApi/WebApi.csproj @@ -0,0 +1,37 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + diff --git a/WebApi/appsettings.Development.json b/WebApi/appsettings.Development.json new file mode 100644 index 00000000..19564ba0 --- /dev/null +++ b/WebApi/appsettings.Development.json @@ -0,0 +1,41 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "ConnectionStrings": { + "TodoAppDB": "Server=localhost,1433;Database=TodoAppDB;User=sa;Password=pass.123;TrustServerCertificate=True" + }, + "Serilog": { + "Using": [ + "Serilog.Sinks.Console", + "Serilog.Sinks.RollingFile" + ], + "MinimumLevel": { + "Default": "Debug", + "Override": { + "Microsoft": "Error", + "System": "Error", + "Microsoft.EntityFrameworkCore.Database.Command": "Error" + } + }, + "WriteTo": [ + { + "Name": "File", + "Args": { + "path": "../Logs/logs.txt", + "outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff}] [{Level}] {MachineName} ({ThreadId}) <{SourceContext}> {Message}{NewLine}{Exception} {NewLine}{NewLine}{NewLine}" + } + }, + { + "Name": "Console", + "Args": { + "theme": "Serilog.Sinks.SystemConsole.Themes.AnsiConsoleTheme::Code, Serilog.Sinks.Console", + "outputTemplate": "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff}] [{Level}] {MachineName} ({ThreadId}) <{SourceContext}> {Message}{NewLine}{Exception} {NewLine}{NewLine}{NewLine}" + } + } + ] + } +} \ No newline at end of file diff --git a/appsettings.json b/WebApi/appsettings.json similarity index 54% rename from appsettings.json rename to WebApi/appsettings.json index d9d9a9bf..ec04bc12 100644 --- a/appsettings.json +++ b/WebApi/appsettings.json @@ -2,9 +2,8 @@ "Logging": { "LogLevel": { "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" + "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*" -} +} \ No newline at end of file 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" - } - } -}