(feature): add redirect handling in Result<T>.ToActionResult
This commit is contained in:
parent
14fe8f2897
commit
60ffad2ea9
@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [2.0.3] - 2026-06-28
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- `Result<T>.ToActionResult()` now returns `RedirectResult` when the status code is a redirection (300–303, 307–308) and `Value` is a non-empty URL string.
|
||||||
|
- Added `MaksIT.Results.Mvc.RedirectResult` for HTTP redirect responses.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Updated ASP.NET Core and Microsoft.Extensions package references.
|
||||||
|
|
||||||
## [2.0.2] - 2026-06-02
|
## [2.0.2] - 2026-06-02
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
@ -82,6 +82,7 @@ public sealed record UserDto(Guid Id, string Name);
|
|||||||
|
|
||||||
- `Result` success: returns status-code-only response.
|
- `Result` success: returns status-code-only response.
|
||||||
- `Result<T>` success with non-null `Value`: returns JSON body + status code.
|
- `Result<T>` success with non-null `Value`: returns JSON body + status code.
|
||||||
|
- `Result<T>` success with a redirection status code (300–303, 307–308) and a non-empty string `Value`: returns an HTTP redirect to that URL.
|
||||||
- Any failure: returns RFC 7807-style `ProblemDetails` JSON with:
|
- Any failure: returns RFC 7807-style `ProblemDetails` JSON with:
|
||||||
- `status` = result status code
|
- `status` = result status code
|
||||||
- `title` = `"An error occurred"`
|
- `title` = `"An error occurred"`
|
||||||
|
|||||||
@ -1,21 +1,21 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="150" height="20" role="img" aria-label="Branch Coverage: 65.4%">
|
<svg xmlns="http://www.w3.org/2000/svg" width="147.5" height="20" role="img" aria-label="Branch Coverage: 75%">
|
||||||
<title>Branch Coverage: 65.4%</title>
|
<title>Branch Coverage: 75%</title>
|
||||||
<linearGradient id="s" x2="0" y2="100%">
|
<linearGradient id="s" x2="0" y2="100%">
|
||||||
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
||||||
<stop offset="1" stop-opacity=".1"/>
|
<stop offset="1" stop-opacity=".1"/>
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
<clipPath id="r">
|
<clipPath id="r">
|
||||||
<rect width="150" height="20" rx="3" fill="#fff"/>
|
<rect width="147.5" height="20" rx="3" fill="#fff"/>
|
||||||
</clipPath>
|
</clipPath>
|
||||||
<g clip-path="url(#r)">
|
<g clip-path="url(#r)">
|
||||||
<rect width="107.5" height="20" fill="#555"/>
|
<rect width="107.5" height="20" fill="#555"/>
|
||||||
<rect x="107.5" width="42.5" height="20" fill="#97ca00"/>
|
<rect x="107.5" width="40" height="20" fill="#97ca00"/>
|
||||||
<rect width="150" height="20" fill="url(#s)"/>
|
<rect width="147.5" height="20" fill="url(#s)"/>
|
||||||
</g>
|
</g>
|
||||||
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="11">
|
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="11">
|
||||||
<text aria-hidden="true" x="53.75" y="15" fill="#010101" fill-opacity=".3">Branch Coverage</text>
|
<text aria-hidden="true" x="53.75" y="15" fill="#010101" fill-opacity=".3">Branch Coverage</text>
|
||||||
<text x="53.75" y="14" fill="#fff">Branch Coverage</text>
|
<text x="53.75" y="14" fill="#fff">Branch Coverage</text>
|
||||||
<text aria-hidden="true" x="128.75" y="15" fill="#010101" fill-opacity=".3">65.4%</text>
|
<text aria-hidden="true" x="127.5" y="15" fill="#010101" fill-opacity=".3">75%</text>
|
||||||
<text x="128.75" y="14" fill="#fff">65.4%</text>
|
<text x="127.5" y="14" fill="#fff">75%</text>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
@ -1,5 +1,5 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="137" height="20" role="img" aria-label="Line Coverage: 17.6%">
|
<svg xmlns="http://www.w3.org/2000/svg" width="137" height="20" role="img" aria-label="Line Coverage: 19.8%">
|
||||||
<title>Line Coverage: 17.6%</title>
|
<title>Line Coverage: 19.8%</title>
|
||||||
<linearGradient id="s" x2="0" y2="100%">
|
<linearGradient id="s" x2="0" y2="100%">
|
||||||
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
||||||
<stop offset="1" stop-opacity=".1"/>
|
<stop offset="1" stop-opacity=".1"/>
|
||||||
@ -15,7 +15,7 @@
|
|||||||
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="11">
|
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="11">
|
||||||
<text aria-hidden="true" x="47.25" y="15" fill="#010101" fill-opacity=".3">Line Coverage</text>
|
<text aria-hidden="true" x="47.25" y="15" fill="#010101" fill-opacity=".3">Line Coverage</text>
|
||||||
<text x="47.25" y="14" fill="#fff">Line Coverage</text>
|
<text x="47.25" y="14" fill="#fff">Line Coverage</text>
|
||||||
<text aria-hidden="true" x="115.75" y="15" fill="#010101" fill-opacity=".3">17.6%</text>
|
<text aria-hidden="true" x="115.75" y="15" fill="#010101" fill-opacity=".3">19.8%</text>
|
||||||
<text x="115.75" y="14" fill="#fff">17.6%</text>
|
<text x="115.75" y="14" fill="#fff">19.8%</text>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
@ -1,5 +1,5 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="150" height="20" role="img" aria-label="Method Coverage: 17.8%">
|
<svg xmlns="http://www.w3.org/2000/svg" width="150" height="20" role="img" aria-label="Method Coverage: 19.9%">
|
||||||
<title>Method Coverage: 17.8%</title>
|
<title>Method Coverage: 19.9%</title>
|
||||||
<linearGradient id="s" x2="0" y2="100%">
|
<linearGradient id="s" x2="0" y2="100%">
|
||||||
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
||||||
<stop offset="1" stop-opacity=".1"/>
|
<stop offset="1" stop-opacity=".1"/>
|
||||||
@ -15,7 +15,7 @@
|
|||||||
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="11">
|
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="11">
|
||||||
<text aria-hidden="true" x="53.75" y="15" fill="#010101" fill-opacity=".3">Method Coverage</text>
|
<text aria-hidden="true" x="53.75" y="15" fill="#010101" fill-opacity=".3">Method Coverage</text>
|
||||||
<text x="53.75" y="14" fill="#fff">Method Coverage</text>
|
<text x="53.75" y="14" fill="#fff">Method Coverage</text>
|
||||||
<text aria-hidden="true" x="128.75" y="15" fill="#010101" fill-opacity=".3">17.8%</text>
|
<text aria-hidden="true" x="128.75" y="15" fill="#010101" fill-opacity=".3">19.9%</text>
|
||||||
<text x="128.75" y="14" fill="#fff">17.8%</text>
|
<text x="128.75" y="14" fill="#fff">19.9%</text>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
@ -14,10 +14,10 @@
|
|||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.3.10" />
|
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.3.11" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.8" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.9" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.8" />
|
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.9" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.6.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.7.0" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
|
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
|||||||
@ -45,4 +45,15 @@ public class ResultToActionResultTests {
|
|||||||
Assert.Equal((int)HttpStatusCode.OK, objectResult.StatusCode);
|
Assert.Equal((int)HttpStatusCode.OK, objectResult.StatusCode);
|
||||||
Assert.Equal(value, objectResult.Value);
|
Assert.Equal(value, objectResult.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ToActionResult_WhenGenericSuccessWithRedirect_ReturnsRedirectResult() {
|
||||||
|
var result = Result<string>.Found("https://example.com/continue");
|
||||||
|
|
||||||
|
var actionResult = result.ToActionResult();
|
||||||
|
|
||||||
|
Assert.IsType<RedirectResult>(actionResult);
|
||||||
|
var redirectResult = (RedirectResult)actionResult;
|
||||||
|
Assert.Equal("https://example.com/continue", redirectResult.Location);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
<!-- NuGet package metadata -->
|
<!-- NuGet package metadata -->
|
||||||
<PackageId>MaksIT.Results</PackageId>
|
<PackageId>MaksIT.Results</PackageId>
|
||||||
<Version>2.0.2</Version>
|
<Version>2.0.3</Version>
|
||||||
<Authors>Maksym Sadovnychyy</Authors>
|
<Authors>Maksym Sadovnychyy</Authors>
|
||||||
<Company>MAKS-IT</Company>
|
<Company>MAKS-IT</Company>
|
||||||
<Product>MaksIT.Results</Product>
|
<Product>MaksIT.Results</Product>
|
||||||
@ -22,9 +22,9 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Abstractions" Version="2.3.10" />
|
<PackageReference Include="Microsoft.AspNetCore.Mvc.Abstractions" Version="2.3.11" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.8" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.9" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.8" />
|
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.9" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
12
src/MaksIT.Results/Mvc/RedirectResult.cs
Normal file
12
src/MaksIT.Results/Mvc/RedirectResult.cs
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace MaksIT.Results.Mvc;
|
||||||
|
|
||||||
|
public class RedirectResult(string location) : IActionResult {
|
||||||
|
public string Location { get; } = location;
|
||||||
|
|
||||||
|
public Task ExecuteResultAsync(ActionContext context) {
|
||||||
|
context.HttpContext.Response.Redirect(Location);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -78,13 +78,24 @@ public partial class Result<T> : Result {
|
|||||||
/// <returns>IActionResult that represents the HTTP response.</returns>
|
/// <returns>IActionResult that represents the HTTP response.</returns>
|
||||||
public override IActionResult ToActionResult() {
|
public override IActionResult ToActionResult() {
|
||||||
if (IsSuccess) {
|
if (IsSuccess) {
|
||||||
if (Value is not null) {
|
if (IsRedirectionStatusCode(StatusCode) && Value is string location && !string.IsNullOrWhiteSpace(location))
|
||||||
|
return new RedirectResult(location);
|
||||||
|
|
||||||
|
if (Value is not null)
|
||||||
return new ObjectResult(Value) { StatusCode = (int)StatusCode };
|
return new ObjectResult(Value) { StatusCode = (int)StatusCode };
|
||||||
}
|
|
||||||
return base.ToActionResult();
|
return base.ToActionResult();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return base.ToActionResult();
|
return base.ToActionResult();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool IsRedirectionStatusCode(HttpStatusCode statusCode) =>
|
||||||
|
statusCode is HttpStatusCode.MultipleChoices
|
||||||
|
or HttpStatusCode.MovedPermanently
|
||||||
|
or HttpStatusCode.Found
|
||||||
|
or HttpStatusCode.SeeOther
|
||||||
|
or HttpStatusCode.TemporaryRedirect
|
||||||
|
or HttpStatusCode.PermanentRedirect;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user