Skip to content

Commit

Permalink
Merge pull request #58 from AKlaus/v3.1
Browse files Browse the repository at this point in the history
  • Loading branch information
AKlaus authored Jan 1, 2023
2 parents b80818a + 2ab8437 commit f506818
Show file tree
Hide file tree
Showing 22 changed files with 195 additions and 97 deletions.
8 changes: 7 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ jobs:
uses: actions/setup-dotnet@v1
with:
dotnet-version: 6.x
- name: Setup .NET 7
uses: actions/setup-dotnet@v1
with:
dotnet-version: 7.x

- name: Install dependencies
run: dotnet restore
Expand All @@ -38,6 +42,8 @@ jobs:

# Test each targeted framework independently to spot errors faster
# Generate coverage report for the latest framework only, as coverage on multiple is not supported
- name: Test on .NET 7
run: dotnet test --no-restore --verbosity normal --framework net7.0 /p:CollectCoverage=true /p:CoverletOutput=TestResults/ /p:CoverletOutputFormat=lcov
- name: Test on .NET 6
run: dotnet test --no-restore --verbosity normal --framework net6.0 /p:CollectCoverage=true /p:CoverletOutput=TestResults/ /p:CoverletOutputFormat=lcov
- name: Test on .NET 5
Expand All @@ -49,4 +55,4 @@ jobs:
uses: coverallsapp/github-action@master
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
path-to-lcov: ./tests/DomainResults.Tests/TestResults/coverage.net6.0.info
path-to-lcov: ./tests/DomainResults.Tests/TestResults/coverage.net7.0.info
4 changes: 4 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ jobs:
uses: actions/setup-dotnet@v1
with:
dotnet-version: 6.x
- name: Setup .NET 7
uses: actions/setup-dotnet@v1
with:
dotnet-version: 7.x

- name: Install dependencies
run: dotnet restore
Expand Down
21 changes: 19 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ Two tiny NuGet packages addressing challenges in the [ASP.NET Web API](https://d
- [Basic use-case](#basic-use-case)
- [Quick start](#quick-start)
- ['DomainResult.Common' package. Returning result from Domain Layer method](#domainresultcommon-package-returning-result-from-domain-layer-method)
- [Examples (Domain)](#examples-domain)
- [Examples (Domain layer)](#examples-domain-layer)
- [Type conversion](#type-conversion)
- ['DomainResult' package](#domainresult-package)
- [Conversion to IActionResult](#conversion-to-iactionresult)
- [Examples (IActionResult conversion)](#examples-iactionresult-conversion)
Expand Down Expand Up @@ -154,7 +155,7 @@ It has **50+ static extension methods** to return a successful or unsuccessful r
| `IDomainResult<T>` | `Task<IDomainResult<T>>` |
| `(T, IDomainResult)` | `Task<(T, IDomainResult)>` |

### Examples (Domain):
### Examples (Domain layer):

```cs
// Successful result with no value
Expand Down Expand Up @@ -184,6 +185,22 @@ _Notes_:
- The `Task` suffix on the extension methods indicates that the returned type is wrapped in a `Task` (e.g. `SuccessTask()`, `FailedTask()`, `NotFoundTask()`, `UnauthorizedTask()`).
- The `Failed()` and `NotFound()` methods take as input parameters: `string`, `string[]`. `Failed()` can also take [ValidationResult](https://docs.microsoft.com/en-us/dotnet/api/system.componentmodel.dataannotations.validationresult).

### Type conversion
Type conversion comes in handy for propagating errors from nested method calls, e.g. from `IDomainResult` to `IDomainResult<T>`, or the other way around, etc.

```cs
IDomainResult failedResult = IDomainResult.Failed("Ahh!");

IDomainResult<int> resOfInt = failedResult.To<int>(); // from IDomainResult to IDomainResult<T>
IDomainResult<long> resOfLong = resOfInt.To<long>(); // from IDomainResult<T> to IDomainResult<V>
DomainResult<int> resFromTuple = (default, failedResult); // from IDomainResult to DomainResult<T>
Task<IDomainResult> failedResultTask = IDomainResult.FailedTask("Ahh!");
Task<IDomainResult<int>> resOfInt = failedResultTask.To<int>(); // from Task<IDomainResult> to Task<IDomainResult<T>>
```
Note that returning [Tuple](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/value-tuples) types drastically simplifies type conversions.

## 'DomainResult' package

**Converts a `IDomainResult`-based object to various `IActionResult` and `IResult`-based types providing 40+ static extension methods.**
Expand Down
2 changes: 1 addition & 1 deletion samples/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" />

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net7.0</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<Features>strict</Features>
Expand Down
2 changes: 1 addition & 1 deletion samples/WebApi/Examples.WebApi.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.3.1" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
</ItemGroup>

<ItemGroup>
Expand Down
2 changes: 1 addition & 1 deletion samples/WebApiMinimal/Examples.WebApiMinimal.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="NSwag.AspNetCore" Version="13.16.1" />
<PackageReference Include="NSwag.AspNetCore" Version="13.18.2" />
</ItemGroup>
</Project>
3 changes: 1 addition & 2 deletions src/Common/DomainResultConversionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@ public static IDomainResult<T> To<T>(this IDomainResult domainResult)
}

/// <summary>
/// Convert to a <see cref="Task" /> of <see cref="IDomainResult{T}" /> (the domain operation result with a returned
/// value)
/// Convert to a <see cref="Task" /> of <see cref="IDomainResult{T}" /> (the domain operation result with a returned value)
/// </summary>
/// <typeparam name="T"> Value type of returned by the domain operation </typeparam>
public static async Task<IDomainResult<T>> To<T>(this Task<IDomainResult> domainResult)
Expand Down
8 changes: 7 additions & 1 deletion src/Common/DomainResultOfT.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,13 @@ public bool TryGetValue([MaybeNullWhen(false)] out TValue value)
/// Implicitly converts the specified <paramref name="value"/> to an <see cref="DomainResult{TValue}"/>
/// </summary>
/// <param name="value"> The parameter for conversion </param>
public static implicit operator DomainResult<TValue>(TValue value) => new DomainResult<TValue>(value);
public static implicit operator DomainResult<TValue>(TValue value) => new (value);

/// <summary>
/// Implicitly converts a <code>(TValue value, IDomainResult domainResult)</code> tuple to an <see cref="DomainResult{TValue}"/>
/// </summary>
/// <param name="domainResultWithValue"> The value and domain operation result for conversion </param>
public static implicit operator DomainResult<TValue>((TValue value, IDomainResult domainResult) domainResultWithValue) => new (domainResultWithValue.value, domainResultWithValue.domainResult);

// TODO: Consider to deprecate the extension methods in this class (below) in favour of ones in 'DomainResult'

Expand Down
2 changes: 1 addition & 1 deletion src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
<ItemGroup>
<!-- SourceLink settings (source debugging experiences) -->
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
<PackageReference Include="MinVer" Version="4.0.0" PrivateAssets="All" />
<PackageReference Include="MinVer" Version="4.2.0" PrivateAssets="All" />
<!-- Reference README file for nuget.org (https://devblogs.microsoft.com/nuget/add-a-readme-to-your-nuget-package/)-->
<None Include="./../../README.md" Pack="true" PackagePath="\" />
</ItemGroup>
Expand Down
2 changes: 1 addition & 1 deletion src/Mvc/DomainResults.Mvc.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ To be used in Web API projects to accompany 'DomainResult.Common' package used i
</Description>

<!-- Can't target .NET Standard as the Microsoft.AspNetCore.XXX dependency is .NET Core specific. Hence target each main version independently. -->
<TargetFrameworks>netcoreapp3.1;net5.0;net6.0</TargetFrameworks>
<TargetFrameworks>netcoreapp3.1;net5.0;net6.0;net7.0</TargetFrameworks>

<AssemblyName>DomainResults.Mvc</AssemblyName>
<RootNamespace>DomainResults.Mvc</RootNamespace>
Expand Down
3 changes: 2 additions & 1 deletion src/Mvc/IResult/DomainResultTo200OkResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Http;

// ReSharper disable once CheckNamespace
// ReSharper disable InconsistentNaming

namespace DomainResults.Mvc;

public static partial class DomainResultExtensions
Expand Down
3 changes: 2 additions & 1 deletion src/Mvc/IResult/DomainResultTo204NoContentResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Http;

// ReSharper disable once CheckNamespace
// ReSharper disable InconsistentNaming

namespace DomainResults.Mvc;

public static partial class DomainResultExtensions
Expand Down
12 changes: 8 additions & 4 deletions src/Mvc/IResult/DomainResultToCustomResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Http;

// ReSharper disable once CheckNamespace
// ReSharper disable InconsistentNaming

namespace DomainResults.Mvc;

public static partial class DomainResultExtensions
Expand All @@ -30,7 +31,10 @@ public static IResult ToCustomResult<V, R, TResult>(this (V, R) domainResult,
Action<ProblemDetails, R>? errorAction = null)
where R : IDomainResultBase
where TResult : IResult
=> ToResult(domainResult.Item1, domainResult.Item2, errorAction, valueToResultFunc);
{
var (value, res) = domainResult;
return ToResult(value, res, errorAction, valueToResultFunc);
}

/// <summary>
/// Custom conversion of successful and unsuccessful domain results to <see cref="IResult"/> types
Expand All @@ -47,8 +51,8 @@ public static async Task<IResult> ToCustomResult<V, R, TResult>(this Task<(V, R)
where R : IDomainResultBase
where TResult : IResult
{
var domainResult = await domainResultTask;
return ToResult(domainResult.Item1, domainResult.Item2, errorAction, valueToResultFunc);
var (value, res) = await domainResultTask;
return ToResult(value, res, errorAction, valueToResultFunc);
}

/// <summary>
Expand Down
5 changes: 3 additions & 2 deletions src/Mvc/IResult/DomainResultToResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Http;

// ReSharper disable once CheckNamespace
// ReSharper disable InconsistentNaming
namespace DomainResults.Mvc;

//
Expand Down Expand Up @@ -44,7 +44,7 @@ private static IResult ToResult<V, R, TResult>([AllowNull] V value,
DomainOperationStatus.CriticalDependencyError
=> SadResult(HttpCodeConvention.CriticalDependencyErrorHttpCode, HttpCodeConvention.CriticalDependencyErrorProblemDetailsTitle, errorDetails, errorAction),
DomainOperationStatus.Success => EqualityComparer<V>.Default.Equals(value!, default!)
? Results.NoContent() // No value, means returning HTTP status 204
? Results.NoContent() // No value, means returning HTTP status 204. For .NET 7 `Results.NoContent` will return `TypedResults.NoContent`
: valueToResultFunc(value),
_ => throw new ArgumentOutOfRangeException(),
};
Expand All @@ -65,6 +65,7 @@ private static IResult SadResult<R>(int statusCode, string title, R? errorDetail
};
errorAction?.Invoke(problemDetails, errorDetails!);

// For .NET 7 `Results.Problem` will return `TypedResults.Problem`
return Results.Problem(
problemDetails.Detail,
null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,18 @@ public async Task Errored_IDomainResultOfT_Task_Converts_To_IDomainResultOfV_Tas
Assert.Equal("Bla", domainResultOfT.Errors.Single());
}
#endregion

#region Tests of converting (TValue, IDomainResult) tuple to IDomainResult<T>

[Fact]
public void IDomainResult_and_Value_Tuple_Implicitly_Converted_To_IDomainResultOfT()
{
var domainResult = DomainResult.Failed("Bla");
DomainResult<int> domainResultOfT = (default, domainResult);

Assert.False(domainResultOfT.IsSuccess);
Assert.Equal("Bla", domainResultOfT.Errors.Single());
Assert.Equal(0, domainResultOfT.Value);
}
#endregion
}
12 changes: 6 additions & 6 deletions tests/DomainResults.Tests/DomainResults.Tests.csproj
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>netcoreapp3.1;net5.0;net6.0</TargetFrameworks>
<TargetFrameworks>netcoreapp3.1;net5.0;net6.0;net7.0</TargetFrameworks>

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

<ItemGroup>
<PackageReference Include="coverlet.msbuild" Version="3.1.2">
<PackageReference Include="coverlet.msbuild" Version="3.2.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<PackageReference Include="coverlet.collector" Version="3.2.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.1.2">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
Expand Down
12 changes: 4 additions & 8 deletions tests/DomainResults.Tests/Mvc/ActionResultConventionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,7 @@ public void FailedHttpCode_Is_Honoured_in_Failed_Response_Test(int? failedHttpCo
Assert.Equal(expectedFailedHttpCode, (actionRes.Value as ProblemDetails)!.Status);
#if NET6_0_OR_GREATER
// for the IResult (minimal API) conversion
res.AssertObjectResultType();
Assert.Equal(expectedFailedHttpCode, res.GetProblemDetails()!.Status);
res.AssertObjectResultTypeWithProblemDetails(expectedFailedHttpCode);
#endif

HttpCodeConvention.FailedHttpCode = defaultValue;
Expand Down Expand Up @@ -72,8 +71,7 @@ public void FailedProblemDetailsTitle_Is_Honoured_in_Error_Response_Test(string
Assert.Equal(expectedFailedTitle, problemDetails!.Title);
#if NET6_0_OR_GREATER
// for the IResult (minimal API) conversion
res.AssertObjectResultType();
Assert.Equal(expectedFailedTitle, res.GetProblemDetails()!.Title);
res.AssertObjectResultTypeWithProblemDetails(HttpCodeConvention.FailedHttpCode, expectedFailedTitle);
#endif

HttpCodeConvention.FailedProblemDetailsTitle = defaultValue;
Expand Down Expand Up @@ -105,8 +103,7 @@ public void NotFoundHttpCode_Is_Honoured_in_NotFound_Response_Test(int? notFound
Assert.Equal(expectedNotFoundHttpCode, (actionRes.Value as ProblemDetails)!.Status);
#if NET6_0_OR_GREATER
// for the IResult (minimal API) conversion
res.AssertObjectResultType();
Assert.Equal(expectedNotFoundHttpCode, res.GetProblemDetails()!.Status);
res.AssertObjectResultTypeWithProblemDetails(expectedNotFoundHttpCode);
#endif

HttpCodeConvention.NotFoundHttpCode = defaultValue;
Expand Down Expand Up @@ -137,8 +134,7 @@ public void NotFoundHttpDetailsTitle_Is_Honoured_in_NotFound_Response_Test(strin
Assert.Equal(expectedNotFoundTitle, problemDetails!.Title);
#if NET6_0_OR_GREATER
// for the IResult (minimal API) conversion
res.AssertObjectResultType();
Assert.Equal(expectedNotFoundTitle, res.GetProblemDetails()!.Title);
res.AssertObjectResultTypeWithProblemDetails(HttpCodeConvention.NotFoundHttpCode, expectedNotFoundTitle);
#endif

HttpCodeConvention.NotFoundProblemDetailsTitle = defaultValue;
Expand Down
Loading

0 comments on commit f506818

Please sign in to comment.