Skip to content

Commit 40d0a3c

Browse files
committed
Add mediatr handlers with tests
1 parent 8d456ba commit 40d0a3c

15 files changed

+237
-24
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
using JixMinApi.Shared;
2+
using MediatR;
3+
4+
namespace JixMinApi.Features.Todo.Commands;
5+
6+
public record CreateTodoCommand(TodoCreateDto input) : IRequest<Result<TodoDto>>;
7+
8+
public class CreateTodoCommandHandler : IRequestHandler<CreateTodoCommand, Result<TodoDto>>
9+
{
10+
private readonly TodoDb _db;
11+
private readonly ILogger<CreateTodoCommandHandler> _logger;
12+
13+
public CreateTodoCommandHandler(TodoDb db, ILogger<CreateTodoCommandHandler> logger) => (_db, _logger) = (db, logger);
14+
15+
public async Task<Result<TodoDto>> Handle(CreateTodoCommand request, CancellationToken cancellationToken)
16+
{
17+
var todo = new Todo() { Name = request.input.Name, IsComplete = request.input.IsComplete };
18+
19+
try
20+
{
21+
await _db.Todos.AddAsync(todo);
22+
await _db.SaveChangesAsync();
23+
24+
_logger.LogInformation("Todo {Id} is successfully created", todo.Id);
25+
// await _emailService.SendEmail(email);
26+
}
27+
catch (Exception ex)
28+
{
29+
_logger.LogError("Todo {Id} failed due to an error: {ExMessage}",
30+
todo.Id, ex.Message);
31+
return new Result<TodoDto>(ex);
32+
}
33+
34+
return new Result<TodoDto>(todo.ToDto());
35+
}
36+
}

JixMinApi/Features/Todo/Extensions.cs

+21
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,25 @@ public static void MapTodoApi(this WebApplication app)
1616
group.MapGet("/", TodoApi.GetAllTodos);
1717
group.MapPost("/", TodoApi.CreateTodo);
1818
}
19+
20+
public static TodoDto ToDto(this Todo model)
21+
{
22+
return new TodoDto(model.Id, model.Name, model.IsComplete);
23+
}
24+
25+
public static List<TodoDto> ToDto(this IList<Todo> model)
26+
{
27+
var result = new List<TodoDto>();
28+
foreach (var item in model)
29+
result.Add(item.ToDto());
30+
return result;
31+
}
32+
public static void SeedTestData(this TodoDb db)
33+
{
34+
if (db.Todos.Any())
35+
return;
36+
37+
db.Todos.AddRange(TodoDbDefaultValues.TestTodos);
38+
db.SaveChanges();
39+
}
1940
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
using MediatR;
2+
using Microsoft.EntityFrameworkCore;
3+
4+
namespace JixMinApi.Features.Todo.Queries;
5+
6+
public record GetAllTodosQuery : IRequest<List<TodoDto>>;
7+
8+
public class GetAllTodosQueryHandler : IRequestHandler<GetAllTodosQuery, List<TodoDto>>
9+
{
10+
private readonly TodoDb _db;
11+
private readonly ILogger<GetAllTodosQueryHandler> _logger;
12+
13+
public GetAllTodosQueryHandler(TodoDb db, ILogger<GetAllTodosQueryHandler> logger) => (_db, _logger) = (db, logger);
14+
15+
public async Task<List<TodoDto>> Handle(GetAllTodosQuery request, CancellationToken cancellationToken)
16+
{
17+
_logger.LogDebug($"Start query");
18+
var data = await _db.Todos
19+
.AsNoTracking()
20+
.ToListAsync();
21+
_logger.LogDebug($"End query");
22+
return data.ToDto();
23+
}
24+
}

JixMinApi/Features/Todo/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Classes are stored in single file for simplicity and will be extracted to individual file as needed.

JixMinApi/Features/Todo/TodoApi.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ public static async Task<Ok<Todo[]>> GetAllTodos(TodoDb db)
1313

1414
public static async Task<Created<Todo>> CreateTodo(TodoCreateDto input, TodoDb db)
1515
{
16-
var todo = new Todo() { Name= input.Name, IsComplete= input.IsComplete };
16+
var todo = new Todo() { Name = input.Name, IsComplete = input.IsComplete };
1717

1818
db.Todos.Add(todo);
1919
await db.SaveChangesAsync();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
namespace JixMinApi.Features.Todo;
2+
3+
public static class TodoDbDefaultValues
4+
{
5+
public static Guid TestTodoId = new Guid("684d921c-755a-450c-91d7-af495e116d9b");
6+
7+
public static List<Todo> TestTodos = new List<Todo> {
8+
new Todo() {
9+
Id = Guid.NewGuid(),
10+
Name = "Completed Todo",
11+
IsComplete = true,
12+
},
13+
new Todo() {
14+
Id = TestTodoId,
15+
Name = "Test Todo",
16+
}
17+
};
18+
}

JixMinApi/JixMinApi.csproj

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk.Web">
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
22

33
<PropertyGroup>
44
<TargetFramework>net8.0</TargetFramework>
@@ -9,6 +9,7 @@
99
</PropertyGroup>
1010

1111
<ItemGroup>
12+
<PackageReference Include="MediatR" Version="12.2.0" />
1213
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.3" />
1314
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.3" />
1415
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.3" />

JixMinApi/Program.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
using JixMinApi.Features.Todo;
2-
using Microsoft.EntityFrameworkCore;
32

43
var builder = WebApplication.CreateBuilder(args);
54

65
// Add services to the container.
76
builder.Services.AddEndpointsApiExplorer();
87
builder.Services.AddSwaggerGen();
8+
builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(Program).Assembly));
99

1010
// Api dependencies
1111
builder.Services.InjectTodoApiDependencies();

JixMinApi/Shared/Result.cs

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
namespace JixMinApi.Shared;
2+
3+
4+
public class Result<T>
5+
{
6+
public bool IsSuccess { get; set; }
7+
public T? Value { get; set; }
8+
9+
public bool IsError { get; set; }
10+
public Exception? Exception { get; set; }
11+
12+
public bool HasValidationError { get; set; }
13+
public List<KeyValuePair<string, string>> ValidationErrors { get; set; } = new List<KeyValuePair<string, string>>();
14+
15+
16+
public Result(T value)
17+
{
18+
Value = value;
19+
IsSuccess = true;
20+
}
21+
22+
public Result(Exception exception)
23+
{
24+
Exception = exception;
25+
IsError = true;
26+
}
27+
28+
public Result(List<KeyValuePair<string, string>> validationErrors)
29+
{
30+
ValidationErrors = validationErrors;
31+
HasValidationError = true;
32+
}
33+
34+
public Result(string field, string validationErrorMessage)
35+
{
36+
var validationErrors = new List<KeyValuePair<string, string>>();
37+
validationErrors.Add(new KeyValuePair<string, string>(field, validationErrorMessage));
38+
ValidationErrors = validationErrors;
39+
HasValidationError = true;
40+
}
41+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
using JixMinApiTests.Features.Todo;
2+
using Xunit;
3+
using Assert = Microsoft.VisualStudio.TestTools.UnitTesting.Assert;
4+
5+
namespace JixMinApi.Features.Todo.Commands.Tests;
6+
7+
public class CreateTodoCommandHandlerTests
8+
{
9+
private CreateTodoCommandHandler sut;
10+
11+
internal void Setup()
12+
{
13+
var (db, logger) = TestSetup.CreateTestMocks<CreateTodoCommandHandler>();
14+
sut = new CreateTodoCommandHandler(db, logger);
15+
}
16+
17+
[Fact()]
18+
public void CreateTodoCommandHandlerTest()
19+
{
20+
Setup();
21+
Assert.IsNotNull(sut);
22+
}
23+
24+
[Fact()]
25+
public async void HandleTest()
26+
{
27+
Setup();
28+
var input = new TodoCreateDto("Test", true);
29+
var result = await sut.Handle(new CreateTodoCommand(input), default);
30+
31+
Xunit.Assert.NotNull(result);
32+
Assert.IsTrue(result.IsSuccess);
33+
Assert.AreEqual(input.Name, result.Value.Name);
34+
Assert.AreEqual(input.IsComplete, result.Value.IsComplete);
35+
}
36+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
using JixMinApiTests.Features.Todo;
2+
using Xunit;
3+
using Assert = Microsoft.VisualStudio.TestTools.UnitTesting.Assert;
4+
5+
namespace JixMinApi.Features.Todo.Queries.Tests;
6+
7+
public class GetAllTodosQueryHandlerTests
8+
{
9+
private GetAllTodosQueryHandler sut;
10+
11+
internal void Setup()
12+
{
13+
var (db, logger) = TestSetup.CreateTestMocks<GetAllTodosQueryHandler>();
14+
sut = new GetAllTodosQueryHandler(db, logger);
15+
}
16+
17+
[Fact()]
18+
public void GetAllTodosQueryHandlerTest()
19+
{
20+
Setup();
21+
Assert.IsNotNull(sut);
22+
}
23+
24+
[Fact()]
25+
public async void HandleTest()
26+
{
27+
Setup();
28+
var result = await sut.Handle(new GetAllTodosQuery(), default);
29+
Xunit.Assert.NotEmpty(result);
30+
}
31+
}
+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using JixMinApi.Features.Todo;
2+
using Microsoft.EntityFrameworkCore;
3+
using Microsoft.Extensions.Logging;
4+
using Microsoft.Extensions.Logging.Abstractions;
5+
6+
namespace JixMinApiTests.Features.Todo;
7+
8+
public static class TestSetup
9+
{
10+
public static (TodoDb, ILogger<T>) CreateTestMocks<T>()
11+
{
12+
var logger = NullLogger<T>.Instance;
13+
14+
// Set EF
15+
var options = new DbContextOptionsBuilder<TodoDb>()
16+
.UseInMemoryDatabase(Guid.NewGuid().ToString())
17+
.Options;
18+
var db = new TodoDb(options);
19+
db.SeedTestData();
20+
21+
return (db, logger);
22+
}
23+
}

JixMinApiTests/Features/Todo/TodoApiTests.cs

-19
This file was deleted.

JixMinApiTests/JixMinApiTests.csproj

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<PropertyGroup>
44
<TargetFramework>net8.0</TargetFramework>
55
<ImplicitUsings>enable</ImplicitUsings>
6-
<Nullable>enable</Nullable>
6+
<Nullable>disable</Nullable>
77

88
<IsPackable>false</IsPackable>
99
<IsTestProject>true</IsTestProject>

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ A simple minimal API Demo that uses vertical slice architecture.
55
- [x] Setup base feature structure
66
- [x] add github action for CI
77
- [ ] pass unit tests
8-
- [ ] add mediatr
8+
- [x] add mediatr
99
- [ ] add app insights/logging
1010
- [ ] set data migration process
1111
- [ ] set release process

0 commit comments

Comments
 (0)