From 6b8a2b8979aa0d363fab9740cded0990afdfd434 Mon Sep 17 00:00:00 2001 From: Mitch Lienau Date: Fri, 7 Nov 2025 12:47:57 -0500 Subject: [PATCH 1/2] #12 - Adds abiltiy to convert a generic result to a non-generic result --- src/F23.Kernel.Tests/ResultMappingTests.cs | 67 ++++++++++++++++++++++ src/F23.Kernel/Result.cs | 19 +++++- 2 files changed, 85 insertions(+), 1 deletion(-) diff --git a/src/F23.Kernel.Tests/ResultMappingTests.cs b/src/F23.Kernel.Tests/ResultMappingTests.cs index 9973569..dbec97d 100644 --- a/src/F23.Kernel.Tests/ResultMappingTests.cs +++ b/src/F23.Kernel.Tests/ResultMappingTests.cs @@ -87,5 +87,72 @@ public void MapFailure_UnknownType_Throws() // Assert Assert.Throws(Act); + } + + [Fact] + public void GenericSuccessResult_IsConvertedTo_NonGenericSuccessResult() + { + var genericResult = Result.Success(42); + var plainResult = genericResult.Map(); + + Assert.IsType(plainResult); + } + + [Fact] + public void GenericValidationFailedResult_IsConvertedTo_NonGenericValidationFailedResult() + { + var genericResult = Result.ValidationFailed("key", "message"); + var plainResult = genericResult.Map(); + + if (plainResult is not ValidationFailedResult validationFailedResult) + { + throw new InvalidOperationException("The converted result is not a ValidationFailedResult."); + } + + Assert.Single(validationFailedResult.Errors); + Assert.Equal("key", validationFailedResult.Errors.First().Key); + Assert.Equal("message", validationFailedResult.Errors.First().Message); + } + + [Fact] + public void GenericValidationPassedResult_IsConvertedTo_NonGenericValidationPassedResult() + { + var genericResult = new ValidationPassedResult(); + var plainResult = genericResult.Map(); + + if (plainResult is not ValidationPassedResult validationFailedResult) + { + throw new InvalidOperationException("The converted result is not a ValidationFailedResult."); + } + + Assert.Equal("Validation passed", validationFailedResult.Message); + } + + [Fact] + public void GenericUnauthorizedResult_IsConvertedTo_NonGenericUnauthorizedResult() + { + var genericResult = Result.Unauthorized("Something went wrong."); + var plainResult = genericResult.Map(); + + if (plainResult is not UnauthorizedResult unauthorizedResult) + { + throw new InvalidOperationException("The converted result is not an UnauthorizedResult."); + } + + Assert.Equal("Something went wrong.", unauthorizedResult.Message); + } + + [Fact] + public void GenericPreconditionFailedResult_IsConvertedTo_NonGenericPreconditionFailedResult() + { + var genericResult = Result.PreconditionFailed(PreconditionFailedReason.NotFound); + var plainResult = genericResult.Map(); + + if (plainResult is not PreconditionFailedResult preconditionFailedResult) + { + throw new InvalidOperationException("The converted result is not a PreconditionFailedResult."); + } + + Assert.Equal(PreconditionFailedReason.NotFound, preconditionFailedResult.Reason); } } diff --git a/src/F23.Kernel/Result.cs b/src/F23.Kernel/Result.cs index 6fb0dc4..4ad9e9b 100644 --- a/src/F23.Kernel/Result.cs +++ b/src/F23.Kernel/Result.cs @@ -155,6 +155,22 @@ public Result MapFailure() => _ => throw new InvalidOperationException("Unknown result type") }; + /// + /// Maps a generic value Result to a non-generic Result. + /// + /// A instance that corresponds to the type of the current result. + /// Thrown if the current result type is unrecognized. + public Result Map() => + this switch + { + SuccessResult => Success(), + PreconditionFailedResult preconditionFailed => Result.PreconditionFailed(preconditionFailed.Reason), + UnauthorizedResult unauthorized => Result.Unauthorized(unauthorized.Message), + ValidationFailedResult validationFailed => Result.ValidationFailed(validationFailed.Errors), + ValidationPassedResult => new ValidationPassedResult(), + _ => throw new InvalidOperationException("Unknown result type") + }; + /// /// Logs the failure result to the specified . /// @@ -178,6 +194,7 @@ public void LogFailure(ILogger logger, LogLevel logLevel = LogLevel.Warning) break; default: throw new InvalidOperationException("Unknown result type"); - }; + } + ; } } From 87ec50a8036cf0f713d5944b9390beeb72eb6a2b Mon Sep 17 00:00:00 2001 From: Mitch Lienau Date: Fri, 7 Nov 2025 13:25:57 -0500 Subject: [PATCH 2/2] PR feedback --- .../MinimalApiResultExtensionsTests.cs | 12 ++++++--- .../AspNetCore/MvcResultExtensionsTests.cs | 12 ++++++--- src/F23.Kernel.Tests/Mocks/UnknownResult.cs | 7 ++++- src/F23.Kernel.Tests/ResultMappingTests.cs | 27 ++++--------------- src/F23.Kernel/Result.cs | 16 +++-------- .../Results/PreconditionFailedResult.cs | 8 +++++- src/F23.Kernel/Results/SuccessResult.cs | 6 +++++ src/F23.Kernel/Results/UnauthorizedResult.cs | 6 +++++ .../Results/ValidationFailedResult.cs | 8 +++++- .../Results/ValidationPassedResult.cs | 6 +++++ 10 files changed, 62 insertions(+), 46 deletions(-) diff --git a/src/F23.Kernel.Tests/AspNetCore/MinimalApiResultExtensionsTests.cs b/src/F23.Kernel.Tests/AspNetCore/MinimalApiResultExtensionsTests.cs index add8d58..fbd4c1f 100644 --- a/src/F23.Kernel.Tests/AspNetCore/MinimalApiResultExtensionsTests.cs +++ b/src/F23.Kernel.Tests/AspNetCore/MinimalApiResultExtensionsTests.cs @@ -3,7 +3,6 @@ using F23.Kernel.AspNetCore; using F23.Kernel.Results; using Microsoft.AspNetCore.Http.HttpResults; -using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ModelBinding; namespace F23.Kernel.Tests.AspNetCore; @@ -76,7 +75,7 @@ public void PreconditionFailedResult_ConcurrencyMismatch_Returns_StatusCodeResul // Assert var statusCodeResult = Assert.IsType(actionResult); - Assert.Equal((int) HttpStatusCode.PreconditionFailed, statusCodeResult.StatusCode); + Assert.Equal((int)HttpStatusCode.PreconditionFailed, statusCodeResult.StatusCode); } [Fact] @@ -179,7 +178,7 @@ public void PreconditionFailedResultT_ConcurrencyMismatch_Returns_StatusCodeResu // Assert var statusCodeResult = Assert.IsType(actionResult); - Assert.Equal((int) HttpStatusCode.PreconditionFailed, statusCodeResult.StatusCode); + Assert.Equal((int)HttpStatusCode.PreconditionFailed, statusCodeResult.StatusCode); } [Fact] @@ -423,6 +422,11 @@ private class TestUnhandledResult() : Result(true) private class TestUnhandledResult() : Result(true) { - public override string Message => "whoopsie"; + public override string Message => "whoopsie"; + + public override Result Map() + { + throw new NotImplementedException(); + } } } diff --git a/src/F23.Kernel.Tests/AspNetCore/MvcResultExtensionsTests.cs b/src/F23.Kernel.Tests/AspNetCore/MvcResultExtensionsTests.cs index 24100c3..47b819e 100644 --- a/src/F23.Kernel.Tests/AspNetCore/MvcResultExtensionsTests.cs +++ b/src/F23.Kernel.Tests/AspNetCore/MvcResultExtensionsTests.cs @@ -3,7 +3,6 @@ using F23.Kernel.AspNetCore; using F23.Kernel.Results; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.ModelBinding; namespace F23.Kernel.Tests.AspNetCore; @@ -75,7 +74,7 @@ public void PreconditionFailedResult_ConcurrencyMismatch_Returns_StatusCodeResul // Assert var statusCodeResult = Assert.IsType(actionResult); - Assert.Equal((int) HttpStatusCode.PreconditionFailed, statusCodeResult.StatusCode); + Assert.Equal((int)HttpStatusCode.PreconditionFailed, statusCodeResult.StatusCode); } [Fact] @@ -178,7 +177,7 @@ public void PreconditionFailedResultT_ConcurrencyMismatch_Returns_StatusCodeResu // Assert var statusCodeResult = Assert.IsType(actionResult); - Assert.Equal((int) HttpStatusCode.PreconditionFailed, statusCodeResult.StatusCode); + Assert.Equal((int)HttpStatusCode.PreconditionFailed, statusCodeResult.StatusCode); } [Fact] @@ -460,6 +459,11 @@ private class TestUnhandledResult() : Result(true) private class TestUnhandledResult() : Result(true) { - public override string Message => "whoopsie"; + public override string Message => "whoopsie"; + + public override Result Map() + { + throw new NotImplementedException(); + } } } diff --git a/src/F23.Kernel.Tests/Mocks/UnknownResult.cs b/src/F23.Kernel.Tests/Mocks/UnknownResult.cs index 0b0b2c3..f266396 100644 --- a/src/F23.Kernel.Tests/Mocks/UnknownResult.cs +++ b/src/F23.Kernel.Tests/Mocks/UnknownResult.cs @@ -2,5 +2,10 @@ namespace F23.Kernel.Tests.Mocks; public class UnknownResult() : Result(false) { - public override string Message => "Unknown result"; + public override string Message => "Unknown result"; + + public override Result Map() + { + throw new NotImplementedException(); + } } diff --git a/src/F23.Kernel.Tests/ResultMappingTests.cs b/src/F23.Kernel.Tests/ResultMappingTests.cs index dbec97d..be0aba7 100644 --- a/src/F23.Kernel.Tests/ResultMappingTests.cs +++ b/src/F23.Kernel.Tests/ResultMappingTests.cs @@ -93,21 +93,16 @@ public void MapFailure_UnknownType_Throws() public void GenericSuccessResult_IsConvertedTo_NonGenericSuccessResult() { var genericResult = Result.Success(42); - var plainResult = genericResult.Map(); - Assert.IsType(plainResult); + Assert.IsType(genericResult.Map()); } [Fact] public void GenericValidationFailedResult_IsConvertedTo_NonGenericValidationFailedResult() { var genericResult = Result.ValidationFailed("key", "message"); - var plainResult = genericResult.Map(); - if (plainResult is not ValidationFailedResult validationFailedResult) - { - throw new InvalidOperationException("The converted result is not a ValidationFailedResult."); - } + var validationFailedResult = Assert.IsType(genericResult.Map()); Assert.Single(validationFailedResult.Errors); Assert.Equal("key", validationFailedResult.Errors.First().Key); @@ -118,12 +113,8 @@ public void GenericValidationFailedResult_IsConvertedTo_NonGenericValidationFail public void GenericValidationPassedResult_IsConvertedTo_NonGenericValidationPassedResult() { var genericResult = new ValidationPassedResult(); - var plainResult = genericResult.Map(); - if (plainResult is not ValidationPassedResult validationFailedResult) - { - throw new InvalidOperationException("The converted result is not a ValidationFailedResult."); - } + var validationFailedResult = Assert.IsType(genericResult.Map()); Assert.Equal("Validation passed", validationFailedResult.Message); } @@ -132,12 +123,8 @@ public void GenericValidationPassedResult_IsConvertedTo_NonGenericValidationPass public void GenericUnauthorizedResult_IsConvertedTo_NonGenericUnauthorizedResult() { var genericResult = Result.Unauthorized("Something went wrong."); - var plainResult = genericResult.Map(); - if (plainResult is not UnauthorizedResult unauthorizedResult) - { - throw new InvalidOperationException("The converted result is not an UnauthorizedResult."); - } + var unauthorizedResult = Assert.IsType(genericResult.Map()); Assert.Equal("Something went wrong.", unauthorizedResult.Message); } @@ -146,12 +133,8 @@ public void GenericUnauthorizedResult_IsConvertedTo_NonGenericUnauthorizedResult public void GenericPreconditionFailedResult_IsConvertedTo_NonGenericPreconditionFailedResult() { var genericResult = Result.PreconditionFailed(PreconditionFailedReason.NotFound); - var plainResult = genericResult.Map(); - if (plainResult is not PreconditionFailedResult preconditionFailedResult) - { - throw new InvalidOperationException("The converted result is not a PreconditionFailedResult."); - } + var preconditionFailedResult = Assert.IsType(genericResult.Map()); Assert.Equal(PreconditionFailedReason.NotFound, preconditionFailedResult.Reason); } diff --git a/src/F23.Kernel/Result.cs b/src/F23.Kernel/Result.cs index 4ad9e9b..dbddb18 100644 --- a/src/F23.Kernel/Result.cs +++ b/src/F23.Kernel/Result.cs @@ -156,20 +156,10 @@ public Result MapFailure() => }; /// - /// Maps a generic value Result to a non-generic Result. + /// Maps the implementing generic Result to a non-generic . /// - /// A instance that corresponds to the type of the current result. - /// Thrown if the current result type is unrecognized. - public Result Map() => - this switch - { - SuccessResult => Success(), - PreconditionFailedResult preconditionFailed => Result.PreconditionFailed(preconditionFailed.Reason), - UnauthorizedResult unauthorized => Result.Unauthorized(unauthorized.Message), - ValidationFailedResult validationFailed => Result.ValidationFailed(validationFailed.Errors), - ValidationPassedResult => new ValidationPassedResult(), - _ => throw new InvalidOperationException("Unknown result type") - }; + /// A new non-generic . + public abstract Result Map(); /// /// Logs the failure result to the specified . diff --git a/src/F23.Kernel/Results/PreconditionFailedResult.cs b/src/F23.Kernel/Results/PreconditionFailedResult.cs index 791b443..d949f14 100644 --- a/src/F23.Kernel/Results/PreconditionFailedResult.cs +++ b/src/F23.Kernel/Results/PreconditionFailedResult.cs @@ -46,5 +46,11 @@ public class PreconditionFailedResult(PreconditionFailedReason reason, string /// Otherwise, it will return the default message associated with the /// value. /// - public override string Message => message ?? Reason.ToMessage(); + public override string Message => message ?? Reason.ToMessage(); + + /// + /// Maps this generic precondition failed result to a non-generic precondition failed result. + /// + /// A non-generic . + public override Result Map() => new PreconditionFailedResult(Reason, Message); } diff --git a/src/F23.Kernel/Results/SuccessResult.cs b/src/F23.Kernel/Results/SuccessResult.cs index dcfb779..68b988e 100644 --- a/src/F23.Kernel/Results/SuccessResult.cs +++ b/src/F23.Kernel/Results/SuccessResult.cs @@ -30,4 +30,10 @@ public class SuccessResult(T value) : Result(true) /// This represents the successful outcome of the operation when the result is a . /// public T Value => value; + + /// + /// Maps the current object to a successful result. + /// + /// A instance representing a successful operation. + public override Result Map() => Success(); } diff --git a/src/F23.Kernel/Results/UnauthorizedResult.cs b/src/F23.Kernel/Results/UnauthorizedResult.cs index 0540c1b..1c3f0ca 100644 --- a/src/F23.Kernel/Results/UnauthorizedResult.cs +++ b/src/F23.Kernel/Results/UnauthorizedResult.cs @@ -21,4 +21,10 @@ public class UnauthorizedResult(string message) : Result(false) /// Gets a message describing the outcome of the operation. /// public override string Message => message; + + /// + /// Maps to a non-generic unauthorized result. + /// + /// A non-generic . + public override Result Map() => Result.Unauthorized(message); } diff --git a/src/F23.Kernel/Results/ValidationFailedResult.cs b/src/F23.Kernel/Results/ValidationFailedResult.cs index 31f67de..a0c61b7 100644 --- a/src/F23.Kernel/Results/ValidationFailedResult.cs +++ b/src/F23.Kernel/Results/ValidationFailedResult.cs @@ -38,5 +38,11 @@ public class ValidationFailedResult(IReadOnlyCollection erro /// This property provides detailed information about the errors that occurred during validation, /// including the key associated with each error and its corresponding message. /// - public IReadOnlyCollection Errors => errors; + public IReadOnlyCollection Errors => errors; + + /// + /// Maps the current validation errors to a . + /// + /// A non-generic containing the validation errors. + public override Result Map() => new ValidationFailedResult(Errors); } diff --git a/src/F23.Kernel/Results/ValidationPassedResult.cs b/src/F23.Kernel/Results/ValidationPassedResult.cs index ef7b9f4..f2b2443 100644 --- a/src/F23.Kernel/Results/ValidationPassedResult.cs +++ b/src/F23.Kernel/Results/ValidationPassedResult.cs @@ -21,4 +21,10 @@ public class ValidationPassedResult() : ValidationResult(true) /// Gets a message describing the outcome of the operation. /// public override string Message => "Validation passed"; + + /// + /// Maps to a non-generic instance. + /// + /// A non-generic . + public override Result Map() => new ValidationPassedResult(); }