Skip to content

Commit

Permalink
Merge pull request #61 from AKlaus/#53_NET7
Browse files Browse the repository at this point in the history
  • Loading branch information
AKlaus authored Dec 28, 2022
2 parents c310798 + c87e50e commit 2ab8437
Show file tree
Hide file tree
Showing 15 changed files with 146 additions and 84 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
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 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
2 changes: 1 addition & 1 deletion tests/DomainResults.Tests/DomainResults.Tests.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<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>
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
125 changes: 98 additions & 27 deletions tests/DomainResults.Tests/Mvc/ResultExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,102 @@ namespace DomainResults.Tests.Mvc;
public static class ResultExtension
{
/// <summary>
/// Extracts <see cref="ProblemDetails"/> from <see cref="IResult"/>
/// Assert that the <see cref="IResult"/> instance represent an internal 'ObjectResult' class with a 'ProblemDetails'
/// </summary>
public static void AssertObjectResultTypeWithProblemDetails(this IResult res, int expectedHttpStatus, string? expectedTitle = null, string? expectedDetail = null)
{
// Assert on the status code
#if NET6_0
Assert.Equal("Microsoft.AspNetCore.Http.Result.ObjectResult", res.GetType().FullName);
// NOTE that we don't check Microsoft.AspNetCore.Http.Result.ObjectResult.StatusCode property as it always remains NULL.
Assert.Equal(expectedHttpStatus, res.GetProblemDetails()!.Status);
#elif NET7_0_OR_GREATER
Assert.Equal(expectedHttpStatus, (res as IStatusCodeHttpResult)?.StatusCode);
var problemDetails = (res as IValueHttpResult)?.Value as ProblemDetails;
Assert.Equal(expectedHttpStatus, problemDetails?.Status);
#endif
// Assert on the title if provided
if (!string.IsNullOrEmpty(expectedTitle))
#if NET6_0
Assert.Equal(expectedTitle, res.GetProblemDetails()!.Title);
#elif NET7_0_OR_GREATER
Assert.Equal(expectedTitle, problemDetails?.Title);
#endif
// Assert on the error details if provided
if (!string.IsNullOrEmpty(expectedDetail))
#if NET6_0
Assert.Equal(expectedDetail, res.GetProblemDetails()!.Detail);
#elif NET7_0_OR_GREATER
Assert.Equal(expectedDetail, problemDetails?.Detail);
#endif
}

/// <summary>
/// Assert that the <see cref="IResult"/> instance represent an internal 'OkObjectResult' class with correct value
/// </summary>
public static void AssertOkObjectResultTypeAndValue<TValue>(this IResult res, TValue expectedValue)
{
// Assert on 200 OK response type
#if NET6_0
Assert.Equal("Microsoft.AspNetCore.Http.Result.OkObjectResult", res.GetType().FullName);
Assert.Equal(200, res.GetPropValue("StatusCode"));
#elif NET7_0_OR_GREATER
var resTyped = res as Microsoft.AspNetCore.Http.HttpResults.Ok<TValue>;
Assert.NotNull(resTyped);
Assert.Equal(200, resTyped!.StatusCode);
#endif
// Assert on the expected value
#if NET6_0
Assert.Equal(expectedValue, res.GetPropValue());
#elif NET7_0_OR_GREATER
Assert.Equal(expectedValue, resTyped.Value);
#endif
}

/// <summary>
/// Assert that the <see cref="IResult"/> instance represent an internal 'NoContentResult' class
/// </summary>
public static void AssertNoContentResultType(this IResult res)
{
#if NET6_0
Assert.Equal("Microsoft.AspNetCore.Http.Result.NoContentResult", res.GetType().FullName);
Assert.Equal(204, res.GetPropValue("StatusCode"));
#elif NET7_0_OR_GREATER
var resTyped = res as Microsoft.AspNetCore.Http.HttpResults.NoContent;
Assert.NotNull(resTyped);
Assert.Equal(204, resTyped!.StatusCode);
#endif
}

/// <summary>
/// Assert that the <see cref="IResult"/> instance represent an internal 'CreatedResult' class
/// </summary>
public static ProblemDetails? GetProblemDetails(this IResult res) => res.GetPropValue() as ProblemDetails;
public static void AssertCreatedResultTypeAndValueAndLocation<TValue>(this IResult res, TValue expectedValue, string expectedLocation)
{
// Assert on 201 Created response type
#if NET6_0
Assert.Equal("Microsoft.AspNetCore.Http.Result.CreatedResult", res.GetType().FullName);
Assert.Equal(201, res.GetPropValue("StatusCode"));
#elif NET7_0_OR_GREATER
var resTyped = res as Microsoft.AspNetCore.Http.HttpResults.Created<TValue>;
Assert.NotNull(resTyped);
Assert.Equal(201, resTyped!.StatusCode);
#endif
// Assert on the expected value
#if NET6_0
Assert.Equal(expectedValue, res.GetPropValue());
#elif NET7_0_OR_GREATER
Assert.Equal(expectedValue, resTyped.Value);
#endif
// Assert on the expected location
#if NET6_0
Assert.Equal(expectedLocation, res.GetPropValue("Location"));
#elif NET7_0_OR_GREATER
Assert.Equal(expectedLocation, resTyped.Location);
#endif
}

#if NET6_0
/// <summary>
/// Extracts value from the specified property of <see cref="IResult"/> instance
/// </summary>
Expand All @@ -25,35 +117,14 @@ public static class ResultExtension
/// Gotta use reflection as Microsoft.AspNetCore.Http.Result.ObjectResult type is internal.
/// Also, we can't use 'dynamic' casting due to https://stackoverflow.com/q/15016561/968003
/// </remarks>
public static object? GetPropValue(this IResult res, string propName = "Value")
private static object? GetPropValue(this IResult res, string propName = "Value")
=> res.GetType().GetProperty(propName)?.GetValue(res);

/// <summary>
/// Assert that the <see cref="IResult"/> instance represent an internal 'ObjectResult' class
/// </summary>
public static void AssertObjectResultType(this IResult res)
=> Assert.Equal("Microsoft.AspNetCore.Http.Result.ObjectResult", res.GetType().FullName);

/// <summary>
/// Assert that the <see cref="IResult"/> instance represent an internal 'OkObjectResult' class
/// </summary>
public static void AssertOkObjectResultType(this IResult res)
=> Assert.Equal("Microsoft.AspNetCore.Http.Result.OkObjectResult", res.GetType().FullName);

/// <summary>
/// Assert that the <see cref="IResult"/> instance represent an internal 'NoContentResult' class
/// </summary>
public static void AssertNoContentResultType(this IResult res)
=> Assert.Equal("Microsoft.AspNetCore.Http.Result.NoContentResult", res.GetType().FullName);

/// <summary>
/// Assert that the <see cref="IResult"/> instance represent an internal 'CreatedResult' class
/// Extracts <see cref="ProblemDetails"/> from <see cref="IResult"/>
/// </summary>
public static void AssertCreatedResultType(this IResult res)
=> Assert.Equal("Microsoft.AspNetCore.Http.Result.CreatedResult", res.GetType().FullName);

// NOTE that we don't check Microsoft.AspNetCore.Http.Result.ObjectResult.StatusCode property as it always remains NULL.
// TODO: Double check if ObjectResult.StatusCode remains NULL in .NET 7 as well.
private static ProblemDetails? GetProblemDetails(this IResult res) => res.GetPropValue() as ProblemDetails;
#endif
}
#endif

14 changes: 4 additions & 10 deletions tests/DomainResults.Tests/Mvc/To200OkResultDomainResultTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,8 @@ public void DomainResult_With_Value_Converted_ToIResult_Test<TValue>(IDomainResu
// WHEN convert a value to IResult
var res = domainValue.ToResult();

// THEN the response type is correct
res.AssertOkObjectResultType();

// and value remains there
Assert.Equal(domainValue.Value!, res.GetPropValue());
// THEN the response type OK with correct value
res.AssertOkObjectResultTypeAndValue(domainValue.Value!);
}
#endif
public static readonly IEnumerable<object[]> SuccessfulTestCases = GetTestCases(false);
Expand Down Expand Up @@ -79,11 +76,8 @@ public async Task DomainResult_With_Value_Task_Converted_ToIResult_Test<TValue>(
// WHEN convert a value to IResult
var res = await domainValueTask.ToResult();

// THEN the response type is correct
res.AssertOkObjectResultType();

// and value remains there
Assert.Equal((await domainValueTask).Value!, res.GetPropValue());
// THEN the response type OK with correct value
res.AssertOkObjectResultTypeAndValue((await domainValueTask).Value!);
}
#endif
public static readonly IEnumerable<object[]> SuccessfulTaskTestCases = GetTestCases(true);
Expand Down
18 changes: 8 additions & 10 deletions tests/DomainResults.Tests/Mvc/To200OkResultTupleValueTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,13 @@ public void ValueResult_Converted_ToActionResult_Test<TValue>((TValue, IDomainRe
[MemberData(nameof(SuccessfulTestCases))]
public void ValueResult_Converted_ToIResult_Test<TValue>((TValue, IDomainResult) tupleValue)
{
var (value, _) = tupleValue;

// WHEN convert a value to IResult
var res = tupleValue.ToResult();

// THEN the response type is correct
res.AssertOkObjectResultType();

// and value remains there
Assert.Equal(tupleValue.Item1!, res.GetPropValue());
// THEN the response type OK with correct value
res.AssertOkObjectResultTypeAndValue(value);
}
#endif

Expand Down Expand Up @@ -82,14 +81,13 @@ public async Task ValueResult_Task_Converted_ToActionResult_Test<TValue>(Task<(T
[MemberData(nameof(SuccessfulTaskTestCases))]
public async Task ValueResult_Task_Converted_ToIResult_Test<TValue>(Task<(TValue, IDomainResult)> tupleValueTask)
{
var (value, _) = await tupleValueTask;

// WHEN convert a value to IResult
var res = await tupleValueTask.ToResult();

// THEN the response type is correct
res.AssertOkObjectResultType();

// and value remains there
Assert.Equal((await tupleValueTask).Item1!, res.GetPropValue());
// THEN the response type OK with correct value
res.AssertOkObjectResultTypeAndValue(value);
}
#endif

Expand Down
Loading

0 comments on commit 2ab8437

Please sign in to comment.