Skip to content

Commit 0646c4e

Browse files
committed
switch to fluent validation
1 parent 7b9c9d6 commit 0646c4e

File tree

6 files changed

+70
-39
lines changed

6 files changed

+70
-39
lines changed

JixMinApi/Features/Todo/Commands/CreateTodoCommand.cs

+25-7
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,22 @@
1-
using MediatR;
1+
using FluentValidation;
2+
using FluentValidation.Results;
3+
using MediatR;
24

35
namespace JixMinApi.Features.Todo.Commands;
46

5-
public record CreateTodoCommand(CreateTodoDto input) : IRequest<Result<TodoDto>>;
7+
public record CreateTodoCommand(string Name, bool IsComplete) : IRequest<Result<TodoDto>>;
8+
9+
public sealed class CreateTodoCommandValidator
10+
: AbstractValidator<CreateTodoCommand>
11+
{
12+
public CreateTodoCommandValidator()
13+
{
14+
RuleFor(command => command.Name)
15+
.NotEmpty()
16+
.MinimumLength(4)
17+
.MaximumLength(24);
18+
}
19+
}
620

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

1428
public async Task<Result<TodoDto>> Handle(CreateTodoCommand request, CancellationToken cancellationToken)
1529
{
16-
// simple inline validation, if needed validate using behaviors https://github.com/jbogard/MediatR/wiki/Behaviors
17-
if (string.IsNullOrEmpty(request.input.Name))
30+
// fluent validation inside handler, if needed validate on pipeline using behaviors https://github.com/jbogard/MediatR/wiki/Behaviors
31+
var validator = new CreateTodoCommandValidator();
32+
ValidationResult validationResult = validator.Validate(request);
33+
34+
if (!validationResult.IsValid)
1835
{
19-
return new Result<TodoDto>([new KeyValuePair<string, string[]>("Name", ["Must not be empty."])]);
36+
var errors = validationResult.Errors.Select(e => new KeyValuePair<string, string>(e.PropertyName, e.ErrorMessage));
37+
return new Result<TodoDto>(errors);
2038
}
2139

2240
var todo = new Todo()
2341
{
24-
Name = request.input.Name,
25-
IsComplete = request.input.IsComplete,
42+
Name = request.Name,
43+
IsComplete = request.IsComplete,
2644
DateCreated = DateTimeOffset.UtcNow,
2745
};
2846

JixMinApi/Features/Todo/Result.cs

+24-19
Original file line numberDiff line numberDiff line change
@@ -3,39 +3,44 @@
33

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

1610
public Result(T value)
1711
{
1812
Value = value;
1913
}
2014

21-
public Result(IEnumerable<KeyValuePair<string, string[]>> validationErrors)
15+
public Result(IEnumerable<KeyValuePair<string, string>> validationErrors)
2216
{
23-
ValidationErrors = validationErrors.ToList();
24-
HasValidationError = true;
17+
Errors = validationErrors.ToList();
2518
}
2619

2720
public Result(string field, string validationErrorMessage)
2821
{
29-
List<KeyValuePair<string, string[]>> validationErrors
30-
= [new KeyValuePair<string, string[]>(field, [validationErrorMessage])];
31-
ValidationErrors = validationErrors;
32-
HasValidationError = true;
22+
Errors = [new(field, validationErrorMessage)];
3323
}
24+
}
3425

35-
public Result(Exception exception)
26+
public static class ResultExtensions
27+
{
28+
public static IDictionary<string, string[]> ToErrorDictionary(this IEnumerable<KeyValuePair<string, string>> errors)
3629
{
37-
Exception = exception;
38-
IsError = true;
30+
Dictionary<string, string[]> result = [];
31+
32+
foreach (var e in errors)
33+
{
34+
if (!result.TryGetValue(e.Key, out var messages))
35+
{
36+
result[e.Key] = [e.Value];
37+
continue;
38+
}
39+
40+
var newArray = messages.Concat([e.Value]).ToArray();
41+
result[e.Key] = newArray;
42+
}
43+
44+
return result;
3945
}
4046
}
41-

JixMinApi/Features/Todo/TodoEndpoints.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -100,11 +100,11 @@ public static async Task<Results<ValidationProblem, NotFound, Ok<TodoDto>>> GetT
100100
/// <response code="400">Invalid payload</response>
101101
public static async Task<Results<Created<TodoDto>, ValidationProblem>> CreateTodoAsync(CreateTodoDto input, IMediator mediator)
102102
{
103-
var result = await mediator.Send(new CreateTodoCommand(input));
103+
var result = await mediator.Send(new CreateTodoCommand(input.Name, input.IsComplete));
104104

105-
if (result.HasValidationError)
105+
if (!result.IsSuccess)
106106
{
107-
return TypedResults.ValidationProblem(result.ValidationErrors.ToDictionary());
107+
return TypedResults.ValidationProblem(result.Errors.ToErrorDictionary());
108108
}
109109

110110
var todo = result.Value;

JixMinApi/JixMinApi.csproj

+2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
</PropertyGroup>
1212

1313
<ItemGroup>
14+
<PackageReference Include="FluentValidation" Version="11.9.0" />
15+
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.9.0" />
1416
<PackageReference Include="MediatR" Version="12.2.0" />
1517
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.3" />
1618
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.3" />

JixMinApi/Program.cs

+12-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
using FluentValidation;
12
using JixMinApi.Features.Todo;
23
using JixMinApi.Shared;
4+
using Microsoft.AspNetCore.Identity;
35
using Microsoft.OpenApi.Models;
46
using System.Reflection;
57

@@ -21,14 +23,14 @@
2123
options.IncludeXmlComments(xmlPath);
2224
});
2325

26+
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
27+
builder.Services.AddProblemDetails();
28+
29+
builder.Services.AddValidatorsFromAssembly(typeof(Program).Assembly);
2430
builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(Program).Assembly));
2531

26-
// Inject endpoint services
2732
builder.Services.AddTodoEndpointServices();
2833

29-
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
30-
builder.Services.AddProblemDetails();
31-
3234
var app = builder.Build();
3335

3436
// Configure the HTTP request pipeline.
@@ -38,8 +40,12 @@
3840
app.UseSwaggerUI();
3941
}
4042

41-
app.UseHttpsRedirection();
42-
app.UseExceptionHandler();
43+
if (!app.Environment.IsDevelopment())
44+
{
45+
app.UseHttpsRedirection();
46+
app.UseExceptionHandler();
47+
}
48+
4349
app.UseTodoEndpoints();
4450

4551
app.Run();

JixMinApiTests/Features/Todo/Commands/CreateTodoCommandHandlerTests.cs

+4-4
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,12 @@ public void CreateTodoCommandHandlerTest()
2525
public async void HandleTest()
2626
{
2727
Setup();
28-
var input = new CreateTodoDto("Test", true);
29-
var result = await sut.Handle(new CreateTodoCommand(input), default);
28+
const string todoName = "test";
29+
var result = await sut.Handle(new CreateTodoCommand(todoName, true), default);
3030

3131
Assert.NotNull(result);
3232
Assert.True(result.IsSuccess);
33-
Assert.Equal(input.Name, result.Value.Name);
34-
Assert.Equal(input.IsComplete, result.Value.IsComplete);
33+
Assert.Equal(todoName, result.Value.Name);
34+
Assert.True(result.Value.IsComplete);
3535
}
3636
}

0 commit comments

Comments
 (0)