Skip to content

Commit 2d76873

Browse files
committed
feat: Mudança no Result Pattern
Após uma análise as mensagens não de validação de campos não ficavam claras do que qual propriedade era realmente o Error, então agora foi feita uma alteração para voltar a proprieadade Details e ajustar outras, ficando mais claro a resposta da API para erros de validação
1 parent e797300 commit 2d76873

File tree

10 files changed

+111
-83
lines changed

10 files changed

+111
-83
lines changed

src/Riber.Api/Common/Api/GlobalExceptionHandler.cs

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,20 +19,19 @@ public async ValueTask<bool> TryHandleAsync(
1919
{
2020
logger.LogError(exception, "Exception occurred: {ExceptionType} - {Message}",
2121
exception.GetType().Name, exception.Message);
22-
23-
(int statusCode, string[] messages) = exception switch
24-
{
25-
RequestTimeoutException timeoutEx => (timeoutEx.Code, [timeoutEx.Message]),
26-
ValidationException validationEx => (ValidationException.Code, validationEx.Messages.Select(e => e.ErrorMessage).ToArray()),
27-
Layer.ApplicationException applicationEx => (applicationEx.Code, [applicationEx.Message]),
28-
DomainException domainEx => (StatusCodes.Status422UnprocessableEntity, [domainEx.Message]),
29-
_ => (StatusCodes.Status500InternalServerError, ["Erro inesperado no servidor!"])
30-
};
31-
32-
await httpContext.WriteUnauthorizedResponse(
33-
code: statusCode,
34-
messages: messages
35-
);
22+
23+
(int statusCode, string message, Dictionary<string, string[]> details) = MapExceptionToError(exception);
24+
await httpContext.WriteErrorResponse(statusCode, message, details);
3625
return true;
3726
}
27+
28+
private static (int StatusCode, string Message, Dictionary<string, string[]> Details) MapExceptionToError(Exception exception)
29+
=> exception switch
30+
{
31+
RequestTimeoutException timeoutEx => (timeoutEx.Code, timeoutEx.Message, []),
32+
ValidationException validationEx => (StatusCodes.Status400BadRequest, string.Empty, validationEx.Details),
33+
Layer.ApplicationException applicationEx => (applicationEx.Code, applicationEx.Message, []),
34+
DomainException domainEx => (StatusCodes.Status422UnprocessableEntity, domainEx.Message, []),
35+
_ => (StatusCodes.Status500InternalServerError, "Erro inesperado no servidor.", [])
36+
};
3837
}

src/Riber.Api/Extensions/HttpContextExtension.cs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,18 @@ public static class HttpContextExtension
1313
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
1414
};
1515

16-
public static async Task WriteUnauthorizedResponse(
16+
public static async Task WriteErrorResponse(
1717
this HttpContext context,
1818
int code,
19-
params string[] messages)
19+
string message,
20+
Dictionary<string, string[]> details)
2021
{
21-
var jsonResponse = JsonSerializer.Serialize(
22-
Result.Failure<object>(new Error(GetErrorCode(code), messages)), JsonOptions);
22+
var errorType = GetErrorCode(code);
23+
var response = details.Count == 0
24+
? Result.Failure<object>(new Error(errorType, message))
25+
: Result.Failure<object>(new Error(errorType, details));
2326

27+
var jsonResponse = JsonSerializer.Serialize(response, JsonOptions);
2428
context.Response.StatusCode = code;
2529
context.Response.ContentType = "application/json";
2630

src/Riber.Api/Middlewares/SecurityStampMiddleware.cs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,10 @@ public async Task InvokeAsync(HttpContext context, RequestDelegate next)
2323
if (string.IsNullOrWhiteSpace(userId) || string.IsNullOrWhiteSpace(tokenSecurityStamp))
2424
{
2525
logger.LogError("Valores: userId = {UserId}, securityStamp = {TokenSecurityStamp}", userId, tokenSecurityStamp);
26-
await context.WriteUnauthorizedResponse(
26+
await context.WriteErrorResponse(
2727
StatusCodes.Status401Unauthorized,
28-
"Token do usuário inválido ou mal formado."
28+
"Token do usuário inválido ou mal formado.",
29+
details: []
2930
);
3031
return;
3132
}
@@ -34,9 +35,10 @@ await context.WriteUnauthorizedResponse(
3435
var user = await authService.FindByIdAsync(userId);
3536
if (user is null || user.SecurityStamp != tokenSecurityStamp)
3637
{
37-
await context.WriteUnauthorizedResponse(
38+
await context.WriteErrorResponse(
3839
code: StatusCodes.Status401Unauthorized,
39-
messages: "Security stamp inválido ou usuário não encontrado."
40+
message: "Security stamp inválido ou usuário não encontrado.",
41+
details: []
4042
);
4143
return;
4244
}

src/Riber.Application/Behaviors/ValidationBehavior.cs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
using FluentValidation;
22
using Mediator;
3-
using Riber.Application.Common;
4-
using Exceptions_ValidationException = Riber.Application.Exceptions.ValidationException;
3+
using LayerValidationException = Riber.Application.Exceptions.ValidationException;
54

65
namespace Riber.Application.Behaviors;
76

@@ -22,11 +21,14 @@ public async ValueTask<TResponse> Handle(
2221
.Select(x => x.Validate(context))
2322
.Where(x => !x.IsValid)
2423
.SelectMany(x => x.Errors)
25-
.Select(x => new ValidationError(x.PropertyName, x.ErrorMessage))
26-
.ToList();
24+
.GroupBy(x => x.PropertyName)
25+
.ToDictionary(
26+
g => g.Key,
27+
g => g.Select(x => x.ErrorMessage).ToArray()
28+
);
2729

2830
return validationErrors.Count > 0
29-
? throw new Exceptions_ValidationException(validationErrors)
31+
? throw new LayerValidationException(validationErrors)
3032
: await next(message, cancellationToken);
3133
}
3234
}

src/Riber.Application/Common/Result.cs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,17 @@ protected Result(bool isSuccess, Error error)
2727

2828
#region Static Methods
2929

30-
public static Result<object> Success() => new(null, true, new Error());
31-
public static Result<T> Success<T>(T value) => new(value, true, new Error());
32-
public static Result<T> Failure<T>(Error error) => new(default, false, error);
30+
public static Result<object> Success()
31+
=> new(null, true, new Error());
32+
33+
public static Result<T> Success<T>(T value)
34+
=> new(value, true, new Error());
35+
36+
public static Result<T> Failure<T>(Error error)
37+
=> new(default, false, error);
38+
39+
public static Result<T> Failure<T>(string type, Dictionary<string, string[]> details)
40+
=> new(default, false, new Error(type, details));
3341

3442
protected static Result<T> Create<T>(T? value) =>
3543
value is not null ? Success(value) : Failure<T>(new Error());

src/Riber.Application/Common/ValidationError.cs

Lines changed: 0 additions & 3 deletions
This file was deleted.

src/Riber.Application/Exceptions/ValidationException.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@
33

44
namespace Riber.Application.Exceptions;
55

6-
public sealed class ValidationException(IEnumerable<ValidationError> messages) : Exception
6+
public sealed class ValidationException(Dictionary<string, string[]> details) : Exception
77
{
8-
public IEnumerable<ValidationError> Messages => messages;
9-
public static int Code => (int)HttpStatusCode.BadRequest;
8+
public Dictionary<string, string[]> Details => details;
109
}

src/Riber.Domain/Abstractions/Error.cs

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,29 @@ public sealed class Error
99
{
1010
#region Properties
1111

12-
public string Code { get; init; } = string.Empty;
13-
public string[] Messages { get; init; } = [];
12+
public string Type { get; init; } = string.Empty;
13+
public string Message { get; init; } = string.Empty;
14+
public Dictionary<string, string[]> Details { get; set; } = [];
1415

1516
#endregion
1617

1718
#region Constructors
1819

1920
[JsonConstructor]
20-
public Error() {}
21-
22-
public Error(string code, params string[] messages)
21+
public Error() { }
22+
23+
public Error(string type, string message)
2324
{
24-
Code = code;
25-
Messages = messages;
25+
Type = type;
26+
Message = message;
2627
}
27-
28+
29+
public Error(string type, Dictionary<string, string[]> details)
30+
{
31+
Type = type;
32+
Message = "Dados Inválidos.";
33+
Details = details;
34+
}
35+
2836
#endregion
2937
}

tests/Riber.Api.Tests/Controllers/AuthControllerTests.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,9 @@ public async Task Should_NotLoginSuccessfully_WithInvalidCredentials()
4848
loginResponse.Should().NotBeNull();
4949
loginResponse.IsSuccess.Should().BeFalse();
5050
loginResponse.Value.Should().BeNull();
51-
loginResponse.Error.Code.Should().Be("NOT_FOUND");
52-
loginResponse.Error.Messages.Should().HaveCountGreaterThan(0);
51+
loginResponse.Error.Type.Should().Be("NOT_FOUND");
52+
loginResponse.Error.Message.Should().NotBeNullOrEmpty();
53+
loginResponse.Error.Details.Should().BeNullOrEmpty();
5354
}
5455

5556
#endregion

tests/Riber.Application.Tests/Behaviors/ValidationBehaviorTests.cs

Lines changed: 45 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public async Task Handle_WhenNoValidatorsProvided_ShouldProceedToNextHandler()
4242
);
4343

4444
// Assert
45-
result.Should().Be(result);
45+
result.Should().Be(_response);
4646
}
4747

4848
[Trait("Category", "Unit")]
@@ -65,7 +65,7 @@ public async Task Handle_WhenValidationPasses_ShouldProceedToNextHandler()
6565
);
6666

6767
// Assert
68-
result.Should().Be(result);
68+
result.Should().Be(_response);
6969
_mockValidator.Verify(x => x.Validate(
7070
It.IsAny<ValidationContext<RequestTest>>()), Times.Once);
7171
}
@@ -96,16 +96,16 @@ public async Task Handle_WhenSingleValidatorFails_ShouldThrowValidationException
9696
);
9797

9898
// Assert
99-
await exception.Should().ThrowExactlyAsync<ApplicationLayer.ValidationException>();
100-
10199
var thrownException = await exception.Should().ThrowExactlyAsync<ApplicationLayer.ValidationException>();
102-
thrownException.Which.Messages.Should().HaveCount(2);
103-
thrownException.Which.Messages.Should()
104-
.Contain(e => e.PropertyName == "Name" && e.ErrorMessage == "Name is required");
105-
thrownException.Which.Messages.Should()
106-
.Contain(e => e.PropertyName == "Age" && e.ErrorMessage == "Age must be between 20 and 50");
107-
108-
_mockValidator.Verify(x => x.Validate(It.IsAny<ValidationContext<RequestTest>>()), Times.Exactly(2));
100+
101+
thrownException.Which.Details.Should().NotBeNull();
102+
thrownException.Which.Details.Should().HaveCount(2);
103+
thrownException.Which.Details.Should().ContainKey("Name");
104+
thrownException.Which.Details.Should().ContainKey("Age");
105+
thrownException.Which.Details["Name"].Should().Equal("Name is required");
106+
thrownException.Which.Details["Age"].Should().Equal("Age must be between 20 and 50");
107+
108+
_mockValidator.Verify(x => x.Validate(It.IsAny<ValidationContext<RequestTest>>()), Times.Once);
109109
}
110110

111111
[Trait("Category", "Unit")]
@@ -138,19 +138,23 @@ public async Task Handle_WhenMultipleValidatorsFail_ShouldThrowValidationExcepti
138138
);
139139

140140
// Assert
141-
await exception.Should().ThrowExactlyAsync<ApplicationLayer.ValidationException>();
142-
143141
var thrownException = await exception.Should().ThrowExactlyAsync<ApplicationLayer.ValidationException>();
144-
thrownException.Which.Messages.Should().HaveCount(3);
145-
thrownException.Which.Messages.Should()
146-
.Contain(e => e.PropertyName == "Name" && e.ErrorMessage == "Name is required");
147-
thrownException.Which.Messages.Should()
148-
.Contain(e => e.PropertyName == "Age" && e.ErrorMessage == "Age must be positive");
149-
thrownException.Which.Messages.Should()
150-
.Contain(e => e.PropertyName == "Name" && e.ErrorMessage == "Name must be unique");
151-
152-
mockValidator1.Verify(x => x.Validate(It.IsAny<ValidationContext<RequestTest>>()), Times.Exactly(2));
153-
mockValidator2.Verify(x => x.Validate(It.IsAny<ValidationContext<RequestTest>>()), Times.Exactly(2));
142+
143+
thrownException.Which.Details.Should().NotBeNull();
144+
thrownException.Which.Details.Should().HaveCount(2);
145+
thrownException.Which.Details.Should().ContainKey("Name");
146+
thrownException.Which.Details.Should().ContainKey("Age");
147+
148+
// Name tem 2 erros agrupados
149+
thrownException.Which.Details["Name"].Should().HaveCount(2);
150+
thrownException.Which.Details["Name"].Should().Contain("Name is required");
151+
thrownException.Which.Details["Name"].Should().Contain("Name must be unique");
152+
153+
// Age tem 1 erro
154+
thrownException.Which.Details["Age"].Should().Equal("Age must be positive");
155+
156+
mockValidator1.Verify(x => x.Validate(It.IsAny<ValidationContext<RequestTest>>()), Times.Once);
157+
mockValidator2.Verify(x => x.Validate(It.IsAny<ValidationContext<RequestTest>>()), Times.Once);
154158
}
155159

156160
[Trait("Category", "Unit")]
@@ -162,7 +166,6 @@ public async Task Handle_WhenCollectingErrorsFromAllValidators_ShouldThrowSingle
162166
var mockValidator2 = new Mock<IValidator<RequestTest>>();
163167
var mockValidator3 = new Mock<IValidator<RequestTest>>();
164168

165-
// First validator fails
166169
mockValidator1.Setup(x => x.Validate(It.IsAny<ValidationContext<RequestTest>>()))
167170
.Returns(new ValidationResult([
168171
new ValidationFailure("Name", "Name is required")
@@ -187,18 +190,18 @@ public async Task Handle_WhenCollectingErrorsFromAllValidators_ShouldThrowSingle
187190
);
188191

189192
// Assert
190-
await exception.Should().ThrowExactlyAsync<ApplicationLayer.ValidationException>();
191-
192193
var thrownException = await exception.Should().ThrowExactlyAsync<ApplicationLayer.ValidationException>();
193-
thrownException.Which.Messages.Should().HaveCount(2);
194-
thrownException.Which.Messages.Should()
195-
.Contain(e => e.PropertyName == "Name" && e.ErrorMessage == "Name is required");
196-
thrownException.Which.Messages.Should()
197-
.Contain(e => e.PropertyName == "Age" && e.ErrorMessage == "Age must be positive");
198-
199-
mockValidator1.Verify(x => x.Validate(It.IsAny<ValidationContext<RequestTest>>()), Times.Exactly(2));
200-
mockValidator2.Verify(x => x.Validate(It.IsAny<ValidationContext<RequestTest>>()), Times.Exactly(2));
201-
mockValidator3.Verify(x => x.Validate(It.IsAny<ValidationContext<RequestTest>>()), Times.Exactly(2));
194+
195+
thrownException.Which.Details.Should().NotBeNull();
196+
thrownException.Which.Details.Should().HaveCount(2);
197+
thrownException.Which.Details.Should().ContainKey("Name");
198+
thrownException.Which.Details.Should().ContainKey("Age");
199+
thrownException.Which.Details["Name"].Should().Equal("Name is required");
200+
thrownException.Which.Details["Age"].Should().Equal("Age must be positive");
201+
202+
mockValidator1.Verify(x => x.Validate(It.IsAny<ValidationContext<RequestTest>>()), Times.Once);
203+
mockValidator2.Verify(x => x.Validate(It.IsAny<ValidationContext<RequestTest>>()), Times.Once);
204+
mockValidator3.Verify(x => x.Validate(It.IsAny<ValidationContext<RequestTest>>()), Times.Once);
202205
}
203206

204207
[Trait("Category", "Unit")]
@@ -230,10 +233,15 @@ public async Task Handle_WhenSomeValidatorsFail_ShouldValidateAllValidators()
230233
);
231234

232235
// Assert
233-
await exception.Should().ThrowExactlyAsync<ApplicationLayer.ValidationException>();
234-
236+
var thrownException = await exception.Should().ThrowExactlyAsync<ApplicationLayer.ValidationException>();
237+
235238
// Verifica que TODOS os validators foram chamados
236239
mockValidator1.Verify(x => x.Validate(It.IsAny<ValidationContext<RequestTest>>()), Times.Once);
237240
mockValidator2.Verify(x => x.Validate(It.IsAny<ValidationContext<RequestTest>>()), Times.Once);
241+
242+
// Verifica que os erros foram agrupados corretamente
243+
thrownException.Which.Details.Should().HaveCount(2);
244+
thrownException.Which.Details.Should().ContainKey("Name");
245+
thrownException.Which.Details.Should().ContainKey("Age");
238246
}
239247
}

0 commit comments

Comments
 (0)