From 12abc0820f99ff9166134c79c897ea3f58c2e4a3 Mon Sep 17 00:00:00 2001 From: Saeed Ghanavat Date: Wed, 9 Jul 2025 00:57:45 +0100 Subject: [PATCH 1/7] - Implemented Aggregate functionality with unit tests - Tidy ups --- Directory.Packages.props | 5 + Ghanavats.ResultPattern.sln | 7 ++ README.md | 6 +- .../FluentValidationFailureExtension.cs | 5 +- src/Ghanavats.ResultPattern/Result.Void.cs | 57 ++++++++- src/Ghanavats.ResultPattern/Result.cs | 34 +++--- .../Ghanavats.ResultPattern.Tests.csproj | 24 ++++ .../ResultTests.cs | 109 ++++++++++++++++++ 8 files changed, 227 insertions(+), 20 deletions(-) create mode 100644 tests/Ghanavats.ResultPattern.Tests/Ghanavats.ResultPattern.Tests.csproj create mode 100644 tests/Ghanavats.ResultPattern.Tests/ResultTests.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 94f7a3d..35be281 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -2,5 +2,10 @@ + + + + + \ No newline at end of file diff --git a/Ghanavats.ResultPattern.sln b/Ghanavats.ResultPattern.sln index 6bb330a..2170bc8 100644 --- a/Ghanavats.ResultPattern.sln +++ b/Ghanavats.ResultPattern.sln @@ -13,6 +13,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionItems", "SolutionIt README.md = README.md EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ghanavats.ResultPattern.Tests", "tests\Ghanavats.ResultPattern.Tests\Ghanavats.ResultPattern.Tests.csproj", "{857C6761-2A3B-4919-8FA7-433E2B2157B4}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -23,8 +25,13 @@ Global {654CA5E2-972C-4561-BA0E-B532E1327A15}.Debug|Any CPU.Build.0 = Debug|Any CPU {654CA5E2-972C-4561-BA0E-B532E1327A15}.Release|Any CPU.ActiveCfg = Release|Any CPU {654CA5E2-972C-4561-BA0E-B532E1327A15}.Release|Any CPU.Build.0 = Release|Any CPU + {857C6761-2A3B-4919-8FA7-433E2B2157B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {857C6761-2A3B-4919-8FA7-433E2B2157B4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {857C6761-2A3B-4919-8FA7-433E2B2157B4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {857C6761-2A3B-4919-8FA7-433E2B2157B4}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {654CA5E2-972C-4561-BA0E-B532E1327A15} = {211150B0-9A81-462E-8D30-C00B45504AD6} + {857C6761-2A3B-4919-8FA7-433E2B2157B4} = {78FEF537-F8DF-4722-BEB6-091C57BA134A} EndGlobalSection EndGlobal diff --git a/README.md b/README.md index 8803b65..be5e6f8 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,15 @@ A Robust and Flexible Result Handling Framework for Modern Applications ## Overview -Ghanavats.ResultPattern is a comprehensive framework designed to bring consistency +Ghanavats.ResultPattern is a small/simple, unambitious framework designed to bring consistency and flexibility to result handling in your applications. Whether you need to return simple success/error outcomes or handle complex validation scenarios, this solution provides the tools you need to implement robust result patterns effortlessly. +The idea behind this was to learn the Result Pattern by implementing it in a good way. +The plan was not and is not to get all developers, worldwide, to adopt it. +Instead, to teach myself, and maybe if I am lucky, to show you what it may look like. + ## The solution includes 1. **Ghanavats.ResultPattern NuGet Package** diff --git a/src/Ghanavats.ResultPattern/Extensions/FluentValidationFailureExtension.cs b/src/Ghanavats.ResultPattern/Extensions/FluentValidationFailureExtension.cs index 486b25a..38356ad 100644 --- a/src/Ghanavats.ResultPattern/Extensions/FluentValidationFailureExtension.cs +++ b/src/Ghanavats.ResultPattern/Extensions/FluentValidationFailureExtension.cs @@ -10,6 +10,9 @@ public static class FluentValidationFailureExtension { public static IEnumerable PopulateValidationErrors(this ValidationResult? input) { - return input is null ? [] : input.Errors.Select(x => new ValidationError(x.ErrorMessage, x.ErrorCode, (ValidationErrorType)x.Severity)); + return input is null + ? [] + : input.Errors + .Select(x => new ValidationError(x.ErrorMessage, x.ErrorCode, (ValidationErrorType)x.Severity)); } } diff --git a/src/Ghanavats.ResultPattern/Result.Void.cs b/src/Ghanavats.ResultPattern/Result.Void.cs index c31e0e6..d19e274 100644 --- a/src/Ghanavats.ResultPattern/Result.Void.cs +++ b/src/Ghanavats.ResultPattern/Result.Void.cs @@ -10,15 +10,19 @@ public class Result : Result { /// /// Private constructor that is used in this class. - /// No need to set Status here as its default id OK. + /// No need to set Status here as its default is OK. /// - private Result() { } + private Result() + { + } /// /// A constructor that accepts /// /// - private Result(ResultStatus status) : base(status) { } + private Result(ResultStatus status) : base(status) + { + } /// /// Represents a successful operation without a return type @@ -47,4 +51,51 @@ private Result(ResultStatus status) : base(status) { } { ErrorMessages = [errorMessage] }; + + public static Result Aggregate(params Result[] results) + { + var resultLocal = new Result(); + + PopulateErrorMessagesForError(); + PopulateValidationErrors(); + + if (resultLocal.Status == ResultStatus.None) + { + resultLocal.Status = ResultStatus.Ok; + } + + return resultLocal; + + void PopulateErrorMessagesForError() + { + var anyErrors = results.Where(x => x.Status == ResultStatus.Error).ToArray(); + resultLocal.Status = anyErrors.Length > 0 ? ResultStatus.Error : ResultStatus.None; + + var errorMessagesLocal = new List(); + foreach (var result in anyErrors) + { + errorMessagesLocal.AddRange(result.ErrorMessages); + } + + resultLocal.ErrorMessages = errorMessagesLocal; + } + void PopulateValidationErrors() + { + var anyValidationErrors = results.Where(x => x.Status == ResultStatus.Invalid).ToArray(); + + if (anyValidationErrors.Length > 0 + && resultLocal.Status != ResultStatus.Error) + { + resultLocal.Status = ResultStatus.Invalid; + } + + var validationErrorsLocal = new List(); + foreach (var result in anyValidationErrors) + { + validationErrorsLocal.AddRange(result.ValidationErrors); + } + + resultLocal.ValidationErrors = validationErrorsLocal; + } + } } diff --git a/src/Ghanavats.ResultPattern/Result.cs b/src/Ghanavats.ResultPattern/Result.cs index bdf4b6a..30dda89 100644 --- a/src/Ghanavats.ResultPattern/Result.cs +++ b/src/Ghanavats.ResultPattern/Result.cs @@ -1,6 +1,9 @@ -using System.Text.Json.Serialization; +using System.Runtime.CompilerServices; +using System.Text.Json.Serialization; using Ghanavats.ResultPattern.Enums; +[assembly: InternalsVisibleTo(assemblyName: "Ghanavats.ResultPattern.Tests")] + namespace Ghanavats.ResultPattern; /// @@ -10,6 +13,15 @@ namespace Ghanavats.ResultPattern; /// public class Result { + /// + /// A constructor that accepts . + /// + /// Constructor parameter of type + public Result(T data) + { + Data = data; + } + /// /// A constructor that accepts . /// It is used internally in this class and Result.Void @@ -19,24 +31,15 @@ internal Result(ResultStatus status) { Status = status; } - + /// /// Default protected constructor. /// /// - /// It is used in Result.Void to return an instance of Success status without needing to pass any extra types. + /// It is used in Result.Void to return an instance of + /// Success status without needing to pass any extra types. /// protected internal Result() { } - - /// - /// A constructor that accepts . - /// It is used internally by this class. - /// - /// Constructor parameter of type - private Result(T data) - { - Data = data; - } /// /// A constructor that accepts . @@ -44,8 +47,9 @@ private Result(T data) /// /// Constructor parameter of type /// Constructor parameter of type - private Result(T data, string successMessage) : this(data) + internal Result(T data, string successMessage) { + Data = data; SuccessMessage = successMessage; } @@ -62,7 +66,7 @@ private Result(T data, string successMessage) : this(data) public T? Data { get; set; } /// - /// Use this property to accurately determine the exact status of the Result + /// Use this property to accurately determine the exact Result status /// [JsonInclude] public ResultStatus Status { get; protected set; } = ResultStatus.Ok; diff --git a/tests/Ghanavats.ResultPattern.Tests/Ghanavats.ResultPattern.Tests.csproj b/tests/Ghanavats.ResultPattern.Tests/Ghanavats.ResultPattern.Tests.csproj new file mode 100644 index 0000000..72b3319 --- /dev/null +++ b/tests/Ghanavats.ResultPattern.Tests/Ghanavats.ResultPattern.Tests.csproj @@ -0,0 +1,24 @@ + + + + false + + + + + + + + + + + + + + + + + + + + diff --git a/tests/Ghanavats.ResultPattern.Tests/ResultTests.cs b/tests/Ghanavats.ResultPattern.Tests/ResultTests.cs new file mode 100644 index 0000000..9fd5776 --- /dev/null +++ b/tests/Ghanavats.ResultPattern.Tests/ResultTests.cs @@ -0,0 +1,109 @@ +using Ghanavats.ResultPattern.Enums; +using Shouldly; + +namespace Ghanavats.ResultPattern.Tests; + +public class ResultTests +{ + [Fact] + public void ResultConstructorWithData_ShouldBeInitialised_WithCorrectValueAndStatus() + { + //Arrange/Act + var sut = new Result(1234); + + //Assert + sut.ShouldNotBeNull(); + sut.Status.ShouldBe(ResultStatus.Ok); + } + + // [Fact] + // public void ResultConstructorWithStatus_ShouldBeInitialised_WithCorrectValueAndStatus() + // { + // //Arrange/Act + // var sut = new Result(ResultStatus.Ok); + // + // //Assert + // sut.ShouldNotBeNull(); + // sut.Status.ShouldBe(ResultStatus.Ok); + // } + + [Fact] + public void ResultAggregate_ShouldCorrectlyCombineAllResults_WhenThereAreErrors() + { + //arrange + var result1 = Result.Error("Error1"); + var result2 = Result.Error("Error2"); + var result3 = Result.Error("Error3"); + + //act + var actual = Result.Aggregate(result1, result2, result3); + + //assert + actual.ShouldNotBeNull(); + actual.Status.ShouldBe(ResultStatus.Error); + actual.ErrorMessages.ShouldNotBeEmpty(); + actual.ErrorMessages.Count().ShouldBe(3); + } + + [Fact] + public void ResultAggregate_ShouldCorrectlyCombineAllResults_WhenThereAreValidationErrors() + { + //arrange + var result1 = Result.Invalid([new ValidationError("Validation error 1", "123", ValidationErrorType.Error)]); + var result2 = Result.Invalid([new ValidationError("Validation error 2", "1234", ValidationErrorType.Error)]); + var result3 = Result.Invalid([new ValidationError("Validation error 3", "1235", ValidationErrorType.Error)]); + + //act + var actual = Result.Aggregate(result1, result2, result3); + + //assert + actual.ShouldNotBeNull(); + actual.Status.ShouldBe(ResultStatus.Invalid); + actual.ValidationErrors.ShouldNotBeEmpty(); + actual.ValidationErrors.Count().ShouldBe(3); + } + + [Fact] + public void ResultAggregate_ShouldCorrectlyCombineAllResults_WhenThereAreValidationErrorsAndErrors() + { + //arrange + var result1 = Result.Invalid([new ValidationError("Validation error Jan", "3434", ValidationErrorType.Error)]); + var result2 = Result.Invalid([new ValidationError("Validation error Feb", "5554", ValidationErrorType.Error)]); + var result3 = Result.Invalid([new ValidationError("Validation error March", "6678", ValidationErrorType.Error)]); + + var result4 = Result.Error("Error234"); + var result5 = Result.Error("Error2676"); + var result6 = Result.Error("Error3009"); + + //act + var actual = Result.Aggregate(result1, result2, result3, result4, result5, result6); + + //assert + actual.ShouldNotBeNull(); + actual.Status.ShouldBe(ResultStatus.Error); + + actual.ErrorMessages.ShouldNotBeEmpty(); + actual.ErrorMessages.Count().ShouldBe(3); + + actual.ValidationErrors.ShouldNotBeEmpty(); + actual.ValidationErrors.Count().ShouldBe(3); + } + + [Fact] + public void ResultAggregate_ShouldReturnCorrectResultStatus_WhenAllResultsAreOK() + { + //arrange + var result1 = Result.Success(); + var result2 = Result.Success(); + var result3 = Result.Success(); + + //act + var actual = Result.Aggregate(result1, result2, result3); + + //assert + actual.ShouldNotBeNull(); + actual.Status.ShouldBe(ResultStatus.Ok); + actual.ErrorMessages.ShouldBeEmpty(); + actual.ValidationErrors.ShouldBeEmpty(); + } +} From 5ebefee14df20060899e004a238d1432ce95f952 Mon Sep 17 00:00:00 2001 From: Saeed Ghanavat Date: Wed, 16 Jul 2025 21:24:52 +0100 Subject: [PATCH 2/7] - Re-implemented Aggregate feature + unit tests --- .../Extensions/ResultExtensions.cs | 61 +++++ .../Models/AggregateResultsModel.cs | 10 + src/Ghanavats.ResultPattern/Result.Void.cs | 53 +---- src/Ghanavats.ResultPattern/Result.cs | 15 ++ .../DummyData/DummyResponseModel.cs | 6 + .../ResultTests.cs | 225 ++++++++++++++---- 6 files changed, 277 insertions(+), 93 deletions(-) create mode 100644 src/Ghanavats.ResultPattern/Extensions/ResultExtensions.cs create mode 100644 src/Ghanavats.ResultPattern/Models/AggregateResultsModel.cs create mode 100644 tests/Ghanavats.ResultPattern.Tests/DummyData/DummyResponseModel.cs diff --git a/src/Ghanavats.ResultPattern/Extensions/ResultExtensions.cs b/src/Ghanavats.ResultPattern/Extensions/ResultExtensions.cs new file mode 100644 index 0000000..3fbc405 --- /dev/null +++ b/src/Ghanavats.ResultPattern/Extensions/ResultExtensions.cs @@ -0,0 +1,61 @@ +using Ghanavats.ResultPattern.Enums; + +namespace Ghanavats.ResultPattern.Extensions; + +public static class ResultExtensions +{ + public static IReadOnlyCollection GetMessages(this Result[] results, ResultStatus status, + bool includeValidationErrors = false) + { + switch (status) + { + case ResultStatus.Error: + return GetErrorMessages(); + case ResultStatus.Invalid: + return includeValidationErrors + ? GetValidationErrors() + : GetValidationMessages(); + case ResultStatus.None: + case ResultStatus.Ok: + return GetSuccessMessages(); + case ResultStatus.NotFound: + default: + return []; + } + + IReadOnlyCollection GetErrorMessages() + { + var resultErrors = results + .Where(error => error.Status == ResultStatus.Error + && error.ErrorMessages.Any()) + .SelectMany(error => error.ErrorMessages).ToList(); + + return resultErrors; + } + + IReadOnlyCollection GetValidationMessages() + { + return results + .Where(invalids => invalids.ValidationErrors.Any() + && invalids.Status == ResultStatus.Invalid) + .SelectMany(invalidMessages => invalidMessages.ValidationErrors + .Select(x => x.ErrorMessage)).ToList(); + } + + IReadOnlyCollection GetValidationErrors() + { + return results + .Where(invalids => invalids.ValidationErrors.Any() + && invalids.Status == ResultStatus.Invalid) + .SelectMany(invalidMessages => invalidMessages.ValidationErrors).ToList(); + } + + IReadOnlyCollection GetSuccessMessages() + { + return results + .Where(x => x.Status == ResultStatus.Ok + && x.ErrorMessages.Any()) + .SelectMany(error => error.ErrorMessages).ToList(); + } + } +} diff --git a/src/Ghanavats.ResultPattern/Models/AggregateResultsModel.cs b/src/Ghanavats.ResultPattern/Models/AggregateResultsModel.cs new file mode 100644 index 0000000..27d0f1c --- /dev/null +++ b/src/Ghanavats.ResultPattern/Models/AggregateResultsModel.cs @@ -0,0 +1,10 @@ +using Ghanavats.ResultPattern.Enums; + +namespace Ghanavats.ResultPattern.Models; + +public class AggregateResultsModel +{ + public ResultStatus Status { get; init; } = ResultStatus.None; + public string? TypeName { get; init; } + public IReadOnlyCollection Messages { get; init; } = []; +} diff --git a/src/Ghanavats.ResultPattern/Result.Void.cs b/src/Ghanavats.ResultPattern/Result.Void.cs index d19e274..5220a98 100644 --- a/src/Ghanavats.ResultPattern/Result.Void.cs +++ b/src/Ghanavats.ResultPattern/Result.Void.cs @@ -1,4 +1,6 @@ using Ghanavats.ResultPattern.Enums; +using Ghanavats.ResultPattern.Extensions; +using Ghanavats.ResultPattern.Models; namespace Ghanavats.ResultPattern; @@ -52,50 +54,15 @@ private Result(ResultStatus status) : base(status) ErrorMessages = [errorMessage] }; - public static Result Aggregate(params Result[] results) + public new static IReadOnlyCollection Aggregate(bool includeValidationErrors = false, + params Result[] results) { - var resultLocal = new Result(); - - PopulateErrorMessagesForError(); - PopulateValidationErrors(); - - if (resultLocal.Status == ResultStatus.None) - { - resultLocal.Status = ResultStatus.Ok; - } - - return resultLocal; - - void PopulateErrorMessagesForError() - { - var anyErrors = results.Where(x => x.Status == ResultStatus.Error).ToArray(); - resultLocal.Status = anyErrors.Length > 0 ? ResultStatus.Error : ResultStatus.None; - - var errorMessagesLocal = new List(); - foreach (var result in anyErrors) - { - errorMessagesLocal.AddRange(result.ErrorMessages); - } - - resultLocal.ErrorMessages = errorMessagesLocal; - } - void PopulateValidationErrors() - { - var anyValidationErrors = results.Where(x => x.Status == ResultStatus.Invalid).ToArray(); - - if (anyValidationErrors.Length > 0 - && resultLocal.Status != ResultStatus.Error) - { - resultLocal.Status = ResultStatus.Invalid; - } - - var validationErrorsLocal = new List(); - foreach (var result in anyValidationErrors) + return results + .GroupBy(result => result.Status) + .Select(whatIWant => new AggregateResultsModel { - validationErrorsLocal.AddRange(result.ValidationErrors); - } - - resultLocal.ValidationErrors = validationErrorsLocal; - } + Status = whatIWant.Key, + Messages = results.GetMessages(whatIWant.Key, includeValidationErrors) + }).ToList().AsReadOnly(); } } diff --git a/src/Ghanavats.ResultPattern/Result.cs b/src/Ghanavats.ResultPattern/Result.cs index 30dda89..4e2a244 100644 --- a/src/Ghanavats.ResultPattern/Result.cs +++ b/src/Ghanavats.ResultPattern/Result.cs @@ -1,6 +1,8 @@ using System.Runtime.CompilerServices; using System.Text.Json.Serialization; using Ghanavats.ResultPattern.Enums; +using Ghanavats.ResultPattern.Extensions; +using Ghanavats.ResultPattern.Models; [assembly: InternalsVisibleTo(assemblyName: "Ghanavats.ResultPattern.Tests")] @@ -139,6 +141,19 @@ public static Result Invalid(IEnumerable validationErrors) { return new Result(ResultStatus.Invalid) { ValidationErrors = validationErrors }; } + + public static IReadOnlyCollection Aggregate(bool includeValidationErrors = false, + params Result[] results) + { + return results + .GroupBy(result => result.Status) + .Select(whatIWant => new AggregateResultsModel + { + Status = whatIWant.Key, + TypeName = typeof(T).Name, + Messages = results.GetMessages(whatIWant.Key, includeValidationErrors) + }).ToList().AsReadOnly(); + } /// /// An operator to automatically convert the return type in a method to the type being returned diff --git a/tests/Ghanavats.ResultPattern.Tests/DummyData/DummyResponseModel.cs b/tests/Ghanavats.ResultPattern.Tests/DummyData/DummyResponseModel.cs new file mode 100644 index 0000000..353f306 --- /dev/null +++ b/tests/Ghanavats.ResultPattern.Tests/DummyData/DummyResponseModel.cs @@ -0,0 +1,6 @@ +namespace Ghanavats.ResultPattern.Tests.DummyData; + +public class DummyResponseModel +{ + public string DummyFieldOne { get; set; } = string.Empty; +} diff --git a/tests/Ghanavats.ResultPattern.Tests/ResultTests.cs b/tests/Ghanavats.ResultPattern.Tests/ResultTests.cs index 9fd5776..0ae9b20 100644 --- a/tests/Ghanavats.ResultPattern.Tests/ResultTests.cs +++ b/tests/Ghanavats.ResultPattern.Tests/ResultTests.cs @@ -1,4 +1,5 @@ using Ghanavats.ResultPattern.Enums; +using Ghanavats.ResultPattern.Tests.DummyData; using Shouldly; namespace Ghanavats.ResultPattern.Tests; @@ -16,94 +17,218 @@ public void ResultConstructorWithData_ShouldBeInitialised_WithCorrectValueAndSta sut.Status.ShouldBe(ResultStatus.Ok); } - // [Fact] - // public void ResultConstructorWithStatus_ShouldBeInitialised_WithCorrectValueAndStatus() - // { - // //Arrange/Act - // var sut = new Result(ResultStatus.Ok); - // - // //Assert - // sut.ShouldNotBeNull(); - // sut.Status.ShouldBe(ResultStatus.Ok); - // } - [Fact] - public void ResultAggregate_ShouldCorrectlyCombineAllResults_WhenThereAreErrors() + public void ResultConstructorWithStatus_ShouldBeInitialised_WithCorrectValueAndStatus() { - //arrange - var result1 = Result.Error("Error1"); - var result2 = Result.Error("Error2"); - var result3 = Result.Error("Error3"); + //Arrange/Act + var sut = new Result(ResultStatus.Ok); - //act - var actual = Result.Aggregate(result1, result2, result3); + //Assert + sut.ShouldNotBeNull(); + sut.IsSuccess.ShouldBeTrue(); + sut.Status.ShouldBe(ResultStatus.Ok); + } + + [Fact] + public void ResultError_ShouldCorrectlySetStatusWithNullData() + { + //arrange/act + var actual = Result.Error("Something wrong happened."); //assert actual.ShouldNotBeNull(); - actual.Status.ShouldBe(ResultStatus.Error); actual.ErrorMessages.ShouldNotBeEmpty(); - actual.ErrorMessages.Count().ShouldBe(3); + actual.Status.ShouldBe(ResultStatus.Error); + actual.Data.ShouldBeNull(); + actual.IsSuccess.ShouldBeFalse(); } [Fact] - public void ResultAggregate_ShouldCorrectlyCombineAllResults_WhenThereAreValidationErrors() + public void Result_ShouldAggregateAllResults_WhenItIsCalledWithMultipleResults_AndIncludeValidationErrorsIsFalse() { //arrange - var result1 = Result.Invalid([new ValidationError("Validation error 1", "123", ValidationErrorType.Error)]); - var result2 = Result.Invalid([new ValidationError("Validation error 2", "1234", ValidationErrorType.Error)]); - var result3 = Result.Invalid([new ValidationError("Validation error 3", "1235", ValidationErrorType.Error)]); + var result1 = Result.Invalid([new ValidationError("Validation error Jan", "3434", ValidationErrorType.Error)]); + var result2 = Result.Invalid([new ValidationError("Validation error Feb", "5554", ValidationErrorType.Error)]); + var result3 = Result.Invalid([new ValidationError("Validation error March", "6678", ValidationErrorType.Error)]); + + var result4 = Result.Error("Failure Error 234"); + var result5 = Result.Error("Failure Error 2676"); + var result6 = Result.Error("Failure Error 3009"); //act - var actual = Result.Aggregate(result1, result2, result3); + var actual = Result.Aggregate(false, result1, result2, result3, result4, result5, result6); //assert actual.ShouldNotBeNull(); - actual.Status.ShouldBe(ResultStatus.Invalid); - actual.ValidationErrors.ShouldNotBeEmpty(); - actual.ValidationErrors.Count().ShouldBe(3); + actual.ShouldNotBeEmpty(); + actual.Count.ShouldBe(2); + + actual.ToList().Where(x => x.Status == ResultStatus.Error).ShouldNotBeEmpty(); + actual.ToList().Where(x => x.Status == ResultStatus.Invalid).ShouldNotBeEmpty(); + + foreach (var item in actual.ToList()) + { + item.Messages.ShouldNotBeEmpty(); + item.Messages.ShouldBeAssignableTo>(); + } } [Fact] - public void ResultAggregate_ShouldCorrectlyCombineAllResults_WhenThereAreValidationErrorsAndErrors() + public void Result_ShouldAggregateAllResults_WhenItIsCalledWithMultipleResults_AndIncludeValidationErrorsIsTrue() { //arrange - var result1 = Result.Invalid([new ValidationError("Validation error Jan", "3434", ValidationErrorType.Error)]); - var result2 = Result.Invalid([new ValidationError("Validation error Feb", "5554", ValidationErrorType.Error)]); - var result3 = Result.Invalid([new ValidationError("Validation error March", "6678", ValidationErrorType.Error)]); + var result1 = Result.Invalid([new ValidationError("Validation error April", "666", ValidationErrorType.Error)]); + var result2 = Result.Invalid([new ValidationError("Validation error May", "777", ValidationErrorType.Error)]); + var result3 = Result.Invalid([new ValidationError("Validation error June", "888", ValidationErrorType.Error)]); - var result4 = Result.Error("Error234"); - var result5 = Result.Error("Error2676"); - var result6 = Result.Error("Error3009"); + var result4 = Result.Error("Failure Error 999"); + var result5 = Result.Error("Failure Error 101010"); + var result6 = Result.Error("Failure Error 111111"); //act - var actual = Result.Aggregate(result1, result2, result3, result4, result5, result6); + var actual = Result.Aggregate(true, result1, result2, result3, result4, result5, result6); //assert actual.ShouldNotBeNull(); - actual.Status.ShouldBe(ResultStatus.Error); + actual.ShouldNotBeEmpty(); + actual.Count.ShouldBe(2); - actual.ErrorMessages.ShouldNotBeEmpty(); - actual.ErrorMessages.Count().ShouldBe(3); + actual.ToList().Where(x => x.Status == ResultStatus.Error).ShouldNotBeEmpty(); + actual.ToList().Where(x => x.Status == ResultStatus.Invalid).ShouldNotBeEmpty(); + + var validationErrors = actual.Where(x => x.Status == ResultStatus.Invalid) + .SelectMany(y => y.Messages.Select(x => x)).ToList().AsReadOnly(); + + validationErrors.ShouldNotBeEmpty(); + + foreach (var item in validationErrors) + { + item.ShouldBeAssignableTo(); + + var validationError = item as ValidationError; + validationError.ShouldNotBeNull(); + validationError.ValidationErrorType.ShouldBe(ValidationErrorType.Error); + validationError.ErrorMessage.ShouldNotBeEmpty(); + validationError.ErrorMessage.ShouldContain("Validation error"); + } + + var errors = actual.Where(x => x.Status == ResultStatus.Error) + .SelectMany(y => y.Messages.Select(x => x)).ToList().AsReadOnly(); - actual.ValidationErrors.ShouldNotBeEmpty(); - actual.ValidationErrors.Count().ShouldBe(3); + errors.ShouldNotBeEmpty(); + + foreach (var item in errors) + { + item.ShouldBeAssignableTo(); + + var errorMessage = item as string; + errorMessage.ShouldNotBeNull(); + } } + + [Fact] + public void ResultOfTypeString_ShouldAggregateAllResults_WhenItIsCalledWithMultipleResults_AndIncludeValidationErrorsIsFalse() + { + //arrange + var result1 = Result.Invalid([new ValidationError("Validation error April", "666", ValidationErrorType.Error)]); + var result2 = Result.Invalid([new ValidationError("Validation error May", "777", ValidationErrorType.Error)]); + + var result4 = Result.Error("Failure Error 999"); + var result5 = Result.Error("Failure Error 101010"); + + //act + var actual = Result.Aggregate(false, result1, result2, result4, result5); + + //assert + var actualList = actual.ToList(); + actualList.ShouldNotBeEmpty(); + actualList.Where(x => x.Status == ResultStatus.Error).ShouldNotBeEmpty(); + actualList.Where(x => x.Status == ResultStatus.Invalid).ShouldNotBeEmpty(); + foreach (var item in actualList) + { + item.TypeName.ShouldNotBeNull(); + item.TypeName.GetType().Name.ShouldBe("String"); + } + + var validationErrors = actual.Where(x => x.Status == ResultStatus.Invalid) + .SelectMany(y => y.Messages.Select(x => x)).ToList().AsReadOnly(); + + validationErrors.ShouldNotBeEmpty(); + + foreach (var item in validationErrors) + { + item.ShouldBeAssignableTo(); + + var validationError = item as string; + validationError.ShouldNotBeNull(); + validationError.ShouldContain("Validation error"); + } + + var errors = actual.Where(x => x.Status == ResultStatus.Error) + .SelectMany(y => y.Messages.Select(x => x)).ToList().AsReadOnly(); + + errors.ShouldNotBeEmpty(); + + foreach (var item in errors) + { + item.ShouldBeAssignableTo(); + + var errorMessage = item as string; + errorMessage.ShouldNotBeNull(); + } + } + [Fact] - public void ResultAggregate_ShouldReturnCorrectResultStatus_WhenAllResultsAreOK() + public void ResultOfComplexType_ShouldAggregateAllResults_WhenItIsCalledWithMultipleResults_AndIncludeValidationErrorsIsFalse() { //arrange - var result1 = Result.Success(); - var result2 = Result.Success(); - var result3 = Result.Success(); + var result1 = Result.Invalid([new ValidationError("Validation error June", "668787", ValidationErrorType.Error)]); + var result2 = Result.Invalid([new ValidationError("Validation error July", "4847", ValidationErrorType.Error)]); + + var result4 = Result.Error("Failure Error 89393"); + var result5 = Result.Error("Failure Error 111112"); //act - var actual = Result.Aggregate(result1, result2, result3); + var actual = Result.Aggregate(false, result1, result2, result4, result5); //assert - actual.ShouldNotBeNull(); - actual.Status.ShouldBe(ResultStatus.Ok); - actual.ErrorMessages.ShouldBeEmpty(); - actual.ValidationErrors.ShouldBeEmpty(); + var actualList = actual.ToList(); + actualList.ShouldNotBeEmpty(); + actualList.Where(x => x.Status == ResultStatus.Error).ShouldNotBeEmpty(); + actualList.Where(x => x.Status == ResultStatus.Invalid).ShouldNotBeEmpty(); + + foreach (var item in actualList) + { + item.TypeName.ShouldNotBeNull(); + item.TypeName.ShouldBe(nameof(DummyResponseModel)); + } + + var validationErrors = actual.Where(x => x.Status == ResultStatus.Invalid) + .SelectMany(y => y.Messages.Select(x => x)).ToList().AsReadOnly(); + + validationErrors.ShouldNotBeEmpty(); + + foreach (var item in validationErrors) + { + item.ShouldBeAssignableTo(); + + var validationError = item as string; + validationError.ShouldNotBeNull(); + validationError.ShouldContain("Validation error"); + } + + var errors = actual.Where(x => x.Status == ResultStatus.Error) + .SelectMany(y => y.Messages.Select(x => x)).ToList().AsReadOnly(); + + errors.ShouldNotBeEmpty(); + + foreach (var item in errors) + { + item.ShouldBeAssignableTo(); + + var errorMessage = item as string; + errorMessage.ShouldNotBeNull(); + } } } From a80f3aab0b35641ce463a6ce7a0bbb401cc3c3f1 Mon Sep 17 00:00:00 2001 From: Saeed Ghanavat Date: Wed, 16 Jul 2025 21:39:35 +0100 Subject: [PATCH 3/7] More unit tests for NotFound, Error, and Success results. --- .../ResultTests.cs | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/tests/Ghanavats.ResultPattern.Tests/ResultTests.cs b/tests/Ghanavats.ResultPattern.Tests/ResultTests.cs index 0ae9b20..5cbc6ef 100644 --- a/tests/Ghanavats.ResultPattern.Tests/ResultTests.cs +++ b/tests/Ghanavats.ResultPattern.Tests/ResultTests.cs @@ -29,6 +29,36 @@ public void ResultConstructorWithStatus_ShouldBeInitialised_WithCorrectValueAndS sut.Status.ShouldBe(ResultStatus.Ok); } + [Fact] + public void ResultSuccessWithDataAndSuccessMessage_ShouldCorrectlySetStatusWithData() + { + //arrange/act + var actual = Result.Success("SomeValue", "Successfully executed."); + + //assert + actual.ShouldNotBeNull(); + actual.ErrorMessages.ShouldBeEmpty(); + actual.Status.ShouldBe(ResultStatus.Ok); + actual.Data.ShouldNotBeNull(); + actual.Data.GetType().Name.ShouldBe("String"); + actual.IsSuccess.ShouldBeTrue(); + actual.SuccessMessage.ShouldBe("Successfully executed."); + } + + [Fact] + public void ResultSuccessWithData_ShouldCorrectlySetStatusWithData() + { + //arrange/act + var actual = Result.Success(1234); + + //assert + actual.ShouldNotBeNull(); + actual.ErrorMessages.ShouldBeEmpty(); + actual.Status.ShouldBe(ResultStatus.Ok); + actual.Data.ShouldBe(1234); + actual.IsSuccess.ShouldBeTrue(); + } + [Fact] public void ResultError_ShouldCorrectlySetStatusWithNullData() { @@ -43,6 +73,19 @@ public void ResultError_ShouldCorrectlySetStatusWithNullData() actual.IsSuccess.ShouldBeFalse(); } + [Fact] + public void ResultNotFound_ShouldCorrectlySetStatus() + { + //arrange/act + var actual = Result.NotFound(); + + //assert + actual.ShouldNotBeNull(); + actual.Status.ShouldBe(ResultStatus.NotFound); + actual.Data.ShouldBeNull(); + actual.IsSuccess.ShouldBeFalse(); + } + [Fact] public void Result_ShouldAggregateAllResults_WhenItIsCalledWithMultipleResults_AndIncludeValidationErrorsIsFalse() { From e449e5c2760678b1b638b9cda96d1babfc563020 Mon Sep 17 00:00:00 2001 From: Saeed Ghanavat Date: Sat, 19 Jul 2025 23:35:50 +0100 Subject: [PATCH 4/7] - Removed Aggregate on Result - Added mechanism to skip OK and NotFound statuses from Aggregate function - Added non-generic NotFound - Covered full unit tests --- .../FluentValidationFailureExtension.cs | 4 +- .../Extensions/ResultExtensions.cs | 36 +- .../Models/AggregateResultsModel.cs | 3 +- src/Ghanavats.ResultPattern/Result.Void.cs | 18 +- src/Ghanavats.ResultPattern/Result.cs | 17 +- .../DummyData/DummyFeatureService.cs | 34 ++ .../ResultTests.cs | 362 +++++++++++++----- 7 files changed, 345 insertions(+), 129 deletions(-) create mode 100644 tests/Ghanavats.ResultPattern.Tests/DummyData/DummyFeatureService.cs diff --git a/src/Ghanavats.ResultPattern/Extensions/FluentValidationFailureExtension.cs b/src/Ghanavats.ResultPattern/Extensions/FluentValidationFailureExtension.cs index 38356ad..a2122f7 100644 --- a/src/Ghanavats.ResultPattern/Extensions/FluentValidationFailureExtension.cs +++ b/src/Ghanavats.ResultPattern/Extensions/FluentValidationFailureExtension.cs @@ -13,6 +13,8 @@ public static IEnumerable PopulateValidationErrors(this Validat return input is null ? [] : input.Errors - .Select(x => new ValidationError(x.ErrorMessage, x.ErrorCode, (ValidationErrorType)x.Severity)); + .Select(validationFailure => new ValidationError(validationFailure.ErrorMessage, + validationFailure.ErrorCode, + (ValidationErrorType)validationFailure.Severity)); } } diff --git a/src/Ghanavats.ResultPattern/Extensions/ResultExtensions.cs b/src/Ghanavats.ResultPattern/Extensions/ResultExtensions.cs index 3fbc405..5b26eeb 100644 --- a/src/Ghanavats.ResultPattern/Extensions/ResultExtensions.cs +++ b/src/Ghanavats.ResultPattern/Extensions/ResultExtensions.cs @@ -4,35 +4,35 @@ namespace Ghanavats.ResultPattern.Extensions; public static class ResultExtensions { - public static IReadOnlyCollection GetMessages(this Result[] results, ResultStatus status, + internal static IReadOnlyCollection GetMessages(this Result[] results, ResultStatus status, bool includeValidationErrors = false) { switch (status) { case ResultStatus.Error: - return GetErrorMessages(); + return GetErrorMessages(); case ResultStatus.Invalid: - return includeValidationErrors - ? GetValidationErrors() - : GetValidationMessages(); - case ResultStatus.None: + return includeValidationErrors + ? GetValidationErrors() + : GetValidationMessages(); case ResultStatus.Ok: - return GetSuccessMessages(); + case ResultStatus.None: case ResultStatus.NotFound: + throw new NotSupportedException("Not supported result status."); default: - return []; + throw new InvalidOperationException("Operation not supported."); } - + IReadOnlyCollection GetErrorMessages() { var resultErrors = results - .Where(error => error.Status == ResultStatus.Error - && error.ErrorMessages.Any()) + .Where(result => result.Status == ResultStatus.Error + && result.ErrorMessages.Any()) .SelectMany(error => error.ErrorMessages).ToList(); - + return resultErrors; } - + IReadOnlyCollection GetValidationMessages() { return results @@ -41,7 +41,7 @@ IReadOnlyCollection GetErrorMessages() .SelectMany(invalidMessages => invalidMessages.ValidationErrors .Select(x => x.ErrorMessage)).ToList(); } - + IReadOnlyCollection GetValidationErrors() { return results @@ -49,13 +49,5 @@ IReadOnlyCollection GetValidationErrors() && invalids.Status == ResultStatus.Invalid) .SelectMany(invalidMessages => invalidMessages.ValidationErrors).ToList(); } - - IReadOnlyCollection GetSuccessMessages() - { - return results - .Where(x => x.Status == ResultStatus.Ok - && x.ErrorMessages.Any()) - .SelectMany(error => error.ErrorMessages).ToList(); - } } } diff --git a/src/Ghanavats.ResultPattern/Models/AggregateResultsModel.cs b/src/Ghanavats.ResultPattern/Models/AggregateResultsModel.cs index 27d0f1c..3d9875a 100644 --- a/src/Ghanavats.ResultPattern/Models/AggregateResultsModel.cs +++ b/src/Ghanavats.ResultPattern/Models/AggregateResultsModel.cs @@ -4,7 +4,6 @@ namespace Ghanavats.ResultPattern.Models; public class AggregateResultsModel { - public ResultStatus Status { get; init; } = ResultStatus.None; - public string? TypeName { get; init; } + public ResultStatus Status { get; init; } public IReadOnlyCollection Messages { get; init; } = []; } diff --git a/src/Ghanavats.ResultPattern/Result.Void.cs b/src/Ghanavats.ResultPattern/Result.Void.cs index 5220a98..3d6fa94 100644 --- a/src/Ghanavats.ResultPattern/Result.Void.cs +++ b/src/Ghanavats.ResultPattern/Result.Void.cs @@ -54,11 +54,25 @@ private Result(ResultStatus status) : base(status) ErrorMessages = [errorMessage] }; - public new static IReadOnlyCollection Aggregate(bool includeValidationErrors = false, - params Result[] results) + /// + /// Represents the not found result for non-generic scenarios + /// + /// Result with NotFound status + public new static Result NotFound() => new(ResultStatus.NotFound); + + /// + /// To gather all non-success results of type Result (non-generic) into a single object. + /// + /// Indicates whether a full ValidationError collection should be aggregated. + /// Default is false. + /// Array of all Results (non-generic) + /// A Read-Only Collection of Aggregate Results Model + public static IReadOnlyCollection Aggregate(bool includeValidationErrors = false, + params Result[] results) { return results .GroupBy(result => result.Status) + .Skip(results.Count(x => x.Status is ResultStatus.Ok or ResultStatus.NotFound)) .Select(whatIWant => new AggregateResultsModel { Status = whatIWant.Key, diff --git a/src/Ghanavats.ResultPattern/Result.cs b/src/Ghanavats.ResultPattern/Result.cs index 4e2a244..ac3fd86 100644 --- a/src/Ghanavats.ResultPattern/Result.cs +++ b/src/Ghanavats.ResultPattern/Result.cs @@ -1,8 +1,6 @@ using System.Runtime.CompilerServices; using System.Text.Json.Serialization; using Ghanavats.ResultPattern.Enums; -using Ghanavats.ResultPattern.Extensions; -using Ghanavats.ResultPattern.Models; [assembly: InternalsVisibleTo(assemblyName: "Ghanavats.ResultPattern.Tests")] @@ -141,22 +139,9 @@ public static Result Invalid(IEnumerable validationErrors) { return new Result(ResultStatus.Invalid) { ValidationErrors = validationErrors }; } - - public static IReadOnlyCollection Aggregate(bool includeValidationErrors = false, - params Result[] results) - { - return results - .GroupBy(result => result.Status) - .Select(whatIWant => new AggregateResultsModel - { - Status = whatIWant.Key, - TypeName = typeof(T).Name, - Messages = results.GetMessages(whatIWant.Key, includeValidationErrors) - }).ToList().AsReadOnly(); - } /// - /// An operator to automatically convert the return type in a method to the type being returned + /// An operator to automatically convert the generic T type to Result of type T /// /// The return data public static implicit operator Result(T data) => new(data); diff --git a/tests/Ghanavats.ResultPattern.Tests/DummyData/DummyFeatureService.cs b/tests/Ghanavats.ResultPattern.Tests/DummyData/DummyFeatureService.cs new file mode 100644 index 0000000..d3cebbc --- /dev/null +++ b/tests/Ghanavats.ResultPattern.Tests/DummyData/DummyFeatureService.cs @@ -0,0 +1,34 @@ +namespace Ghanavats.ResultPattern.Tests.DummyData; + +public class DummyFeatureService +{ + public Result DoSomething() + { + var response = new DummyResponseModel + { + DummyFieldOne = "Some data" + }; + + return response; + } + + public DummyResponseModel DoSomethingImplicitConversionOfResultGenericToInnerType() + { + var response = new DummyResponseModel + { + DummyFieldOne = "Test Field Data" + }; + + return Result.Success(response); + } + + public Result DoSomethingImplicitConversionOfNonGenericResultToGenericResultWithDefault() + { + _ = new DummyResponseModel + { + DummyFieldOne = "DataNotGoingToBeUsed" + }; + + return Result.Success(); + } +} diff --git a/tests/Ghanavats.ResultPattern.Tests/ResultTests.cs b/tests/Ghanavats.ResultPattern.Tests/ResultTests.cs index 5cbc6ef..e0ca21a 100644 --- a/tests/Ghanavats.ResultPattern.Tests/ResultTests.cs +++ b/tests/Ghanavats.ResultPattern.Tests/ResultTests.cs @@ -1,4 +1,7 @@ -using Ghanavats.ResultPattern.Enums; +using FluentValidation; +using FluentValidation.Results; +using Ghanavats.ResultPattern.Enums; +using Ghanavats.ResultPattern.Extensions; using Ghanavats.ResultPattern.Tests.DummyData; using Shouldly; @@ -59,6 +62,20 @@ public void ResultSuccessWithData_ShouldCorrectlySetStatusWithData() actual.IsSuccess.ShouldBeTrue(); } + [Fact] + public void ResultNonGenericSuccessWithDataAndSuccessMessage_ShouldCorrectlySetStatusWithData() + { + //arrange/act + var actual = Result.Success(); + + //assert + actual.ShouldNotBeNull(); + actual.ErrorMessages.ShouldBeEmpty(); + actual.Status.ShouldBe(ResultStatus.Ok); + actual.Data.ShouldBeNull(); + actual.IsSuccess.ShouldBeTrue(); + } + [Fact] public void ResultError_ShouldCorrectlySetStatusWithNullData() { @@ -73,6 +90,68 @@ public void ResultError_ShouldCorrectlySetStatusWithNullData() actual.IsSuccess.ShouldBeFalse(); } + [Fact] + public void ResultNonGenericInvalid_ShouldCorrectlySetStatusAndValidationErrors() + { + //arrange/act + var expectedValidationResult = new ValidationResult + { + Errors = [ + new ValidationFailure + { + ErrorMessage = "Test validation failure message.", + ErrorCode = "ErrorCodeTest", + PropertyName = "TestProperty", + Severity = Severity.Error + } + ] + }; + + var actual = Result.Invalid(expectedValidationResult.PopulateValidationErrors()); + + //assert + actual.ShouldNotBeNull(); + actual.ValidationErrors.ShouldNotBeEmpty(); + actual.ErrorMessages.ShouldBeEmpty(); + actual.Status.ShouldBe(ResultStatus.Invalid); + actual.Data.ShouldBeNull(); + actual.IsSuccess.ShouldBeFalse(); + } + + [Fact] + public void ResultInvalid_ShouldCorrectlySetStatusAndValidationErrors() + { + //arrange/act + var expectedValidationResult = new ValidationResult + { + Errors = [ + new ValidationFailure + { + ErrorMessage = "Test validation failure message.", + ErrorCode = "ErrorCodeTest", + PropertyName = "DummyFieldOne", + Severity = Severity.Error + } + ] + }; + + var actual = Result.Invalid(expectedValidationResult.PopulateValidationErrors()); + + //assert + actual.ShouldNotBeNull(); + actual.ValidationErrors.ShouldNotBeEmpty(); + actual.ErrorMessages.ShouldBeEmpty(); + actual.Status.ShouldBe(ResultStatus.Invalid); + actual.Data.ShouldBeNull(); + actual.IsSuccess.ShouldBeFalse(); + + foreach (var item in actual.ValidationErrors) + { + item.ErrorMessage.ShouldNotBeNullOrEmpty(); + item.ErrorCode.ShouldNotBeNullOrEmpty(); + } + } + [Fact] public void ResultNotFound_ShouldCorrectlySetStatus() { @@ -168,110 +247,221 @@ public void Result_ShouldAggregateAllResults_WhenItIsCalledWithMultipleResults_A errorMessage.ShouldNotBeNull(); } } - + [Fact] - public void ResultOfTypeString_ShouldAggregateAllResults_WhenItIsCalledWithMultipleResults_AndIncludeValidationErrorsIsFalse() + public void Result_ShouldIgnoreAggregatingNoneOkNotFoundStatuses() { //arrange - var result1 = Result.Invalid([new ValidationError("Validation error April", "666", ValidationErrorType.Error)]); - var result2 = Result.Invalid([new ValidationError("Validation error May", "777", ValidationErrorType.Error)]); - - var result4 = Result.Error("Failure Error 999"); - var result5 = Result.Error("Failure Error 101010"); + var result1 = Result.Success(); + var result2 = Result.NotFound(); //act - var actual = Result.Aggregate(false, result1, result2, result4, result5); + var action = Result.Aggregate(true, result1, result2); //assert - var actualList = actual.ToList(); - actualList.ShouldNotBeEmpty(); - actualList.Where(x => x.Status == ResultStatus.Error).ShouldNotBeEmpty(); - actualList.Where(x => x.Status == ResultStatus.Invalid).ShouldNotBeEmpty(); - - foreach (var item in actualList) - { - item.TypeName.ShouldNotBeNull(); - item.TypeName.GetType().Name.ShouldBe("String"); - } - - var validationErrors = actual.Where(x => x.Status == ResultStatus.Invalid) - .SelectMany(y => y.Messages.Select(x => x)).ToList().AsReadOnly(); - - validationErrors.ShouldNotBeEmpty(); - - foreach (var item in validationErrors) - { - item.ShouldBeAssignableTo(); - - var validationError = item as string; - validationError.ShouldNotBeNull(); - validationError.ShouldContain("Validation error"); - } - - var errors = actual.Where(x => x.Status == ResultStatus.Error) - .SelectMany(y => y.Messages.Select(x => x)).ToList().AsReadOnly(); - - errors.ShouldNotBeEmpty(); - - foreach (var item in errors) - { - item.ShouldBeAssignableTo(); - - var errorMessage = item as string; - errorMessage.ShouldNotBeNull(); - } + action.ShouldNotBeNull(); + action.ShouldBeEmpty(); } [Fact] - public void ResultOfComplexType_ShouldAggregateAllResults_WhenItIsCalledWithMultipleResults_AndIncludeValidationErrorsIsFalse() + public void Result_ShouldIgnoreAggregatingNoneOkNotFound_WhenMixedWithErrorStatusResults() { //arrange - var result1 = Result.Invalid([new ValidationError("Validation error June", "668787", ValidationErrorType.Error)]); - var result2 = Result.Invalid([new ValidationError("Validation error July", "4847", ValidationErrorType.Error)]); - - var result4 = Result.Error("Failure Error 89393"); - var result5 = Result.Error("Failure Error 111112"); + var result1 = Result.Success(); + var result2 = Result.NotFound(); + var result3 = Result.Error("Something went wrong"); //act - var actual = Result.Aggregate(false, result1, result2, result4, result5); + //Action action = () => Result.Aggregate(true, result1, result2, result3); + var actual = Result.Aggregate(true, result1, result2, result3); //assert - var actualList = actual.ToList(); - actualList.ShouldNotBeEmpty(); - actualList.Where(x => x.Status == ResultStatus.Error).ShouldNotBeEmpty(); - actualList.Where(x => x.Status == ResultStatus.Invalid).ShouldNotBeEmpty(); - - foreach (var item in actualList) - { - item.TypeName.ShouldNotBeNull(); - item.TypeName.ShouldBe(nameof(DummyResponseModel)); - } - - var validationErrors = actual.Where(x => x.Status == ResultStatus.Invalid) - .SelectMany(y => y.Messages.Select(x => x)).ToList().AsReadOnly(); + actual.ShouldNotBeNull(); + actual.ShouldNotBeEmpty(); + actual.Count.ShouldBe(1); + actual.ToList()[0].Status.ShouldBe(ResultStatus.Error); + } + + [Fact] + public void Result_ImplicitOperator_ShouldCorrectlyConvertTheDataTypeToResultOfTypeImplicitly() + { + //arrange + var sut = new DummyFeatureService(); - validationErrors.ShouldNotBeEmpty(); + //act + var actual = sut.DoSomething(); - foreach (var item in validationErrors) - { - item.ShouldBeAssignableTo(); - - var validationError = item as string; - validationError.ShouldNotBeNull(); - validationError.ShouldContain("Validation error"); - } - - var errors = actual.Where(x => x.Status == ResultStatus.Error) - .SelectMany(y => y.Messages.Select(x => x)).ToList().AsReadOnly(); + //assert + actual.ShouldNotBeNull(); + actual.ShouldBeOfType>(); + actual.IsSuccess.ShouldBeTrue(); + actual.Data.ShouldNotBeNull(); + } + + [Fact] + public void Result_ImplicitOperator_ShouldCorrectlyConvertTheResultOfGenericTypeToTheInnerType() + { + //arrange + var sut = new DummyFeatureService(); - errors.ShouldNotBeEmpty(); + //act + var actual = sut.DoSomethingImplicitConversionOfResultGenericToInnerType(); + + //assert + actual.ShouldNotBeNull(); + actual.ShouldBeOfType(); + } + + [Fact] + public void Result_ImplicitOperator_ShouldCorrectlyConvertNonGenericResultToGenericResultWithDefaultState() + { + //arrange + var sut = new DummyFeatureService(); - foreach (var item in errors) - { - item.ShouldBeAssignableTo(); - - var errorMessage = item as string; - errorMessage.ShouldNotBeNull(); - } + //act + var actual = sut.DoSomethingImplicitConversionOfNonGenericResultToGenericResultWithDefault(); + + //assert + actual.ShouldNotBeNull(); + actual.ShouldBeOfType>(); + actual.IsSuccess.ShouldBeTrue(); + actual.Data.ShouldBeNull(); + actual.ErrorMessages.ShouldBeEmpty(); + actual.ValidationErrors.ShouldBeEmpty(); + actual.SuccessMessage.ShouldBeEmpty(); + actual.Status.ShouldBe(ResultStatus.Ok); } + + // [Fact] + // public void ResultOfTypeString_ShouldAggregateAllResults_WhenItIsCalledWithMultipleResults_AndIncludeValidationErrorsIsFalse() + // { + // //arrange + // var result1 = Result.Invalid([new ValidationError("Validation error April", "666", ValidationErrorType.Error)]); + // var result2 = Result.Invalid([new ValidationError("Validation error May", "777", ValidationErrorType.Error)]); + // + // var result4 = Result.Error("Failure Error 999"); + // var result5 = Result.Error("Failure Error 101010"); + // + // //act + // var actual = Result.Aggregate(false, result1, result2, result4, result5); + // + // //assert + // var actualList = actual.ToList(); + // actualList.ShouldNotBeEmpty(); + // actualList.Where(x => x.Status == ResultStatus.Error).ShouldNotBeEmpty(); + // actualList.Where(x => x.Status == ResultStatus.Invalid).ShouldNotBeEmpty(); + // + // foreach (var item in actualList) + // { + // item.TypeName.ShouldNotBeNull(); + // item.TypeName.GetType().Name.ShouldBe("String"); + // } + // + // var validationErrors = actual.Where(x => x.Status == ResultStatus.Invalid) + // .SelectMany(y => y.Messages.Select(x => x)).ToList().AsReadOnly(); + // + // validationErrors.ShouldNotBeEmpty(); + // + // foreach (var item in validationErrors) + // { + // item.ShouldBeAssignableTo(); + // + // var validationError = item as string; + // validationError.ShouldNotBeNull(); + // validationError.ShouldContain("Validation error"); + // } + // + // var errors = actual.Where(x => x.Status == ResultStatus.Error) + // .SelectMany(y => y.Messages.Select(x => x)).ToList().AsReadOnly(); + // + // errors.ShouldNotBeEmpty(); + // + // foreach (var item in errors) + // { + // item.ShouldBeAssignableTo(); + // + // var errorMessage = item as string; + // errorMessage.ShouldNotBeNull(); + // } + // } + // + // [Fact] + // public void ResultOfComplexType_ShouldAggregateAllResults_WhenItIsCalledWithMultipleResults_AndIncludeValidationErrorsIsFalse() + // { + // //arrange + // var result1 = Result.Invalid([new ValidationError("Validation error June", "668787", ValidationErrorType.Error)]); + // var result2 = Result.Invalid([new ValidationError("Validation error July", "4847", ValidationErrorType.Error)]); + // + // var result4 = Result.Error("Failure Error 89393"); + // var result5 = Result.Error("Failure Error 111112"); + // + // //act + // var actual = Result.Aggregate(false, result1, result2, result4, result5); + // + // //assert + // var actualList = actual.ToList(); + // actualList.ShouldNotBeEmpty(); + // actualList.Where(x => x.Status == ResultStatus.Error).ShouldNotBeEmpty(); + // actualList.Where(x => x.Status == ResultStatus.Invalid).ShouldNotBeEmpty(); + // + // foreach (var item in actualList) + // { + // item.TypeName.ShouldNotBeNull(); + // item.TypeName.ShouldBe(nameof(DummyResponseModel)); + // } + // + // var validationErrors = actual.Where(x => x.Status == ResultStatus.Invalid) + // .SelectMany(y => y.Messages.Select(x => x)).ToList().AsReadOnly(); + // + // validationErrors.ShouldNotBeEmpty(); + // + // foreach (var item in validationErrors) + // { + // item.ShouldBeAssignableTo(); + // + // var validationError = item as string; + // validationError.ShouldNotBeNull(); + // validationError.ShouldContain("Validation error"); + // } + // + // var errors = actual.Where(x => x.Status == ResultStatus.Error) + // .SelectMany(y => y.Messages.Select(x => x)).ToList().AsReadOnly(); + // + // errors.ShouldNotBeEmpty(); + // + // foreach (var item in errors) + // { + // item.ShouldBeAssignableTo(); + // + // var errorMessage = item as string; + // errorMessage.ShouldNotBeNull(); + // } + // } + + // [Fact] + // public void Result_ShouldAggregateAllSuccessResults() + // { + // //arrange + // var result1 = Result.Success(); + // var result2 = Result.Success(new DummyResponseModel + // { + // DummyFieldOne = "SomeData" + // }, + // "Success message two."); + // + // //act + // var actual = Result.Aggregate(false, result1, result2); + // + // //assert + // var actualList = actual.ToList(); + // actualList.ShouldNotBeEmpty(); + // actualList.Where(x => x.Status == ResultStatus.Ok).ShouldNotBeEmpty(); + // + // foreach (var item in actualList) + // { + // item.TypeName.ShouldNotBeNull(); + // item.TypeName.ShouldBe(nameof(DummyResponseModel)); + // item.Messages.ShouldNotBeEmpty(); + // } + // } } From 0191cf7a8b174365db55de5bd872417bfd23cf2a Mon Sep 17 00:00:00 2001 From: Saeed Ghanavat Date: Sat, 19 Jul 2025 23:37:22 +0100 Subject: [PATCH 5/7] Bumped the version to 1.1.0 --- src/Ghanavats.ResultPattern/Ghanavats.ResultPattern.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ghanavats.ResultPattern/Ghanavats.ResultPattern.csproj b/src/Ghanavats.ResultPattern/Ghanavats.ResultPattern.csproj index 587de89..ef04473 100644 --- a/src/Ghanavats.ResultPattern/Ghanavats.ResultPattern.csproj +++ b/src/Ghanavats.ResultPattern/Ghanavats.ResultPattern.csproj @@ -12,7 +12,7 @@ result-pattern git https://github.com/ghanavat/ResultPattern - 1.0.2 + 1.1.0 README.md LICENSE From d091d55999f77b57d5043da8dcfa7560e4e8cef4 Mon Sep 17 00:00:00 2001 From: Saeed Ghanavat Date: Sat, 19 Jul 2025 23:46:28 +0100 Subject: [PATCH 6/7] Fixed path to test project --- .github/workflows/continues_integration.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/continues_integration.yml b/.github/workflows/continues_integration.yml index ace20ba..ef274f8 100644 --- a/.github/workflows/continues_integration.yml +++ b/.github/workflows/continues_integration.yml @@ -26,4 +26,4 @@ jobs: - name: Build run: dotnet build Ghanavats.ResultPattern.sln --configuration Release - name: Test - run: dotnet test --no-build --verbosity normal + run: dotnet test tests/Ghanavats.ResultPattern.Tests/Ghanavats.ResultPattern.Tests.csproj From 2dcdf88f7a382713e160945455a54795ef89a8cc Mon Sep 17 00:00:00 2001 From: Saeed Ghanavat Date: Sun, 20 Jul 2025 00:20:37 +0100 Subject: [PATCH 7/7] Updated all NuGet packages --- Directory.Packages.props | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 35be281..cc32700 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,11 +1,17 @@ - + - - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + - - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + \ No newline at end of file