Skip to content

Commit 114011d

Browse files
committed
Add payload and parameter validations
1 parent f58ff43 commit 114011d

File tree

4 files changed

+72
-43
lines changed

4 files changed

+72
-43
lines changed

JixMinApi/Features/Todo/Commands/CreateTodoCommand.cs

+12-17
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
using JixMinApi.Shared;
2-
using MediatR;
1+
using MediatR;
32

43
namespace JixMinApi.Features.Todo.Commands;
54

@@ -14,7 +13,11 @@ public class CreateTodoCommandHandler : IRequestHandler<CreateTodoCommand, Resul
1413

1514
public async Task<Result<TodoDto>> Handle(CreateTodoCommand request, CancellationToken cancellationToken)
1615
{
17-
// add validation then set Result.ValidationErrors
16+
// simple inline validation, if needed validate using behaviors https://github.com/jbogard/MediatR/wiki/Behaviors
17+
if (string.IsNullOrEmpty(request.input.Name))
18+
{
19+
return new Result<TodoDto>([new KeyValuePair<string, string[]>("Name", ["Must not be empty."])]);
20+
}
1821

1922
var todo = new Todo()
2023
{
@@ -23,21 +26,13 @@ public async Task<Result<TodoDto>> Handle(CreateTodoCommand request, Cancellatio
2326
DateCreated = DateTimeOffset.UtcNow,
2427
};
2528

26-
try
27-
{
28-
await _db.Todos.AddAsync(todo);
29-
await _db.SaveChangesAsync();
29+
await _db.Todos.AddAsync(todo);
30+
await _db.SaveChangesAsync();
3031

31-
_logger.LogInformation($"Todo {todo.Id} is successfully created");
32-
// publish mediatr notification
33-
// await _mediator.Publish(new TodoCreatedNotification(todo), cancellationToken);
34-
}
35-
catch (Exception ex)
36-
{
37-
_logger.LogError($"Todo {todo.Id} failed due to an error: {ex.Message}",
38-
todo.Id, ex.Message);
39-
return new Result<TodoDto>(ex);
40-
}
32+
_logger.LogInformation($"Todo {todo.Id} is successfully created");
33+
34+
// publish mediatr notification https://github.com/jbogard/MediatR/wiki#notifications
35+
// await _mediator.Publish(new TodoCreatedNotification(todo), cancellationToken);
4136

4237
return new Result<TodoDto>(todo.ToDto());
4338
}

JixMinApi/Features/Todo/Result.cs

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
namespace JixMinApi.Features.Todo;
2+
3+
4+
public class Result<T>
5+
{
6+
public bool IsSuccess { get; init; }
7+
public T? Value { get; init; }
8+
9+
public bool HasValidationError { get; init; }
10+
public IReadOnlyList<KeyValuePair<string, string[]>> ValidationErrors { get; init; } = [];
11+
12+
//public bool IsError { get; init; }
13+
//public Exception? Exception { get; init; }
14+
15+
16+
public Result(T value)
17+
{
18+
Value = value;
19+
IsSuccess = true;
20+
}
21+
22+
public Result(IEnumerable<KeyValuePair<string, string[]>> validationErrors)
23+
{
24+
ValidationErrors = validationErrors.ToList();
25+
HasValidationError = true;
26+
}
27+
28+
public Result(string field, string validationErrorMessage)
29+
{
30+
List<KeyValuePair<string, string[]>> validationErrors
31+
= [new KeyValuePair<string, string[]>(field, [validationErrorMessage])];
32+
ValidationErrors = validationErrors;
33+
HasValidationError = true;
34+
}
35+
}
36+

JixMinApi/Features/Todo/TodoEndpoints.cs

+11-14
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,13 @@ public static void MapTodoEndpoints(this WebApplication app)
3939

4040
group.MapGet("/{id}", GetTodoByIdAsync)
4141
.Produces<TodoDto>(StatusCodes.Status200OK)
42-
.Produces<ValidationErrorDto>(StatusCodes.Status400BadRequest)
42+
.Produces<HttpValidationProblemDetails>(StatusCodes.Status400BadRequest)
4343
.Produces(StatusCodes.Status404NotFound);
4444

4545
group.MapPost("/", CreateTodoAsync)
4646
.Accepts<TodoCreateDto>(MediaTypeNames.Application.Json)
4747
.Produces<TodoDto>(StatusCodes.Status201Created)
48-
.Produces<ValidationErrorDto>(StatusCodes.Status400BadRequest);
48+
.Produces<HttpValidationProblemDetails>(StatusCodes.Status400BadRequest);
4949

5050
//group.MapDelete("/{id}", GetAllTodosAsync)
5151
// .Produces(StatusCodes.Status204NoContent)
@@ -61,14 +61,15 @@ public static async Task<Ok<TodoDto[]>> GetAllTodosAsync(IMediator mediator)
6161
/// <summary>
6262
/// Fetches a todo by Id
6363
/// </summary>
64-
public static async Task<Results<BadRequest<ValidationErrorDto>, NotFound, Ok<TodoDto>>> GetTodoByIdAsync(Guid id, IMediator mediator)
64+
public static async Task<Results<ValidationProblem, NotFound, Ok<TodoDto>>> GetTodoByIdAsync(Guid id, IMediator mediator)
6565
{
6666
if (id == Guid.Empty)
6767
{
68-
return TypedResults.BadRequest<ValidationErrorDto>(
69-
new(
70-
ValidationErrors: [new ValidationErrorItem("id", "id must not be an empty guid.")])
71-
);
68+
var errors = new Dictionary<string, string[]>
69+
{
70+
["id"] = ["id parameter must not be an empty guid."],
71+
};
72+
return TypedResults.ValidationProblem(errors);
7273
}
7374

7475
var todos = await mediator.Send(new GetAllTodosQuery());
@@ -97,17 +98,13 @@ public static async Task<Results<BadRequest<ValidationErrorDto>, NotFound, Ok<To
9798
/// </remarks>
9899
/// <response code="201">Returns the newly created item</response>
99100
/// <response code="400">Invalid payload</response>
100-
public static async Task<Results<Created<TodoDto>, BadRequest>> CreateTodoAsync(TodoCreateDto input, IMediator mediator)
101+
public static async Task<Results<Created<TodoDto>, ValidationProblem>> CreateTodoAsync(TodoCreateDto input, IMediator mediator)
101102
{
102103
var result = await mediator.Send(new CreateTodoCommand(input));
103-
if (result.IsError && result.Exception is not null)
104-
{
105-
throw result.Exception;
106-
}
107104

108-
if (result.HasValidationError && result.ValidationErrors.Any())
105+
if (result.HasValidationError)
109106
{
110-
return TypedResults.BadRequest();
107+
return TypedResults.ValidationProblem(result.ValidationErrors.ToDictionary());
111108
}
112109

113110
var todo = result.Value;

JixMinApiTests/Features/Todo/TodoEndpointsTests.cs

+13-12
Original file line numberDiff line numberDiff line change
@@ -16,23 +16,24 @@ public async Task GetTodoByIdAsync_Returns_BadRequest_When_Id_Is_Empty()
1616
// Arrange
1717
var mediatorMock = new Mock<IMediator>();
1818
var emptyId = Guid.Empty;
19-
var expectedBadRequest = new ValidationErrorDto(
20-
[new ValidationErrorItem("id", "id must not be an empty guid.")]
21-
);
19+
var expectedErrors = new Dictionary<string, string[]>
20+
{
21+
["id"] = ["id parameter must not be an empty guid."],
22+
};
2223

2324
// Act
2425
var response = await TodoEndpoints.GetTodoByIdAsync(emptyId, mediatorMock.Object);
2526

2627
// Assert
27-
Assert.IsType<Results<BadRequest<ValidationErrorDto>, NotFound, Ok<TodoDto>>>(response);
28+
Assert.IsType<Results<ValidationProblem, NotFound, Ok<TodoDto>>>(response);
2829

29-
var result = (BadRequest<ValidationErrorDto>)response.Result;
30+
var result = (ValidationProblem)response.Result;
3031
Assert.NotNull(result);
3132
Assert.Equal(StatusCodes.Status400BadRequest, result.StatusCode);
3233

33-
var value = Assert.IsType<ValidationErrorDto>(result.Value);
34-
Assert.True(value.ValidationErrors.Any());
35-
Assert.Equal(expectedBadRequest.ValidationErrors.FirstOrDefault(), value.ValidationErrors.FirstOrDefault());
34+
var value = Assert.IsType<HttpValidationProblemDetails>(result.ProblemDetails);
35+
Assert.True(value.Errors.Any());
36+
Assert.Equal(expectedErrors.FirstOrDefault(), value.Errors.FirstOrDefault());
3637
}
3738

3839
[Fact]
@@ -48,8 +49,8 @@ public async Task GetTodoByIdAsync_Returns_NotFound_When_Todo_Not_Found()
4849
var response = await TodoEndpoints.GetTodoByIdAsync(nonExistentId, mediatorMock.Object);
4950

5051
// Assert
51-
Assert.IsType<Results<BadRequest<ValidationErrorDto>, NotFound, Ok<TodoDto>>>(response);
52-
52+
Assert.IsType<Results<ValidationProblem, NotFound, Ok<TodoDto>>>(response);
53+
5354
var result = (NotFound)response.Result;
5455
Assert.NotNull(result);
5556
Assert.Equal(StatusCodes.Status404NotFound, result.StatusCode);
@@ -72,8 +73,8 @@ public async Task GetTodoByIdAsync_Returns_Ok_When_Todo_Found()
7273
var response = await TodoEndpoints.GetTodoByIdAsync(existingId, mediatorMock.Object);
7374

7475
// Assert
75-
Assert.IsType<Results<BadRequest<ValidationErrorDto>, NotFound, Ok<TodoDto>>>(response);
76-
76+
Assert.IsType<Results<ValidationProblem, NotFound, Ok<TodoDto>>>(response);
77+
7778
var result = (Ok<TodoDto>)response.Result;
7879
Assert.NotNull(result);
7980
Assert.Equal(StatusCodes.Status200OK, result.StatusCode);

0 commit comments

Comments
 (0)