Skip to content

Commit

Permalink
Add payload and parameter validations
Browse files Browse the repository at this point in the history
  • Loading branch information
jicking committed Apr 18, 2024
1 parent f58ff43 commit 114011d
Show file tree
Hide file tree
Showing 4 changed files with 72 additions and 43 deletions.
29 changes: 12 additions & 17 deletions JixMinApi/Features/Todo/Commands/CreateTodoCommand.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using JixMinApi.Shared;
using MediatR;
using MediatR;

namespace JixMinApi.Features.Todo.Commands;

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

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

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

try
{
await _db.Todos.AddAsync(todo);
await _db.SaveChangesAsync();
await _db.Todos.AddAsync(todo);
await _db.SaveChangesAsync();

_logger.LogInformation($"Todo {todo.Id} is successfully created");
// publish mediatr notification
// await _mediator.Publish(new TodoCreatedNotification(todo), cancellationToken);
}
catch (Exception ex)
{
_logger.LogError($"Todo {todo.Id} failed due to an error: {ex.Message}",
todo.Id, ex.Message);
return new Result<TodoDto>(ex);
}
_logger.LogInformation($"Todo {todo.Id} is successfully created");

// publish mediatr notification https://github.com/jbogard/MediatR/wiki#notifications
// await _mediator.Publish(new TodoCreatedNotification(todo), cancellationToken);

return new Result<TodoDto>(todo.ToDto());
}
Expand Down
36 changes: 36 additions & 0 deletions JixMinApi/Features/Todo/Result.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
namespace JixMinApi.Features.Todo;


public class Result<T>
{
public bool IsSuccess { get; init; }
public T? Value { get; init; }

public bool HasValidationError { get; init; }
public IReadOnlyList<KeyValuePair<string, string[]>> ValidationErrors { get; init; } = [];

//public bool IsError { get; init; }
//public Exception? Exception { get; init; }


public Result(T value)
{
Value = value;
IsSuccess = true;
}

public Result(IEnumerable<KeyValuePair<string, string[]>> validationErrors)
{
ValidationErrors = validationErrors.ToList();
HasValidationError = true;
}

public Result(string field, string validationErrorMessage)
{
List<KeyValuePair<string, string[]>> validationErrors
= [new KeyValuePair<string, string[]>(field, [validationErrorMessage])];
ValidationErrors = validationErrors;
HasValidationError = true;
}
}

25 changes: 11 additions & 14 deletions JixMinApi/Features/Todo/TodoEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,13 @@ public static void MapTodoEndpoints(this WebApplication app)

group.MapGet("/{id}", GetTodoByIdAsync)
.Produces<TodoDto>(StatusCodes.Status200OK)
.Produces<ValidationErrorDto>(StatusCodes.Status400BadRequest)
.Produces<HttpValidationProblemDetails>(StatusCodes.Status400BadRequest)
.Produces(StatusCodes.Status404NotFound);

group.MapPost("/", CreateTodoAsync)
.Accepts<TodoCreateDto>(MediaTypeNames.Application.Json)
.Produces<TodoDto>(StatusCodes.Status201Created)
.Produces<ValidationErrorDto>(StatusCodes.Status400BadRequest);
.Produces<HttpValidationProblemDetails>(StatusCodes.Status400BadRequest);

//group.MapDelete("/{id}", GetAllTodosAsync)
// .Produces(StatusCodes.Status204NoContent)
Expand All @@ -61,14 +61,15 @@ public static async Task<Ok<TodoDto[]>> GetAllTodosAsync(IMediator mediator)
/// <summary>
/// Fetches a todo by Id
/// </summary>
public static async Task<Results<BadRequest<ValidationErrorDto>, NotFound, Ok<TodoDto>>> GetTodoByIdAsync(Guid id, IMediator mediator)
public static async Task<Results<ValidationProblem, NotFound, Ok<TodoDto>>> GetTodoByIdAsync(Guid id, IMediator mediator)
{
if (id == Guid.Empty)
{
return TypedResults.BadRequest<ValidationErrorDto>(
new(
ValidationErrors: [new ValidationErrorItem("id", "id must not be an empty guid.")])
);
var errors = new Dictionary<string, string[]>
{
["id"] = ["id parameter must not be an empty guid."],
};
return TypedResults.ValidationProblem(errors);
}

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

if (result.HasValidationError && result.ValidationErrors.Any())
if (result.HasValidationError)
{
return TypedResults.BadRequest();
return TypedResults.ValidationProblem(result.ValidationErrors.ToDictionary());
}

var todo = result.Value;
Expand Down
25 changes: 13 additions & 12 deletions JixMinApiTests/Features/Todo/TodoEndpointsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,24 @@ public async Task GetTodoByIdAsync_Returns_BadRequest_When_Id_Is_Empty()
// Arrange
var mediatorMock = new Mock<IMediator>();
var emptyId = Guid.Empty;
var expectedBadRequest = new ValidationErrorDto(
[new ValidationErrorItem("id", "id must not be an empty guid.")]
);
var expectedErrors = new Dictionary<string, string[]>
{
["id"] = ["id parameter must not be an empty guid."],
};

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

// Assert
Assert.IsType<Results<BadRequest<ValidationErrorDto>, NotFound, Ok<TodoDto>>>(response);
Assert.IsType<Results<ValidationProblem, NotFound, Ok<TodoDto>>>(response);

var result = (BadRequest<ValidationErrorDto>)response.Result;
var result = (ValidationProblem)response.Result;
Assert.NotNull(result);
Assert.Equal(StatusCodes.Status400BadRequest, result.StatusCode);

var value = Assert.IsType<ValidationErrorDto>(result.Value);
Assert.True(value.ValidationErrors.Any());
Assert.Equal(expectedBadRequest.ValidationErrors.FirstOrDefault(), value.ValidationErrors.FirstOrDefault());
var value = Assert.IsType<HttpValidationProblemDetails>(result.ProblemDetails);
Assert.True(value.Errors.Any());
Assert.Equal(expectedErrors.FirstOrDefault(), value.Errors.FirstOrDefault());
}

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

// Assert
Assert.IsType<Results<BadRequest<ValidationErrorDto>, NotFound, Ok<TodoDto>>>(response);
Assert.IsType<Results<ValidationProblem, NotFound, Ok<TodoDto>>>(response);

var result = (NotFound)response.Result;
Assert.NotNull(result);
Assert.Equal(StatusCodes.Status404NotFound, result.StatusCode);
Expand All @@ -72,8 +73,8 @@ public async Task GetTodoByIdAsync_Returns_Ok_When_Todo_Found()
var response = await TodoEndpoints.GetTodoByIdAsync(existingId, mediatorMock.Object);

// Assert
Assert.IsType<Results<BadRequest<ValidationErrorDto>, NotFound, Ok<TodoDto>>>(response);
Assert.IsType<Results<ValidationProblem, NotFound, Ok<TodoDto>>>(response);

var result = (Ok<TodoDto>)response.Result;
Assert.NotNull(result);
Assert.Equal(StatusCodes.Status200OK, result.StatusCode);
Expand Down

0 comments on commit 114011d

Please sign in to comment.