Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/continues_integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
13 changes: 12 additions & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
<Project>
<ItemGroup>
<PackageVersion Include="FluentValidation" Version="11.11.0" />
<PackageVersion Include="FluentValidation" Version="12.0.0" />
<PackageVersion Include="NuGet.Versioning" Version="6.12.1" />
<PackageVersion Include="coverlet.collector" Version="6.0.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageVersion>
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageVersion Include="Shouldly" Version="4.3.0" />
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageVersion>
</ItemGroup>
</Project>
7 changes: 7 additions & 0 deletions Ghanavats.ResultPattern.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ public static class FluentValidationFailureExtension
{
public static IEnumerable<ValidationError> 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(validationFailure => new ValidationError(validationFailure.ErrorMessage,
validationFailure.ErrorCode,
(ValidationErrorType)validationFailure.Severity));
}
}
53 changes: 53 additions & 0 deletions src/Ghanavats.ResultPattern/Extensions/ResultExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using Ghanavats.ResultPattern.Enums;

namespace Ghanavats.ResultPattern.Extensions;

public static class ResultExtensions
{
internal static IReadOnlyCollection<object?> 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.Ok:
case ResultStatus.None:
case ResultStatus.NotFound:
throw new NotSupportedException("Not supported result status.");
default:
throw new InvalidOperationException("Operation not supported.");
}

IReadOnlyCollection<string> GetErrorMessages()
{
var resultErrors = results
.Where(result => result.Status == ResultStatus.Error
&& result.ErrorMessages.Any())
.SelectMany(error => error.ErrorMessages).ToList();

return resultErrors;
}

IReadOnlyCollection<string?> GetValidationMessages()
{
return results
.Where(invalids => invalids.ValidationErrors.Any()
&& invalids.Status == ResultStatus.Invalid)
.SelectMany(invalidMessages => invalidMessages.ValidationErrors
.Select(x => x.ErrorMessage)).ToList();
}

IReadOnlyCollection<ValidationError> GetValidationErrors()
{
return results
.Where(invalids => invalids.ValidationErrors.Any()
&& invalids.Status == ResultStatus.Invalid)
.SelectMany(invalidMessages => invalidMessages.ValidationErrors).ToList();
}
}
}
2 changes: 1 addition & 1 deletion src/Ghanavats.ResultPattern/Ghanavats.ResultPattern.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
<PackageTags>result-pattern</PackageTags>
<RepositoryType>git</RepositoryType>
<RepositoryUrl>https://github.com/ghanavat/ResultPattern</RepositoryUrl>
<Version>1.0.2</Version>
<Version>1.1.0</Version>

<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
Expand Down
9 changes: 9 additions & 0 deletions src/Ghanavats.ResultPattern/Models/AggregateResultsModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using Ghanavats.ResultPattern.Enums;

namespace Ghanavats.ResultPattern.Models;

public class AggregateResultsModel
{
public ResultStatus Status { get; init; }
public IReadOnlyCollection<object?> Messages { get; init; } = [];
}
38 changes: 35 additions & 3 deletions src/Ghanavats.ResultPattern/Result.Void.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using Ghanavats.ResultPattern.Enums;
using Ghanavats.ResultPattern.Extensions;
using Ghanavats.ResultPattern.Models;

namespace Ghanavats.ResultPattern;

Expand All @@ -10,15 +12,19 @@ public class Result : Result<Result>
{
/// <summary>
/// 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.
/// </summary>
private Result() { }
private Result()
{
}

/// <summary>
/// A constructor that accepts <paramref name="status"/>
/// </summary>
/// <param name="status"></param>
private Result(ResultStatus status) : base(status) { }
private Result(ResultStatus status) : base(status)
{
}

/// <summary>
/// Represents a successful operation without a return type
Expand Down Expand Up @@ -47,4 +53,30 @@ private Result(ResultStatus status) : base(status) { }
{
ErrorMessages = [errorMessage]
};

/// <summary>
/// Represents the not found result for non-generic scenarios
/// </summary>
/// <returns>Result with NotFound status</returns>
public new static Result NotFound() => new(ResultStatus.NotFound);

/// <summary>
/// To gather all non-success results of type Result (non-generic) into a single object.
/// </summary>
/// <param name="includeValidationErrors">Indicates whether a full ValidationError collection should be aggregated.
/// Default is false.</param>
/// <param name="results">Array of all Results (non-generic)</param>
/// <returns>A Read-Only Collection of Aggregate Results Model</returns>
public static IReadOnlyCollection<AggregateResultsModel> 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,
Messages = results.GetMessages(whatIWant.Key, includeValidationErrors)
}).ToList().AsReadOnly();
}
}
36 changes: 20 additions & 16 deletions src/Ghanavats.ResultPattern/Result.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
Expand All @@ -10,6 +13,15 @@ namespace Ghanavats.ResultPattern;
/// <typeparam name="T"></typeparam>
public class Result<T>
{
/// <summary>
/// A constructor that accepts <paramref name="data"/>.
/// </summary>
/// <param name="data">Constructor parameter of type <paramref name="data"/></param>
public Result(T data)
{
Data = data;
}

/// <summary>
/// A constructor that accepts <paramref name="status"/>.
/// It is used internally in this class and Result.Void
Expand All @@ -19,33 +31,25 @@ internal Result(ResultStatus status)
{
Status = status;
}

/// <summary>
/// Default protected constructor.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
protected internal Result() { }

/// <summary>
/// A constructor that accepts <paramref name="data"/>.
/// It is used internally by this class.
/// </summary>
/// <param name="data">Constructor parameter of type <paramref name="data"/></param>
private Result(T data)
{
Data = data;
}

/// <summary>
/// A constructor that accepts <paramref name="data"/>.
/// It is used by the 'Success' method when a Success Message needed to be set.
/// </summary>
/// <param name="data">Constructor parameter of type <paramref name="data"/></param>
/// <param name="successMessage">Constructor parameter of type <paramref name="successMessage"/></param>
private Result(T data, string successMessage) : this(data)
internal Result(T data, string successMessage)
{
Data = data;
SuccessMessage = successMessage;
}

Expand All @@ -62,7 +66,7 @@ private Result(T data, string successMessage) : this(data)
public T? Data { get; set; }

/// <summary>
/// Use this property to accurately determine the exact status of the Result
/// Use this property to accurately determine the exact Result status
/// </summary>
[JsonInclude]
public ResultStatus Status { get; protected set; } = ResultStatus.Ok;
Expand Down Expand Up @@ -137,7 +141,7 @@ public static Result<T> Invalid(IEnumerable<ValidationError> validationErrors)
}

/// <summary>
/// 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
/// </summary>
/// <param name="data">The return data</param>
public static implicit operator Result<T>(T data) => new(data);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
namespace Ghanavats.ResultPattern.Tests.DummyData;

public class DummyFeatureService
{
public Result<DummyResponseModel> DoSomething()
{
var response = new DummyResponseModel
{
DummyFieldOne = "Some data"
};

return response;
}

public DummyResponseModel DoSomethingImplicitConversionOfResultGenericToInnerType()
{
var response = new DummyResponseModel
{
DummyFieldOne = "Test Field Data"
};

return Result<DummyResponseModel>.Success(response);
}

public Result<DummyResponseModel> DoSomethingImplicitConversionOfNonGenericResultToGenericResultWithDefault()
{
_ = new DummyResponseModel
{
DummyFieldOne = "DataNotGoingToBeUsed"
};

return Result.Success();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Ghanavats.ResultPattern.Tests.DummyData;

public class DummyResponseModel
{
public string DummyFieldOne { get; set; } = string.Empty;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="coverlet.collector"/>
<PackageReference Include="Microsoft.NET.Test.Sdk"/>
<PackageReference Include="Shouldly" />
<PackageReference Include="xunit"/>
<PackageReference Include="xunit.runner.visualstudio"/>
</ItemGroup>

<ItemGroup>
<Using Include="Xunit"/>
<AssemblyAttribute Include="System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverageAttribute" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Ghanavats.ResultPattern\Ghanavats.ResultPattern.csproj" />
</ItemGroup>

</Project>
Loading