Skip to content

Commit 9c440d0

Browse files
nandos13KSemenenkoCopilot
authored
Ensure failed results always have a Problem (#40)
* Partial rework to disallow invalid Result states #32 #35 Begin work to improve Result state safety, ie disallow a Result to be both successful & failed, and to ensure failed results are never missing a Problem. Made constructors for Result types private. Replaced Result Create methods with CreateSuccess & CreateFailed to force parameters for either case. Result.Problem will never be null when the result is unsuccessful. If no problem was recorded, it falls back to returning Problem.GenericError. * CollectionResult changes, less reliance on HasProblem Repeated changes from last commit in the CollectionResult type. IsFailed and HasProblem properties just return the opposite of IsSuccess, since failed results now always have a Problem by design. ThrowIfFail implementations don't rely on HasProblem. HasProblem is now functionally equivalent to IsFailed and is only necessary for the IResultProblem interface. * Fix json deserialization Added [JsonInclude] attribute to IsSuccess properties, as init setter is now private. Moved Json-related attributes from Problem properties to _problem backing fields. Added [JsonIgnore] attribute to Problem properties, as the backing field should now be serialized. Added [JsonInclude] attribute to _problem fields. * Corrected 'HasProblem' test conditions Changed several tests that expect 'HasProblem' to be false in the case of Fail methods that previously did not assign a Problem. This change has been made under the assumption that this is indeed an oversight: #32 (comment) * Additional fixes Change tests that use the AddInvalidMessage methods to operate on a failed result, since said methods now throw when invoked on a successful result. Restore a private unused constructor for Result<T>, which is accessed via Activator.CreateInstance in CommunicationOutgoingGrainCallFilter * Add tests for AddInvalidMessage throws Added two new tests that assert the AddInvalidMessage methods should throw an InvalidOperationException when the result is successful * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Improve 'AddInvalidMessage' usage Moved main implementation of AddInvalidMessage methods to the Problem class (these were previously duplicated identically in all 3 Result types). The same is done for InvalidField & InvalidFieldError methods. AddInvalidMessage methods on Result types are now obsolete and no longer throw an exception when the result is successful. Moved AddInvalidMessage test methods for CollectionResult type to instead test directly on the Problem type. * Test fixes Removed Problem.AddInvalidMessage methods added in last commit. Use the existing AddValidationError method instead which does the same thing, except safer. All unit tests now passing again. --------- Co-authored-by: ksemenenko <KSemenenko@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 9e2a531 commit 9c440d0

26 files changed

+324
-295
lines changed

ManagedCode.Communication.AspNetCore/WebApi/Extensions/ControllerExtensions.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ public static IActionResult ToActionResult<T>(this Result<T> result)
1010
if (result.IsSuccess)
1111
return new OkObjectResult(result.Value);
1212

13-
var problem = result.Problem ?? Problem.Create("Operation failed", "Unknown error occurred", 500);
13+
var problem = result.GetProblemNoFallback() ?? Problem.Create("Operation failed", "Unknown error occurred", 500);
1414
return new ObjectResult(problem)
1515
{
1616
StatusCode = problem.StatusCode
@@ -22,7 +22,7 @@ public static IActionResult ToActionResult(this Result result)
2222
if (result.IsSuccess)
2323
return new NoContentResult();
2424

25-
var problem = result.Problem ?? Problem.Create("Operation failed", "Unknown error occurred", 500);
25+
var problem = result.GetProblemNoFallback() ?? Problem.Create("Operation failed", "Unknown error occurred", 500);
2626
return new ObjectResult(problem)
2727
{
2828
StatusCode = problem.StatusCode
@@ -34,7 +34,7 @@ public static Microsoft.AspNetCore.Http.IResult ToHttpResult<T>(this Result<T> r
3434
if (result.IsSuccess)
3535
return Results.Ok(result.Value);
3636

37-
var problem = result.Problem ?? Problem.Create("Operation failed", "Unknown error occurred", 500);
37+
var problem = result.GetProblemNoFallback() ?? Problem.Create("Operation failed", "Unknown error occurred", 500);
3838
return Results.Problem(
3939
title: problem.Title,
4040
detail: problem.Detail,
@@ -50,7 +50,7 @@ public static Microsoft.AspNetCore.Http.IResult ToHttpResult(this Result result)
5050
if (result.IsSuccess)
5151
return Results.NoContent();
5252

53-
var problem = result.Problem ?? Problem.Create("Operation failed", "Unknown error occurred", 500);
53+
var problem = result.GetProblemNoFallback() ?? Problem.Create("Operation failed", "Unknown error occurred", 500);
5454
return Results.Problem(
5555
title: problem.Title,
5656
detail: problem.Detail,

ManagedCode.Communication.Orleans/Converters/CollectionResultTSurrogateConverter.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ public sealed class CollectionResultTSurrogateConverter<T> : IConverter<Collecti
99
{
1010
public CollectionResult<T> ConvertFromSurrogate(in CollectionResultTSurrogate<T> surrogate)
1111
{
12-
return CollectionResult<T>.Create(surrogate.IsSuccess, surrogate.Collection, surrogate.PageNumber, surrogate.PageSize, surrogate.TotalItems,
13-
surrogate.Problem);
12+
if (surrogate.IsSuccess)
13+
return CollectionResult<T>.CreateSuccess(surrogate.Collection, surrogate.PageNumber, surrogate.PageSize, surrogate.TotalItems);
14+
15+
return CollectionResult<T>.CreateFailed(surrogate.Problem ?? Problem.GenericError(), surrogate.Collection);
1416
}
1517

1618
public CollectionResultTSurrogate<T> ConvertToSurrogate(in CollectionResult<T> value)

ManagedCode.Communication.Orleans/Converters/ResultSurrogateConverter.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ public sealed class ResultSurrogateConverter : IConverter<Result, ResultSurrogat
88
{
99
public Result ConvertFromSurrogate(in ResultSurrogate surrogate)
1010
{
11-
return Result.Create(surrogate.IsSuccess, surrogate.Problem);
11+
if (surrogate.IsSuccess)
12+
return Result.Succeed();
13+
14+
return Result.CreateFailed(surrogate.Problem ?? Problem.GenericError());
1215
}
1316

1417
public ResultSurrogate ConvertToSurrogate(in Result value)

ManagedCode.Communication.Orleans/Converters/ResultTSurrogateConverter.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ public sealed class ResultTSurrogateConverter<T> : IConverter<Result<T>, ResultT
88
{
99
public Result<T> ConvertFromSurrogate(in ResultTSurrogate<T> surrogate)
1010
{
11-
return Result<T>.Create(surrogate.IsSuccess, surrogate.Value, surrogate.Problem);
11+
if (surrogate.IsSuccess)
12+
return Result<T>.Succeed(surrogate.Value!);
13+
14+
return Result<T>.CreateFailed(surrogate.Problem ?? Problem.GenericError(), surrogate.Value);
1215
}
1316

1417
public ResultTSurrogate<T> ConvertToSurrogate(in Result<T> value)

ManagedCode.Communication.Tests/AspNetCore/Extensions/ControllerExtensionsTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ public void ToActionResult_WithValidationError_Returns400WithProblemDetails()
8686
public void ToActionResult_WithNoProblem_ReturnsDefaultError()
8787
{
8888
// Arrange - manually create failed result without problem
89-
var result = new Result<string> { IsSuccess = false, Problem = null };
89+
var result = (Result<string>)default;
9090

9191
// Act
9292
var actionResult = result.ToActionResult();

ManagedCode.Communication.Tests/CollectionResults/CollectionResultFailMethodsTests.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public void Fail_NoParameters_ShouldCreateFailedResult()
2828
result.PageSize.Should().Be(0);
2929
result.TotalItems.Should().Be(0);
3030
result.TotalPages.Should().Be(0);
31-
result.HasProblem.Should().BeFalse();
31+
result.HasProblem.Should().BeTrue();
3232
}
3333

3434
#endregion
@@ -48,7 +48,7 @@ public void Fail_WithEnumerable_ShouldCreateFailedResultWithItems()
4848
result.IsFailed.Should().BeTrue();
4949
result.Collection.Should().BeEquivalentTo(items);
5050
result.Collection.Should().HaveCount(5);
51-
result.HasProblem.Should().BeFalse();
51+
result.HasProblem.Should().BeTrue();
5252
}
5353

5454
[Fact]
@@ -84,7 +84,7 @@ public void Fail_WithArray_ShouldCreateFailedResultWithItems()
8484
result.IsFailed.Should().BeTrue();
8585
result.Collection.Should().BeEquivalentTo(items);
8686
result.Collection.Should().HaveCount(3);
87-
result.HasProblem.Should().BeFalse();
87+
result.HasProblem.Should().BeTrue();
8888
}
8989

9090
[Fact]

ManagedCode.Communication.Tests/CollectionResults/CollectionResultFromMethodsTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -460,7 +460,7 @@ public void From_FailedCollectionResultWithoutProblem_ShouldReturnFailedResult()
460460

461461
// Assert
462462
result.IsFailed.Should().BeTrue();
463-
result.HasProblem.Should().BeFalse();
463+
result.HasProblem.Should().BeTrue();
464464
}
465465

466466
#endregion
@@ -507,7 +507,7 @@ public void From_GenericFailedCollectionResultWithoutProblem_ShouldReturnFailedR
507507

508508
// Assert
509509
result.IsFailed.Should().BeTrue();
510-
result.HasProblem.Should().BeFalse();
510+
result.HasProblem.Should().BeTrue();
511511
}
512512

513513
#endregion

ManagedCode.Communication.Tests/Extensions/ProblemExtensionsTests.cs

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,4 +286,43 @@ public void ToFailedResult_WithComplexExtensions_ShouldPreserveAllData()
286286
errors["email"].Should().Contain("Already exists");
287287
errors["password"].Should().Contain("Too short");
288288
}
289-
}
289+
290+
[Fact]
291+
public void AddInvalidMessage_ShouldAddValidationError()
292+
{
293+
// Arrange
294+
var problem = new Problem();
295+
296+
// Act
297+
problem.AddValidationError("email", "Email is required");
298+
problem.AddValidationError("email", "Email format is invalid");
299+
300+
// Assert
301+
problem.InvalidField("email")
302+
.Should()
303+
.BeTrue();
304+
var emailErrors = problem.InvalidFieldError("email");
305+
emailErrors.Should()
306+
.Contain("Email is required");
307+
emailErrors.Should()
308+
.Contain("Email format is invalid");
309+
}
310+
311+
[Fact]
312+
public void AddInvalidMessage_WithGeneralMessage_ShouldAddToGeneralErrors()
313+
{
314+
// Arrange
315+
var problem = new Problem();
316+
317+
// Act
318+
problem.AddValidationError("General error occurred");
319+
320+
// Assert
321+
problem.InvalidField("_general")
322+
.Should()
323+
.BeTrue();
324+
var generalErrors = problem.InvalidFieldError("_general");
325+
generalErrors.Should()
326+
.Be("General error occurred");
327+
}
328+
}

ManagedCode.Communication.Tests/Results/CollectionResultTests.cs

Lines changed: 0 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -292,45 +292,6 @@ public void InvalidFieldError_WithValidationProblem_ShouldReturnErrorMessage()
292292
.BeEmpty();
293293
}
294294

295-
[Fact]
296-
public void AddInvalidMessage_ShouldAddValidationError()
297-
{
298-
// Arrange
299-
var result = CollectionResult<string>.Empty();
300-
301-
// Act
302-
result.AddInvalidMessage("email", "Email is required");
303-
result.AddInvalidMessage("email", "Email format is invalid");
304-
305-
// Assert
306-
result.InvalidField("email")
307-
.Should()
308-
.BeTrue();
309-
var emailErrors = result.InvalidFieldError("email");
310-
emailErrors.Should()
311-
.Contain("Email is required");
312-
emailErrors.Should()
313-
.Contain("Email format is invalid");
314-
}
315-
316-
[Fact]
317-
public void AddInvalidMessage_WithGeneralMessage_ShouldAddToGeneralErrors()
318-
{
319-
// Arrange
320-
var result = CollectionResult<string>.Empty();
321-
322-
// Act
323-
result.AddInvalidMessage("General error occurred");
324-
325-
// Assert
326-
result.InvalidField("_general")
327-
.Should()
328-
.BeTrue();
329-
var generalErrors = result.InvalidFieldError("_general");
330-
generalErrors.Should()
331-
.Be("General error occurred");
332-
}
333-
334295
[Fact]
335296
public void ThrowIfFail_WithSuccessfulResult_ShouldNotThrow()
336297
{

ManagedCode.Communication.Tests/Results/ResultFailMethodsTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public void Result_Fail_NoParameters_ShouldCreateFailedResult()
2020
// Assert
2121
result.IsFailed.Should().BeTrue();
2222
result.IsSuccess.Should().BeFalse();
23-
result.HasProblem.Should().BeFalse();
23+
result.HasProblem.Should().BeTrue();
2424
}
2525

2626
#endregion

0 commit comments

Comments
 (0)