Skip to content

Commit 9c5f469

Browse files
authored
Merge pull request #81 from cnblogs/68-support-multiple-validation-errors
feat!: support multiple validation errors
2 parents fc2701d + 0779e34 commit 9c5f469

File tree

13 files changed

+92
-42
lines changed

13 files changed

+92
-42
lines changed

src/Cnblogs.Architecture.Ddd.Cqrs.Abstractions/Cnblogs.Architecture.Ddd.Cqrs.Abstractions.csproj

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,13 @@
1111
</PropertyGroup>
1212

1313
<ItemGroup>
14-
<ProjectReference Include="..\Cnblogs.Architecture.Ddd.Infrastructure.Abstractions\Cnblogs.Architecture.Ddd.Infrastructure.Abstractions.csproj"/>
14+
<ProjectReference Include="..\Cnblogs.Architecture.Ddd.Infrastructure.Abstractions\Cnblogs.Architecture.Ddd.Infrastructure.Abstractions.csproj" />
1515
</ItemGroup>
1616

1717
<ItemGroup>
18-
<PackageReference Include="Mapster" Version="7.3.0"/>
19-
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.0"/>
20-
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="7.0.0"/>
18+
<PackageReference Include="Mapster" Version="7.3.0" />
19+
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.0" />
20+
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="7.0.0" />
2121
</ItemGroup>
2222

2323
</Project>

src/Cnblogs.Architecture.Ddd.Cqrs.Abstractions/CommandResponse.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public abstract record CommandResponse : IValidationResponse, ILockableResponse
2626
public string ErrorMessage { get; init; } = string.Empty;
2727

2828
/// <inheritdoc />
29-
public ValidationError? ValidationError { get; init; }
29+
public ValidationErrors ValidationErrors { get; init; } = new();
3030

3131
/// <inheritdoc />
3232
public bool LockAcquired { get; set; }

src/Cnblogs.Architecture.Ddd.Cqrs.Abstractions/IValidatable.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
public interface IValidatable
77
{
88
/// <summary>
9-
/// Validate the object, return <see cref="ValidationError"/> if fails or <code>null</code> if passed.
9+
/// Validate the object, validate will pass if <paramref name="validationErrors"/> is empty.
1010
/// </summary>
11-
ValidationError? Validate();
12-
}
11+
/// <param name="validationErrors">The validation error collection.</param>
12+
void Validate(ValidationErrors validationErrors);
13+
}

src/Cnblogs.Architecture.Ddd.Cqrs.Abstractions/IValidationResponse.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public interface IValidationResponse
1616
string ErrorMessage { get; init; }
1717

1818
/// <summary>
19-
/// The validation results, null if validation was passed.
19+
/// The validation results, empty if validation was passed.
2020
/// </summary>
21-
ValidationError? ValidationError { get; init; }
21+
ValidationErrors ValidationErrors { get; init; }
2222
}

src/Cnblogs.Architecture.Ddd.Cqrs.Abstractions/ValidationBehavior.cs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,9 @@ public async Task<TResponse> Handle(
3131
CancellationToken cancellationToken)
3232
{
3333
_logger.LogInformation("----- Validating request {RequestType}", request.GetType().Name);
34-
var error = request.Validate();
35-
if (error is null)
34+
var errors = new ValidationErrors();
35+
request.Validate(errors);
36+
if (errors.Count == 0)
3637
{
3738
return await next();
3839
}
@@ -41,13 +42,13 @@ public async Task<TResponse> Handle(
4142
"----- Validation failed with error, type: {RequestType}, Request: {Request}, Message: {Message}",
4243
request.GetType().Name,
4344
request,
44-
error.Message);
45+
errors.First().Message);
4546

4647
return new TResponse
4748
{
4849
IsValidationError = true,
49-
ErrorMessage = error.Message,
50-
ValidationError = error
50+
ErrorMessage = errors.First().Message,
51+
ValidationErrors = errors
5152
};
5253
}
5354
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
using System.Collections;
2+
3+
namespace Cnblogs.Architecture.Ddd.Cqrs.Abstractions;
4+
5+
/// <summary>
6+
/// Collection of <see cref="ValidationError"/>.
7+
/// </summary>
8+
public class ValidationErrors : ICollection<ValidationError>
9+
{
10+
private readonly List<ValidationError> _validationErrors = new();
11+
12+
/// <summary>
13+
/// Add a new validation error.
14+
/// </summary>
15+
/// <param name="validationError">The validation error.</param>
16+
public void Add(ValidationError validationError)
17+
{
18+
_validationErrors.Add(validationError);
19+
}
20+
21+
/// <summary>
22+
/// Clear all validation errors.
23+
/// </summary>
24+
public void Clear()
25+
{
26+
_validationErrors.Clear();
27+
}
28+
29+
/// <inheritdoc />
30+
public bool Contains(ValidationError item) => _validationErrors.Contains(item);
31+
32+
/// <inheritdoc />
33+
public void CopyTo(ValidationError[] array, int arrayIndex) => _validationErrors.CopyTo(array, arrayIndex);
34+
35+
/// <inheritdoc />
36+
public bool Remove(ValidationError item) => _validationErrors.Remove(item);
37+
38+
/// <inheritdoc />
39+
public int Count => _validationErrors.Count;
40+
41+
/// <inheritdoc />
42+
public bool IsReadOnly => false;
43+
44+
/// <inheritdoc />
45+
public IEnumerator<ValidationError> GetEnumerator() => _validationErrors.GetEnumerator();
46+
47+
/// <inheritdoc />
48+
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
49+
}

src/Cnblogs.Architecture.Ddd.Cqrs.AspNetCore/ApiControllerBase.cs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,9 +95,11 @@ private IActionResult MapErrorCommandResponseToProblemDetails<TError>(CommandRes
9595
{
9696
if (response.IsValidationError)
9797
{
98-
ModelState.AddModelError(
99-
response.ValidationError!.ParameterName ?? "command",
100-
response.ValidationError!.Message);
98+
foreach (var (message, parameterName) in response.ValidationErrors)
99+
{
100+
ModelState.AddModelError(parameterName ?? "command", message);
101+
}
102+
101103
return ValidationProblem();
102104
}
103105

@@ -118,7 +120,7 @@ private IActionResult MapErrorCommandResponseToPlainText<TError>(CommandResponse
118120
{
119121
if (response.IsValidationError)
120122
{
121-
return BadRequest(response.ValidationError!.Message);
123+
return BadRequest(string.Join('\n', response.ValidationErrors.Select(x => x.Message)));
122124
}
123125

124126
if (response is { IsConcurrentError: true, LockAcquired: false })

src/Cnblogs.Architecture.Ddd.Cqrs.AspNetCore/CommandEndpointHandler.cs

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ private static IResult HandleErrorCommandResponseWithPlainText(CommandResponse r
7878
{
7979
if (response.IsValidationError)
8080
{
81-
return Results.Text(response.ValidationError!.Message, statusCode: 400);
81+
return Results.Text(string.Join('\n', response.ValidationErrors.Select(x => x.Message)), statusCode: 400);
8282
}
8383

8484
if (response is { IsConcurrentError: true, LockAcquired: false })
@@ -93,12 +93,9 @@ private static IResult HandleErrorCommandResponseWithProblemDetails(CommandRespo
9393
{
9494
if (response.IsValidationError)
9595
{
96-
var errors = new Dictionary<string, string[]>
97-
{
98-
{
99-
response.ValidationError!.ParameterName ?? "command", new[] { response.ValidationError!.Message }
100-
}
101-
};
96+
var errors = response.ValidationErrors
97+
.GroupBy(x => x.ParameterName ?? "command")
98+
.ToDictionary(x => x.Key, x => x.Select(y => y.Message).ToArray());
10299
return Results.ValidationProblem(errors, statusCode: 400);
103100
}
104101

src/Cnblogs.Architecture.Ddd.Infrastructure.Dapper.Clickhouse/Cnblogs.Architecture.Ddd.Infrastructure.Dapper.Clickhouse.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
</ItemGroup>
1414

1515
<ItemGroup>
16-
<PackageReference Include="ClickHouse.Client" Version="6.5.0" />
16+
<PackageReference Include="ClickHouse.Client" Version="6.5.1" />
1717
</ItemGroup>
1818

1919
</Project>

test/Cnblogs.Architecture.IntegrationTestProject/Application/Commands/UpdateCommand.cs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,11 @@ public record UpdateCommand(
1111
: ICommand<TestError>, IValidatable
1212
{
1313
/// <inheritdoc />
14-
public ValidationError? Validate()
14+
public void Validate(ValidationErrors errors)
1515
{
1616
if (NeedValidationError)
1717
{
18-
return new ValidationError("need validation error", nameof(NeedValidationError));
18+
errors.Add(new ValidationError("need validation error", nameof(NeedValidationError)));
1919
}
20-
21-
return null;
2220
}
2321
}

test/Cnblogs.Architecture.UnitTests/Cqrs/Behaviors/ValidationBehaviorTests.cs

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
using Cnblogs.Architecture.Ddd.Cqrs.Abstractions;
22
using Cnblogs.Architecture.UnitTests.Cqrs.FakeObjects;
3-
43
using FluentAssertions;
5-
64
using Microsoft.Extensions.Logging.Abstractions;
75

86
namespace Cnblogs.Architecture.UnitTests.Cqrs.Behaviors;
@@ -13,16 +11,17 @@ public class ValidationBehaviorTests
1311
public async Task ValidationBehavior_ValidationFailed_ReturnObjectAsync()
1412
{
1513
// Arrange
16-
var request = new FakeQuery<FakeResponse>(() => new ValidationError("failed", "parameter"));
14+
var error = new ValidationError("failed", "parameter");
15+
var request = new FakeQuery<FakeResponse>(() => error);
1716
var behavior = new ValidationBehavior<FakeQuery<FakeResponse>, FakeResponse>(
1817
NullLogger<ValidationBehavior<FakeQuery<FakeResponse>, FakeResponse>>.Instance);
1918

2019
// Act
2120
var result = await behavior.Handle(request, () => Task.FromResult(new FakeResponse()), default);
2221

2322
// Assert
24-
result.Should().BeEquivalentTo(
25-
new { IsValidationError = true, ValidationError = new ValidationError("failed", "parameter") });
23+
var errors = new ValidationErrors { error };
24+
result.Should().BeEquivalentTo(new { IsValidationError = true, ValidationErrors = errors });
2625
}
2726

2827
[Fact]
@@ -37,7 +36,6 @@ public async Task ValidationBehavior_ValidationSuccess_ReturnNextAsync()
3736
var result = await behavior.Handle(request, () => Task.FromResult(new FakeResponse()), default);
3837

3938
// Assert
40-
result.Should().BeEquivalentTo(
41-
new { IsValidationError = false, ValidationError = (ValidationError?)null });
39+
result.Should().BeEquivalentTo(new { IsValidationError = false, ValidationErrors = new ValidationErrors() });
4240
}
43-
}
41+
}

test/Cnblogs.Architecture.UnitTests/Cqrs/FakeObjects/FakeQuery.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,12 @@ public FakeQuery(string? cacheGroupKey, string cacheKey)
5555
}
5656

5757
/// <inheritdoc />
58-
public ValidationError? Validate()
58+
public void Validate(ValidationErrors validationErrors)
5959
{
60-
return ValidateFunction.Invoke();
60+
var error = ValidateFunction.Invoke();
61+
if (error is not null)
62+
{
63+
validationErrors.Add(error);
64+
}
6165
}
6266
}

test/Cnblogs.Architecture.UnitTests/Cqrs/FakeObjects/FakeResponse.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,5 @@ public class FakeResponse : IValidationResponse
1111
public string ErrorMessage { get; init; } = string.Empty;
1212

1313
/// <inheritdoc />
14-
public ValidationError? ValidationError { get; init; }
14+
public ValidationErrors ValidationErrors { get; init; } = new();
1515
}

0 commit comments

Comments
 (0)