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"
- }
- }
-}