Skip to content

Commit

Permalink
switch to fluent validation
Browse files Browse the repository at this point in the history
  • Loading branch information
jicking committed Apr 18, 2024
1 parent 7b9c9d6 commit 7298ec8
Show file tree
Hide file tree
Showing 6 changed files with 86 additions and 40 deletions.
32 changes: 25 additions & 7 deletions JixMinApi/Features/Todo/Commands/CreateTodoCommand.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,22 @@
using MediatR;
using FluentValidation;
using FluentValidation.Results;
using MediatR;

namespace JixMinApi.Features.Todo.Commands;

public record CreateTodoCommand(CreateTodoDto input) : IRequest<Result<TodoDto>>;
public record CreateTodoCommand(string Name, bool IsComplete) : IRequest<Result<TodoDto>>;

public sealed class CreateTodoCommandValidator
: AbstractValidator<CreateTodoCommand>
{
public CreateTodoCommandValidator()
{
RuleFor(command => command.Name)
.NotEmpty()
.MinimumLength(4)
.MaximumLength(24);
}
}

public class CreateTodoCommandHandler : IRequestHandler<CreateTodoCommand, Result<TodoDto>>
{
Expand All @@ -13,16 +27,20 @@ public class CreateTodoCommandHandler : IRequestHandler<CreateTodoCommand, Resul

public async Task<Result<TodoDto>> Handle(CreateTodoCommand request, CancellationToken cancellationToken)
{
// simple inline validation, if needed validate using behaviors https://github.com/jbogard/MediatR/wiki/Behaviors
if (string.IsNullOrEmpty(request.input.Name))
// fluent validation inside handler, if needed validate on pipeline using behaviors https://github.com/jbogard/MediatR/wiki/Behaviors
var validator = new CreateTodoCommandValidator();
ValidationResult validationResult = validator.Validate(request);

if (!validationResult.IsValid)
{
return new Result<TodoDto>([new KeyValuePair<string, string[]>("Name", ["Must not be empty."])]);
var errors = validationResult.Errors.Select(e => new KeyValuePair<string, string>(e.PropertyName, e.ErrorMessage));
return new Result<TodoDto>(errors);
}

var todo = new Todo()
{
Name = request.input.Name,
IsComplete = request.input.IsComplete,
Name = request.Name,
IsComplete = request.IsComplete,
DateCreated = DateTimeOffset.UtcNow,
};

Expand Down
43 changes: 24 additions & 19 deletions JixMinApi/Features/Todo/Result.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,39 +3,44 @@

public class Result<T>
{
public bool IsSuccess => (!HasValidationError && !IsError);
public bool IsSuccess => !Errors.Any();
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 IReadOnlyList<KeyValuePair<string, string>> Errors { get; init; } = [];

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

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

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

public Result(Exception exception)
public static class ResultExtensions
{
public static IDictionary<string, string[]> ToErrorDictionary(this IEnumerable<KeyValuePair<string, string>> errors)
{
Exception = exception;
IsError = true;
Dictionary<string, string[]> result = [];

foreach (var e in errors)
{
if (!result.TryGetValue(e.Key, out var messages))
{
result[e.Key] = [e.Value];
continue;
}

var newArray = messages.Concat([e.Value]).ToArray();
result[e.Key] = newArray;
}

return result;
}
}

6 changes: 3 additions & 3 deletions JixMinApi/Features/Todo/TodoEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,11 +100,11 @@ public static async Task<Results<ValidationProblem, NotFound, Ok<TodoDto>>> GetT
/// <response code="400">Invalid payload</response>
public static async Task<Results<Created<TodoDto>, ValidationProblem>> CreateTodoAsync(CreateTodoDto input, IMediator mediator)
{
var result = await mediator.Send(new CreateTodoCommand(input));
var result = await mediator.Send(new CreateTodoCommand(input.Name, input.IsComplete));

if (result.HasValidationError)
if (!result.IsSuccess)
{
return TypedResults.ValidationProblem(result.ValidationErrors.ToDictionary());
return TypedResults.ValidationProblem(result.Errors.ToErrorDictionary());
}

var todo = result.Value;
Expand Down
2 changes: 2 additions & 0 deletions JixMinApi/JixMinApi.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="FluentValidation" Version="11.9.0" />
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.9.0" />
<PackageReference Include="MediatR" Version="12.2.0" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.3" />
Expand Down
18 changes: 12 additions & 6 deletions JixMinApi/Program.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using FluentValidation;
using JixMinApi.Features.Todo;
using JixMinApi.Shared;
using Microsoft.AspNetCore.Identity;
using Microsoft.OpenApi.Models;
using System.Reflection;

Expand All @@ -21,14 +23,14 @@
options.IncludeXmlComments(xmlPath);
});

builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
builder.Services.AddProblemDetails();

builder.Services.AddValidatorsFromAssembly(typeof(Program).Assembly);
builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(Program).Assembly));

// Inject endpoint services
builder.Services.AddTodoEndpointServices();

builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
builder.Services.AddProblemDetails();

var app = builder.Build();

// Configure the HTTP request pipeline.
Expand All @@ -38,8 +40,12 @@
app.UseSwaggerUI();
}

app.UseHttpsRedirection();
app.UseExceptionHandler();
if (!app.Environment.IsDevelopment())
{
app.UseHttpsRedirection();
app.UseExceptionHandler();
}

app.UseTodoEndpoints();

app.Run();
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,30 @@ public void CreateTodoCommandHandlerTest()
}

[Fact()]
public async void HandleTest()
public async void Handle_OkTest()
{
Setup();
var input = new CreateTodoDto("Test", true);
var result = await sut.Handle(new CreateTodoCommand(input), default);
const string todoName = "Test";

var result = await sut.Handle(new CreateTodoCommand(todoName, true), default);

Assert.NotNull(result);
Assert.True(result.IsSuccess);
Assert.Equal(input.Name, result.Value.Name);
Assert.Equal(input.IsComplete, result.Value.IsComplete);
Assert.Equal(todoName, result.Value.Name);
Assert.True(result.Value.IsComplete);
}

[Theory()]
[InlineData("")]
[InlineData("TES")]
public async void Handle_ReturnsFailureWhenNameIsNotValid(string name)
{
Setup();

var result = await sut.Handle(new CreateTodoCommand(name, true), default);

Assert.NotNull(result);
Assert.False(result.IsSuccess);
Assert.True(result.Errors.Any());
}
}

0 comments on commit 7298ec8

Please sign in to comment.