Compare commits

...

2 Commits
v2.0.2 ... main

Author SHA1 Message Date
Maksym Sadovnychyy
60ffad2ea9 (feature): add redirect handling in Result<T>.ToActionResult 2026-06-28 10:45:15 +02:00
Maksym Sadovnychyy
14fe8f2897 (chore): add cursor agent config to load shared maksit skills from homelab 2026-06-04 19:49:41 +02:00
13 changed files with 104 additions and 25 deletions

View File

@ -0,0 +1,9 @@
{
"$schema": "homelab-maksit-skills-manifest-v1",
"skillsRoot": "E:\\Users\\maksym\\source\\repos\\private\\homelab\\ai\\skills",
"skills": [
"common/csharp",
"common/maksit-repo-maintenance",
"local-ollama"
]
}

View File

@ -0,0 +1,15 @@
---
description: Load MaksIT agent skills from homelab (deterministic)
globs: "**/*.{cs,csproj,slnx,md}"
alwaysApply: true
---
# MaksIT skills (maksit-results)
Complementary skills (no precedence). Read each `SKILL.md` when relevant:
1. `E:\Users\maksym\source\repos\private\homelab\ai\skills\common\csharp\SKILL.md`
2. `E:\Users\maksym\source\repos\private\homelab\ai\skills\common\maksit-repo-maintenance\SKILL.md`
3. `E:\Users\maksym\source\repos\private\homelab\ai\skills\local-ollama\SKILL.md` — local Ollama offload (`@local-ollama`)
Manifest: `.cursor/maksit-skills.json`.

11
AGENTS.md Normal file
View File

@ -0,0 +1,11 @@
# Agent instructions (maksit-results)
Complementary skills (no precedence):
| Skill | Path |
|-------|------|
| csharp | [SKILL.md](E:\Users\maksym\source\repos\private\homelab\ai\skills\common\csharp\SKILL.md) |
| maksit-repo-maintenance | [SKILL.md](E:\Users\maksym\source\repos\private\homelab\ai\skills\common\maksit-repo-maintenance\SKILL.md) |
| local-ollama | [SKILL.md](E:\Users\maksym\source\repos\private\homelab\ai\skills\local-ollama\SKILL.md) |
Manifest: `.cursor/maksit-skills.json`.

View File

@ -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/),
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 (300303, 307308) 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
### Changed

View File

@ -82,6 +82,7 @@ public sealed record UserDto(Guid Id, string Name);
- `Result` success: returns status-code-only response.
- `Result<T>` success with non-null `Value`: returns JSON body + status code.
- `Result<T>` success with a redirection status code (300303, 307308) and a non-empty string `Value`: returns an HTTP redirect to that URL.
- Any failure: returns RFC 7807-style `ProblemDetails` JSON with:
- `status` = result status code
- `title` = `"An error occurred"`

View File

@ -1,21 +1,21 @@
<svg xmlns="http://www.w3.org/2000/svg" width="150" height="20" role="img" aria-label="Branch Coverage: 65.4%">
<title>Branch Coverage: 65.4%</title>
<svg xmlns="http://www.w3.org/2000/svg" width="147.5" height="20" role="img" aria-label="Branch Coverage: 75%">
<title>Branch Coverage: 75%</title>
<linearGradient id="s" x2="0" y2="100%">
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
<stop offset="1" stop-opacity=".1"/>
</linearGradient>
<clipPath id="r">
<rect width="150" height="20" rx="3" fill="#fff"/>
<rect width="147.5" height="20" rx="3" fill="#fff"/>
</clipPath>
<g clip-path="url(#r)">
<rect width="107.5" height="20" fill="#555"/>
<rect x="107.5" width="42.5" height="20" fill="#97ca00"/>
<rect width="150" height="20" fill="url(#s)"/>
<rect x="107.5" width="40" height="20" fill="#97ca00"/>
<rect width="147.5" height="20" fill="url(#s)"/>
</g>
<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 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 x="128.75" y="14" fill="#fff">65.4%</text>
<text aria-hidden="true" x="127.5" y="15" fill="#010101" fill-opacity=".3">75%</text>
<text x="127.5" y="14" fill="#fff">75%</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -1,5 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="137" height="20" role="img" aria-label="Line Coverage: 17.6%">
<title>Line Coverage: 17.6%</title>
<svg xmlns="http://www.w3.org/2000/svg" width="137" height="20" role="img" aria-label="Line Coverage: 19.8%">
<title>Line Coverage: 19.8%</title>
<linearGradient id="s" x2="0" y2="100%">
<stop offset="0" stop-color="#bbb" 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">
<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 aria-hidden="true" x="115.75" y="15" fill="#010101" fill-opacity=".3">17.6%</text>
<text x="115.75" y="14" fill="#fff">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">19.8%</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -1,5 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="150" height="20" role="img" aria-label="Method Coverage: 17.8%">
<title>Method Coverage: 17.8%</title>
<svg xmlns="http://www.w3.org/2000/svg" width="150" height="20" role="img" aria-label="Method Coverage: 19.9%">
<title>Method Coverage: 19.9%</title>
<linearGradient id="s" x2="0" y2="100%">
<stop offset="0" stop-color="#bbb" 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">
<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 aria-hidden="true" x="128.75" y="15" fill="#010101" fill-opacity=".3">17.8%</text>
<text x="128.75" y="14" fill="#fff">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">19.9%</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -14,10 +14,10 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.3.10" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.8" />
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.8" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.6.0" />
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.3.11" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.9" />
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.9" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.7.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

@ -45,4 +45,15 @@ public class ResultToActionResultTests {
Assert.Equal((int)HttpStatusCode.OK, objectResult.StatusCode);
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);
}
}

View File

@ -8,7 +8,7 @@
<!-- NuGet package metadata -->
<PackageId>MaksIT.Results</PackageId>
<Version>2.0.2</Version>
<Version>2.0.3</Version>
<Authors>Maksym Sadovnychyy</Authors>
<Company>MAKS-IT</Company>
<Product>MaksIT.Results</Product>
@ -22,9 +22,9 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Abstractions" Version="2.3.10" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.8" />
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.8" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Abstractions" Version="2.3.11" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.9" />
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.9" />
</ItemGroup>
<ItemGroup>

View 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;
}
}

View File

@ -78,13 +78,24 @@ public partial class Result<T> : Result {
/// <returns>IActionResult that represents the HTTP response.</returns>
public override IActionResult ToActionResult() {
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 base.ToActionResult();
}
else {
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;
}