Compare commits

..

1 Commits
v2.0.6 ... main

Author SHA1 Message Date
Maksym Sadovnychyy
7958f3c961 (feature): add FromActionResult and FromHttpResponseAsync; preserve redirect status 2026-10-28 19:07:37 +01:00
27 changed files with 809 additions and 1425 deletions

View File

@ -5,6 +5,23 @@ 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.1.0] - 2026-10-28
### Added
- `Result.FromActionResult` / `Result<T>.FromActionResult` - reverse of `ToActionResult` for MaksIT MVC result types.
- `Result.FromHttpResponseAsync` / `Result<T>.FromHttpResponseAsync` - parse HTTP responses in the `ToActionResult` wire shape (JSON, redirect `Location`, `ProblemDetails`).
- Package reference to `MaksIT.Core` for JSON helpers used by the HTTP parse path.
### Changed
- `RedirectResult` accepts an explicit status code; default remains 302 Found.
- README rewritten around the service <-> API <-> client round-trip (`ToActionResult` / `FromActionResult` / `FromHttpResponseAsync`).
- Pruned unused RepoUtils plugins so `utils/plugins` matches the enabled list in `scriptSettings.json`.
- Stopped packaging local `assets/badges` copies; coverage badges remain shields.io URLs in the README.
### Fixed
- `RedirectResult` now sets `StatusCode` and `Location` instead of always using `Response.Redirect` (302).
- `Result<T>.ToActionResult` passes the result status into `RedirectResult`.
## [2.0.6] - 2026-10-27
### Changed

View File

@ -26,7 +26,7 @@ dotnet build MaksIT.Results.slnx
### Running Tests
Preferred: `utils\Invoke-TestEngine.bat` (DotNetTest → QualityGate → CoverageBadges). Tests run under **Microsoft Testing Platform** (`src/global.json` `test.runner` next to the `.slnx`) with **xunit.v3** and **coverlet.MTP**.
Preferred: `utils\Invoke-TestEngine.bat` (DotNetTest -> QualityGate -> CoverageBadges). Tests run under **Microsoft Testing Platform** (`src/global.json` `test.runner` next to the `.slnx`) with **xunit.v3** and **coverlet.MTP**.
```bash
cd src

156
README.md
View File

@ -1,38 +1,57 @@
# MaksIT.Results
![Line Coverage](https://img.shields.io/badge/Line%20Coverage-18.4%25-orange)
![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-76.3%25-green)
![Method Coverage](https://img.shields.io/badge/Method%20Coverage-12.8%25-orange)
[![NuGet](https://img.shields.io/nuget/v/MaksIT.Results)](https://www.nuget.org/packages/MaksIT.Results)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue)](LICENSE.md)
`MaksIT.Results` is a .NET library for modeling operation outcomes as HTTP-aware result objects and converting them to `IActionResult` in ASP.NET Core.
![Line Coverage](https://img.shields.io/badge/Line%20Coverage-44.8%25-yellowgreen)
![Branch Coverage](https://img.shields.io/badge/Branch%20Coverage-54.7%25-yellowgreen)
![Method Coverage](https://img.shields.io/badge/Method%20Coverage-20.5%25-yellow)
## Features
HTTP-aware `Result` / `Result<T>` for .NET - model success and failure once, then map to ASP.NET Core `IActionResult` or parse the same wire shape from an `HttpResponseMessage`.
- `Result` and `Result<T>` models with status code, success flag, and messages.
- Static factory methods for common and extended HTTP status codes (1xx, 2xx, 3xx, 4xx, 5xx).
- Built-in conversion to `IActionResult` via `ToActionResult()`.
- RFC 7807-style error payloads for failures (`application/problem+json`).
- Camel-case JSON serialization for response bodies; respects app-configured `JsonSerializerOptions` (e.g. `AddJsonOptions` with `DefaultIgnoreCondition.WhenWritingNull`).
**Target:** .NET 10 (`net10.0`)
## Installation
## Why
Package Manager:
Most Result libraries give you success/failure (and sometimes a typed error), then leave HTTP as a second mapping in the controller: domain `NotFound` -> 404, `Conflict` -> 409, and so on. That remapping is where APIs drift.
```bash
Install-Package MaksIT.Results
```
**MaksIT.Results uses HTTP status codes as the outcome vocabulary.** One type carries success/failure, messages, and a shared classification (`NotFound`, `Conflict`, `TooManyRequests`, ...). The service already decided the status; the controller calls `ToActionResult()`.
`dotnet` CLI:
That is not "leaking HTTP into the domain." Status codes are a mature taxonomy of outcomes that apply beyond the wire - including desktop, workers, and CLI:
| Status idea | Same idea outside the browser |
|-------------|-------------------------------|
| 404 | entity / file / setting not found |
| 409 | concurrent edit / state conflict |
| 401 / 403 | not signed in / not allowed |
| 422 | validation failed |
| 429 | rate limit / throttle |
| 503 | dependency down / retry later |
| 202 | accepted, still processing |
### Compared to typical Result libs
| | Typical Result lib | MaksIT.Results |
|--|--------------------|----------------|
| Outcome model | Success / Failure (+ custom errors) | Success / Failure + `HttpStatusCode` |
| Controller mapping | Invent codes, map again to HTTP | Status already set; `ToActionResult()` |
| Failure body | Often ad hoc | RFC 7807 `ProblemDetails` |
| Client parse | Rare / out of scope | `FromHttpResponseAsync` / `FromActionResult` |
| Status factories | Usually `Ok` / `Fail` (+ a few) | Named 1xx-5xx factories |
**Bottom line:** most Result packages stop at "return from a method." This one is a service <-> API <-> client result model - one contract from domain to wire and back.
## Install
```bash
dotnet add package MaksIT.Results
```
## Target Framework
```powershell
Install-Package MaksIT.Results
```
- `.NET 10` (`net10.0`)
## Quick Start
## Quick start
### Create results
@ -42,33 +61,30 @@ using MaksIT.Results;
Result ok = Result.Ok("Operation completed");
Result failed = Result.BadRequest("Validation failed");
Result<int> okWithValue = Result<int>.Ok(42, "Answer generated");
Result<string?> notFound = Result<string?>.NotFound(null, "Entity not found");
Result<int> answer = Result<int>.Ok(42, "Answer generated");
Result<UserDto?> missing = Result<UserDto?>.NotFound(null, "Entity not found");
```
### Convert between result types
### Map between result shapes
```csharp
using MaksIT.Results;
Result<int> source = Result<int>.Ok(42, "Value loaded");
// Result<T> -> Result<U>
Result<string?> mapped = source.ToResultOfType(v => v?.ToString());
// Result<T> -> Result
Result<string?> asString = source.ToResultOfType(v => v?.ToString());
Result nonGeneric = source.ToResult();
```
### Use in an ASP.NET Core controller
### Controllers
```csharp
using MaksIT.Results;
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("users")]
public sealed class UsersController : ControllerBase {
[HttpGet("{id:guid}")]
public IActionResult GetUser(Guid id) {
public IActionResult Get(Guid id) {
Result<UserDto?> result = id == Guid.Empty
? Result<UserDto?>.BadRequest(null, "Invalid id")
: Result<UserDto?>.Ok(new UserDto(id, "maks"), "User loaded");
@ -80,20 +96,47 @@ public sealed class UsersController : ControllerBase {
public sealed record UserDto(Guid Id, string Name);
```
## `ToActionResult()` Behavior
### HTTP clients
- `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 (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:
- `status` = result status code
- `title` = `"An error occurred"`
- `detail` = joined `Messages`
- content type `application/problem+json`
Parse responses produced in the same contract (JSON body, redirect `Location`, or `problem+json`):
```csharp
using HttpResponseMessage response = await http.GetAsync(url);
Result bare = await Result.FromHttpResponseAsync(response);
Result<UserDto> typed = await Result<UserDto>.FromHttpResponseAsync(response);
```
Or reverse an `IActionResult` you already built:
```csharp
IActionResult action = result.ToActionResult();
Result parsed = Result.FromActionResult(action);
Result<UserDto> typed = Result<UserDto>.FromActionResult(action);
```
## `ToActionResult()` contract
| Outcome | Response |
|--------|----------|
| `Result` success | Status code only |
| `Result<T>` success, non-null `Value` | JSON body + status |
| `Result<T>` success, redirect status (300-303, 307-308) and non-empty string `Value` | Redirect; status preserved on `RedirectResult` |
| Any failure | `ProblemDetails`: `status`, `title` = `"An error occurred"`, `detail` = joined `Messages`, content type `application/problem+json` |
## `FromActionResult` / `FromHttpResponseAsync`
| Wire shape | Result |
|------------|--------|
| Success, no body | Success `Result` / empty `Result<T>` |
| Success JSON body | Deserialized into `Result<T>.Value` |
| Redirect + `Location` | `Result<string>` with that URL and redirect status |
| `application/problem+json` (or JSON with `detail` / `title`) | Failure; messages from problem details |
## JSON options
`ObjectResult` uses the same `JsonSerializerOptions` as your app when you configure them with `AddJsonOptions`:
`ObjectResult` uses the app's `JsonSerializerOptions` when configured via `AddJsonOptions`:
```csharp
builder.Services.AddControllers()
@ -102,29 +145,26 @@ builder.Services.AddControllers()
});
```
If no options are registered, a default (camel-case) serializer is used.
If none are registered, responses fall back to camel case.
## Status Code Factories
## Status code factories
- Informational: `Result.Continue(...)`, `Result.SwitchingProtocols(...)`, `Result.Processing(...)`, etc.
- Success: `Result.Ok(...)`, `Result.Created(...)`, `Result.NoContent(...)`, etc.
- Redirection: `Result.Found(...)`, `Result.PermanentRedirect(...)`, etc.
- Client error: `Result.BadRequest(...)`, `Result.NotFound(...)`, `Result.TooManyRequests(...)`, etc.
- Server error: `Result.InternalServerError(...)`, `Result.ServiceUnavailable(...)`, etc.
| Family | Examples |
|--------|----------|
| Informational | `Continue`, `SwitchingProtocols`, `Processing`, ... |
| Success | `Ok`, `Created`, `NoContent`, ... |
| Redirection | `Found`, `MovedPermanently`, `PermanentRedirect`, ... |
| Client error | `BadRequest`, `NotFound`, `Conflict`, `TooManyRequests`, ... |
| Server error | `InternalServerError`, `ServiceUnavailable`, ... |
Generic equivalents are available via `Result<T>`, for example `Result<MyDto>.Ok(value, "message")`.
Use `Result.*` or `Result<T>.*` (e.g. `Result<MyDto>.Ok(value, "message")`).
## Contributing
See `CONTRIBUTING.md`.
## Contact
If you have any questions or need further assistance, feel free to reach out:
- **Email**: [maksym.sadovnychyy@gmail.com](mailto:maksym.sadovnychyy@gmail.com)
- **Reddit**: [MaksIT.Results: Streamline Your ASP.NET Core API Response Handling](https://www.reddit.com/r/MaksIT/comments/1f89ifn/maksitresults_streamline_your_aspnet_core_api/?utm_source=share&utm_medium=web3x&utm_name=web3xcss&utm_term=1&utm_content=share_button)
See [CONTRIBUTING.md](CONTRIBUTING.md). Changelog: [CHANGELOG.md](CHANGELOG.md).
## License
See `LICENSE.md`.
[MIT](LICENSE.md) - Maksym Sadovnychyy (MAKS-IT)
**Contact:** [maksym.sadovnychyy@gmail.com](mailto:maksym.sadovnychyy@gmail.com)

View File

@ -3,7 +3,6 @@ using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using MaksIT.Results.Mvc;
namespace MaksIT.Results.Tests.Mvc;

View File

@ -0,0 +1,61 @@
using System.Net;
using MaksIT.Results.Mvc;
namespace MaksIT.Results.Tests;
public class ResultFromActionResultTests {
[Fact]
public void FromActionResult_RoundTripsSuccessStatusCode() {
var original = Result.Ok("done");
var action = original.ToActionResult();
var parsed = Result.FromActionResult(action);
Assert.True(parsed.IsSuccess);
Assert.Equal(HttpStatusCode.OK, parsed.StatusCode);
}
[Fact]
public void FromActionResult_RoundTripsFailureProblemDetails() {
var original = Result.BadRequest("Validation failed", "Field required");
var action = original.ToActionResult();
var parsed = Result.FromActionResult(action);
Assert.False(parsed.IsSuccess);
Assert.Equal(HttpStatusCode.BadRequest, parsed.StatusCode);
Assert.Equal(original.Messages, parsed.Messages);
}
[Fact]
public void FromActionResult_RoundTripsGenericValue() {
var dto = new SampleDto(7, "vault");
var original = Result<SampleDto>.Ok(dto);
var action = original.ToActionResult();
var parsed = Result<SampleDto>.FromActionResult(action);
Assert.True(parsed.IsSuccess);
Assert.Equal(HttpStatusCode.OK, parsed.StatusCode);
Assert.NotNull(parsed.Value);
Assert.Equal(dto.Id, parsed.Value!.Id);
Assert.Equal(dto.Name, parsed.Value.Name);
}
[Fact]
public void FromActionResult_RoundTripsRedirectWithStatus() {
var original = Result<string>.MovedPermanently("https://example.com/new");
var action = original.ToActionResult();
var redirect = Assert.IsType<RedirectResult>(action);
Assert.Equal((int)HttpStatusCode.MovedPermanently, redirect.StatusCode);
var parsed = Result<string>.FromActionResult(action);
Assert.True(parsed.IsSuccess);
Assert.Equal(HttpStatusCode.MovedPermanently, parsed.StatusCode);
Assert.Equal("https://example.com/new", parsed.Value);
}
private sealed record SampleDto(int Id, string Name);
}

View File

@ -0,0 +1,77 @@
using System.Net;
using System.Text;
using System.Text.Json;
using MaksIT.Results.Mvc;
namespace MaksIT.Results.Tests;
public class ResultFromHttpResponseTests {
[Fact]
public async Task FromHttpResponseAsync_WhenOk_ReturnsSuccess() {
using var response = new HttpResponseMessage(HttpStatusCode.OK);
var result = await Result.FromHttpResponseAsync(response, TestContext.Current.CancellationToken);
Assert.True(result.IsSuccess);
Assert.Equal(HttpStatusCode.OK, result.StatusCode);
}
[Fact]
public async Task FromHttpResponseAsync_WhenProblemDetails_ReturnsFailureMessages() {
var problem = new ProblemDetails {
Status = 404,
Title = "An error occurred",
Detail = "Missing; Gone"
};
using var response = new HttpResponseMessage(HttpStatusCode.NotFound) {
Content = new StringContent(JsonSerializer.Serialize(problem), Encoding.UTF8, "application/problem+json")
};
var result = await Result.FromHttpResponseAsync(response, TestContext.Current.CancellationToken);
Assert.False(result.IsSuccess);
Assert.Equal(HttpStatusCode.NotFound, result.StatusCode);
Assert.Equal(["Missing", "Gone"], result.Messages);
}
[Fact]
public async Task FromHttpResponseAsync_WhenJsonBody_DeserializesValue() {
using var response = new HttpResponseMessage(HttpStatusCode.OK) {
Content = new StringContent("""{"id":3,"name":"hub"}""", Encoding.UTF8, "application/json")
};
var result = await Result<SampleDto>.FromHttpResponseAsync(response, TestContext.Current.CancellationToken);
Assert.True(result.IsSuccess);
Assert.Equal(3, result.Value!.Id);
Assert.Equal("hub", result.Value.Name);
}
[Fact]
public async Task FromHttpResponseAsync_WhenRedirect_ReturnsLocation() {
using var response = new HttpResponseMessage(HttpStatusCode.Found);
response.Headers.Location = new Uri("https://accounts.example.com/auth");
var result = await Result<string>.FromHttpResponseAsync(response, TestContext.Current.CancellationToken);
Assert.True(result.IsSuccess);
Assert.Equal(HttpStatusCode.Found, result.StatusCode);
Assert.Equal("https://accounts.example.com/auth", result.Value);
}
[Fact]
public async Task FromHttpResponseAsync_WhenDetailOnlyJson_ParsesWithoutProblemMediaType() {
using var response = new HttpResponseMessage(HttpStatusCode.NotFound) {
Content = new StringContent("""{"detail":"Invalid or expired code."}""", Encoding.UTF8, "application/json")
};
var result = await Result<string>.FromHttpResponseAsync(response, TestContext.Current.CancellationToken);
Assert.False(result.IsSuccess);
Assert.Equal(HttpStatusCode.NotFound, result.StatusCode);
Assert.Contains("Invalid or expired code.", result.Messages[0]);
}
private sealed record SampleDto(int Id, string Name);
}

View File

@ -1,5 +1,4 @@
using System.Net;
using Microsoft.AspNetCore.Mvc;
using MaksIT.Results.Mvc;
namespace MaksIT.Results.Tests;
@ -11,8 +10,8 @@ public class ResultToActionResultTests {
var actionResult = result.ToActionResult();
Assert.IsType<StatusCodeResult>(actionResult);
var statusCodeResult = (StatusCodeResult)actionResult;
var statusCodeResult = Assert.IsType<StatusCodeResult>(actionResult);
Assert.Equal((int)HttpStatusCode.OK, statusCodeResult.StatusCode);
}
@ -23,11 +22,12 @@ public class ResultToActionResultTests {
var actionResult = result.ToActionResult();
Assert.IsType<ObjectResult>(actionResult);
var objectResult = (ObjectResult)actionResult;
var objectResult = Assert.IsType<ObjectResult>(actionResult);
Assert.Equal((int)HttpStatusCode.BadRequest, objectResult.StatusCode);
Assert.IsType<ProblemDetails>(objectResult.Value);
var problemDetails = (ProblemDetails)objectResult.Value!;
var problemDetails = Assert.IsType<ProblemDetails>(objectResult.Value);
Assert.Equal((int)HttpStatusCode.BadRequest, problemDetails.Status);
Assert.Equal("An error occurred", problemDetails.Title);
Assert.Equal(errorMessage, problemDetails.Detail);
@ -40,8 +40,8 @@ public class ResultToActionResultTests {
var actionResult = result.ToActionResult();
Assert.IsType<ObjectResult>(actionResult);
var objectResult = (ObjectResult)actionResult;
var objectResult = Assert.IsType<ObjectResult>(actionResult);
Assert.Equal((int)HttpStatusCode.OK, objectResult.StatusCode);
Assert.Equal(value, objectResult.Value);
}
@ -52,8 +52,9 @@ public class ResultToActionResultTests {
var actionResult = result.ToActionResult();
Assert.IsType<RedirectResult>(actionResult);
var redirectResult = (RedirectResult)actionResult;
var redirectResult = Assert.IsType<RedirectResult>(actionResult);
Assert.Equal("https://example.com/continue", redirectResult.Location);
Assert.Equal((int)HttpStatusCode.Found, redirectResult.StatusCode);
}
}

View File

@ -8,7 +8,7 @@
<!-- NuGet package metadata -->
<PackageId>MaksIT.Results</PackageId>
<Version>2.0.6</Version>
<Version>2.1.0</Version>
<Authors>Maksym Sadovnychyy</Authors>
<Company>MAKS-IT</Company>
<Product>MaksIT.Results</Product>
@ -22,6 +22,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MaksIT.Core" Version="1.6.14" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Abstractions" Version="2.3.13" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.14" />
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.14" />
@ -31,9 +32,5 @@
<None Include="..\..\LICENSE.md" Pack="true" PackagePath="\" Link="LICENSE.md" />
<None Include="..\..\README.md" Pack="true" PackagePath="\" Link="README.md" />
<None Include="..\..\CHANGELOG.md" Pack="true" PackagePath="\" Link="CHANGELOG.md" />
<None Include="..\..\assets\badges\**\*" Link="assets\badges\%(RecursiveDir)%(Filename)%(Extension)">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@ -1,4 +1,3 @@
using System.Text.Json;
namespace MaksIT.Results.Mvc;

View File

@ -2,7 +2,6 @@
namespace MaksIT.Results.Mvc;
/// <summary>
/// A machine-readable format for specifying errors in HTTP API responses based on <see href="https://tools.ietf.org/html/rfc7807"/>.
/// </summary>

View File

@ -2,11 +2,30 @@ using Microsoft.AspNetCore.Mvc;
namespace MaksIT.Results.Mvc;
public class RedirectResult(string location) : IActionResult {
public string Location { get; } = location;
/// <summary>
/// HTTP redirect <see cref="IActionResult"/>. Preserves the redirection status code (301/302/303/307/308).
/// </summary>
public class RedirectResult : IActionResult {
/// <summary>Target URL for the <c>Location</c> header.</summary>
public string Location { get; }
/// <summary>HTTP redirection status code.</summary>
public int StatusCode { get; }
/// <summary>Creates a 302 Found redirect.</summary>
public RedirectResult(string location) : this(location, (int)System.Net.HttpStatusCode.Found) {
}
/// <summary>Creates a redirect with an explicit status code.</summary>
public RedirectResult(string location, int statusCode) {
Location = location;
StatusCode = statusCode;
}
public Task ExecuteResultAsync(ActionContext context) {
context.HttpContext.Response.Redirect(Location);
var response = context.HttpContext.Response;
response.StatusCode = StatusCode;
response.Headers["Location"] = Location;
return Task.CompletedTask;
}
}

View File

@ -1,14 +1,9 @@
using Microsoft.AspNetCore.Mvc;
namespace MaksIT.Results.Mvc;
public class StatusCodeResult : IActionResult {
public int StatusCode { get; }
public StatusCodeResult(int statusCode) {
StatusCode = statusCode;
}
public class StatusCodeResult(int statusCode) : IActionResult {
public int StatusCode { get; } = statusCode;
public async Task ExecuteResultAsync(ActionContext context) {
context.HttpContext.Response.StatusCode = StatusCode;

View File

@ -1,6 +1,5 @@
using System.Net;
namespace MaksIT.Results;
public partial class Result {
@ -11,81 +10,71 @@ public partial class Result {
/// Returns a result indicating that the server could not understand the request due to invalid syntax.
/// Corresponds to HTTP status code 400 Bad Request.
/// </summary>
public static Result BadRequest(params string [] messages) {
return new Result(false, [..messages], HttpStatusCode.BadRequest);
}
public static Result BadRequest(params string [] messages) =>
new (false, [..messages], HttpStatusCode.BadRequest);
/// <summary>
/// Returns a result indicating that the client must authenticate itself to get the requested response.
/// Corresponds to HTTP status code 401 Unauthorized.
/// </summary>
public static Result Unauthorized(params string [] messages) {
return new Result(false, [..messages], HttpStatusCode.Unauthorized);
}
public static Result Unauthorized(params string [] messages) =>
new (false, [..messages], HttpStatusCode.Unauthorized);
/// <summary>
/// Returns a result indicating that the client does not have access rights to the content.
/// Corresponds to HTTP status code 403 Forbidden.
/// </summary>
public static Result Forbidden(params string [] messages) {
return new Result(false, [..messages], HttpStatusCode.Forbidden);
}
public static Result Forbidden(params string [] messages) =>
new (false, [..messages], HttpStatusCode.Forbidden);
/// <summary>
/// Returns a result indicating that the server can not find the requested resource.
/// Corresponds to HTTP status code 404 Not Found.
/// </summary>
public static Result NotFound(params string [] messages) {
return new Result(false, [.. messages], HttpStatusCode.NotFound);
}
public static Result NotFound(params string [] messages) =>
new (false, [.. messages], HttpStatusCode.NotFound);
/// <summary>
/// Returns a result indicating that the request could not be completed due to a conflict with the current state of the resource.
/// Corresponds to HTTP status code 409 Conflict.
/// </summary>
public static Result Conflict(params string [] messages) {
return new Result(false, [..messages], HttpStatusCode.Conflict);
}
public static Result Conflict(params string [] messages) =>
new (false, [..messages], HttpStatusCode.Conflict);
/// <summary>
/// Returns a result indicating that the requested resource is no longer available and will not be available again.
/// Corresponds to HTTP status code 410 Gone.
/// </summary>
public static Result Gone(params string [] messages) {
return new Result(false, [..messages], (HttpStatusCode)410); // 410 Gone
}
public static Result Gone(params string [] messages) =>
new (false, [..messages], (HttpStatusCode)410); // 410 Gone
/// <summary>
/// Returns a result indicating that the server cannot process the request because it expects the request to have a defined Content-Length header.
/// Corresponds to HTTP status code 411 Length Required.
/// </summary>
public static Result LengthRequired(params string [] messages) {
return new Result(false, [..messages], (HttpStatusCode)411); // 411 Length Required
}
public static Result LengthRequired(params string [] messages) =>
new (false, [..messages], (HttpStatusCode)411); // 411 Length Required
/// <summary>
/// Returns a result indicating that the server cannot process the request entity because it is too large.
/// Corresponds to HTTP status code 413 Payload Too Large.
/// </summary>
public static Result PayloadTooLarge(params string [] messages) {
return new Result(false, [..messages], (HttpStatusCode)413); // 413 Payload Too Large
}
public static Result PayloadTooLarge(params string [] messages) =>
new (false, [..messages], (HttpStatusCode)413); // 413 Payload Too Large
/// <summary>
/// Returns a result indicating that the server cannot process the request because the URI is too long.
/// Corresponds to HTTP status code 414 URI Too Long.
/// </summary>
public static Result UriTooLong(params string [] messages) {
return new Result(false, [..messages], (HttpStatusCode)414); // 414 URI Too Long
}
public static Result UriTooLong(params string [] messages) =>
new (false, [..messages], (HttpStatusCode)414); // 414 URI Too Long
/// <summary>
/// Returns a result indicating that the server cannot process the request because the media type is unsupported.
/// Corresponds to HTTP status code 415 Unsupported Media Type.
/// </summary>
public static Result UnsupportedMediaType(params string [] messages) {
return new Result(false, [..messages], HttpStatusCode.UnsupportedMediaType);
}
public static Result UnsupportedMediaType(params string [] messages) =>
new (false, [..messages], HttpStatusCode.UnsupportedMediaType);
#endregion
@ -95,153 +84,134 @@ public partial class Result {
/// Returns a result indicating that payment is required to access the requested resource.
/// Corresponds to HTTP status code 402 Payment Required.
/// </summary>
public static Result PaymentRequired(params string [] messages) {
return new Result(false, [..messages], (HttpStatusCode)402); // 402 Payment Required
}
public static Result PaymentRequired(params string [] messages) =>
new (false, [..messages], (HttpStatusCode)402); // 402 Payment Required
/// <summary>
/// Returns a result indicating that the request method is known by the server but is not supported by the target resource.
/// Corresponds to HTTP status code 405 Method Not Allowed.
/// </summary>
public static Result MethodNotAllowed(params string [] messages) {
return new Result(false, [..messages], (HttpStatusCode)405); // 405 Method Not Allowed
}
public static Result MethodNotAllowed(params string [] messages) =>
new (false, [..messages], (HttpStatusCode)405); // 405 Method Not Allowed
/// <summary>
/// Returns a result indicating that the server cannot produce a response matching the list of acceptable values defined in the request headers.
/// Corresponds to HTTP status code 406 Not Acceptable.
/// </summary>
public static Result NotAcceptable(params string [] messages) {
return new Result(false, [..messages], (HttpStatusCode)406); // 406 Not Acceptable
}
public static Result NotAcceptable(params string [] messages) =>
new (false, [..messages], (HttpStatusCode)406); // 406 Not Acceptable
/// <summary>
/// Returns a result indicating that authentication with a proxy is required.
/// Corresponds to HTTP status code 407 Proxy Authentication Required.
/// </summary>
public static Result ProxyAuthenticationRequired(params string [] messages) {
return new Result(false, [..messages], (HttpStatusCode)407); // 407 Proxy Authentication Required
}
public static Result ProxyAuthenticationRequired(params string [] messages) =>
new (false, [..messages], (HttpStatusCode)407); // 407 Proxy Authentication Required
/// <summary>
/// Returns a result indicating that the server timed out waiting for the request.
/// Corresponds to HTTP status code 408 Request Timeout.
/// </summary>
public static Result RequestTimeout(params string [] messages) {
return new Result(false, [..messages], (HttpStatusCode)408); // 408 Request Timeout
}
public static Result RequestTimeout(params string [] messages) =>
new (false, [..messages], (HttpStatusCode)408); // 408 Request Timeout
/// <summary>
/// Returns a result indicating that one or more conditions given in the request header fields evaluated to false.
/// Corresponds to HTTP status code 412 Precondition Failed.
/// </summary>
public static Result PreconditionFailed(params string [] messages) {
return new Result(false, [..messages], (HttpStatusCode)412); // 412 Precondition Failed
}
public static Result PreconditionFailed(params string [] messages) =>
new (false, [..messages], (HttpStatusCode)412); // 412 Precondition Failed
/// <summary>
/// Returns a result indicating that the range specified by the Range header field cannot be fulfilled.
/// Corresponds to HTTP status code 416 Range Not Satisfiable.
/// </summary>
public static Result RangeNotSatisfiable(params string [] messages) {
return new Result(false, [..messages], (HttpStatusCode)416); // 416 Range Not Satisfiable
}
public static Result RangeNotSatisfiable(params string [] messages) =>
new (false, [..messages], (HttpStatusCode)416); // 416 Range Not Satisfiable
/// <summary>
/// Returns a result indicating that the expectation given in the request's Expect header could not be met.
/// Corresponds to HTTP status code 417 Expectation Failed.
/// </summary>
public static Result ExpectationFailed(params string [] messages) {
return new Result(false, [..messages], (HttpStatusCode)417); // 417 Expectation Failed
}
public static Result ExpectationFailed(params string [] messages) =>
new (false, [..messages], (HttpStatusCode)417); // 417 Expectation Failed
/// <summary>
/// Returns a result indicating that the server refuses to brew coffee because it is, permanently, a teapot.
/// Corresponds to HTTP status code 418 I'm a teapot.
/// </summary>
public static Result ImATeapot(params string [] messages) {
return new Result(false, [..messages], (HttpStatusCode)418); // 418 I'm a teapot
}
public static Result ImATeapot(params string [] messages) =>
new (false, [..messages], (HttpStatusCode)418); // 418 I'm a teapot
/// <summary>
/// Returns a result indicating that the request was directed at a server that is not able to produce a response.
/// Corresponds to HTTP status code 421 Misdirected Request.
/// </summary>
public static Result MisdirectedRequest(params string [] messages) {
return new Result(false, [..messages], (HttpStatusCode)421); // 421 Misdirected Request
}
public static Result MisdirectedRequest(params string [] messages) =>
new (false, [..messages], (HttpStatusCode)421); // 421 Misdirected Request
/// <summary>
/// Returns a result indicating that the server cannot process the request due to an illegal request entity.
/// Corresponds to HTTP status code 422 Unprocessable Entity.
/// </summary>
public static Result UnprocessableEntity(params string [] messages) {
return new Result(false, [..messages], (HttpStatusCode)422); // 422 Unprocessable Entity
}
public static Result UnprocessableEntity(params string [] messages) =>
new (false, [..messages], (HttpStatusCode)422); // 422 Unprocessable Entity
/// <summary>
/// Returns a result indicating that access to the target resource is denied because the resource is locked.
/// Corresponds to HTTP status code 423 Locked.
/// </summary>
public static Result Locked(params string [] messages) {
return new Result(false, [..messages], (HttpStatusCode)423); // 423 Locked
}
public static Result Locked(params string [] messages) =>
new (false, [..messages], (HttpStatusCode)423); // 423 Locked
/// <summary>
/// Returns a result indicating that the request failed because it depended on another request and that request failed.
/// Corresponds to HTTP status code 424 Failed Dependency.
/// </summary>
public static Result FailedDependency(params string [] messages) {
return new Result(false, [..messages], (HttpStatusCode)424); // 424 Failed Dependency
}
public static Result FailedDependency(params string [] messages) =>
new (false, [..messages], (HttpStatusCode)424); // 424 Failed Dependency
/// <summary>
/// Returns a result indicating that the server is unwilling to risk processing a request that might be replayed.
/// Corresponds to HTTP status code 425 Too Early.
/// </summary>
public static Result TooEarly(params string [] messages) {
return new Result(false, [..messages], (HttpStatusCode)425); // 425 Too Early
}
public static Result TooEarly(params string [] messages) =>
new (false, [..messages], (HttpStatusCode)425); // 425 Too Early
/// <summary>
/// Returns a result indicating that the server refuses to perform the request using the current protocol.
/// Corresponds to HTTP status code 426 Upgrade Required.
/// </summary>
public static Result UpgradeRequired(params string [] messages) {
return new Result(false, [..messages], (HttpStatusCode)426); // 426 Upgrade Required
}
public static Result UpgradeRequired(params string [] messages) =>
new (false, [..messages], (HttpStatusCode)426); // 426 Upgrade Required
/// <summary>
/// Returns a result indicating that the server requires the request to be conditional.
/// Corresponds to HTTP status code 428 Precondition Required.
/// </summary>
public static Result PreconditionRequired(params string [] messages) {
return new Result(false, [..messages], (HttpStatusCode)428); // 428 Precondition Required
}
public static Result PreconditionRequired(params string [] messages) =>
new (false, [..messages], (HttpStatusCode)428); // 428 Precondition Required
/// <summary>
/// Returns a result indicating that the user has sent too many requests in a given amount of time.
/// Corresponds to HTTP status code 429 Too Many Requests.
/// </summary>
public static Result TooManyRequests(params string [] messages) {
return new Result(false, [..messages], (HttpStatusCode)429); // 429 Too Many Requests
}
public static Result TooManyRequests(params string [] messages) =>
new (false, [..messages], (HttpStatusCode)429); // 429 Too Many Requests
/// <summary>
/// Returns a result indicating that the server is unwilling to process the request because its header fields are too large.
/// Corresponds to HTTP status code 431 Request Header Fields Too Large.
/// </summary>
public static Result RequestHeaderFieldsTooLarge(params string [] messages) {
return new Result(false, [..messages], (HttpStatusCode)431); // 431 Request Header Fields Too Large
}
public static Result RequestHeaderFieldsTooLarge(params string [] messages) =>
new (false, [..messages], (HttpStatusCode)431); // 431 Request Header Fields Too Large
/// <summary>
/// Returns a result indicating that access to the requested resource is denied for legal reasons.
/// Corresponds to HTTP status code 451 Unavailable For Legal Reasons.
/// </summary>
public static Result UnavailableForLegalReasons(params string [] messages) {
return new Result(false, [..messages], (HttpStatusCode)451); // 451 Unavailable For Legal Reasons
}
public static Result UnavailableForLegalReasons(params string [] messages) =>
new (false, [..messages], (HttpStatusCode)451); // 451 Unavailable For Legal Reasons
#endregion
}
@ -254,81 +224,71 @@ public partial class Result<T> : Result {
/// Returns a result indicating that the server could not understand the request due to invalid syntax.
/// Corresponds to HTTP status code 400 Bad Request.
/// </summary>
public static Result<T> BadRequest(T? value, params string [] messages) {
return new Result<T>(value, false, [..messages], HttpStatusCode.BadRequest);
}
public static Result<T> BadRequest(T? value, params string [] messages) =>
new (value, false, [..messages], HttpStatusCode.BadRequest);
/// <summary>
/// Returns a result indicating that the client must authenticate itself to get the requested response.
/// Corresponds to HTTP status code 401 Unauthorized.
/// </summary>
public static Result<T> Unauthorized(T? value, params string [] messages) {
return new Result<T>(value, false, [..messages], HttpStatusCode.Unauthorized);
}
public static Result<T> Unauthorized(T? value, params string [] messages) =>
new (value, false, [..messages], HttpStatusCode.Unauthorized);
/// <summary>
/// Returns a result indicating that the client does not have access rights to the content.
/// Corresponds to HTTP status code 403 Forbidden.
/// </summary>
public static Result<T> Forbidden(T? value, params string [] messages) {
return new Result<T>(value, false, [..messages], HttpStatusCode.Forbidden);
}
public static Result<T> Forbidden(T? value, params string [] messages) =>
new (value, false, [..messages], HttpStatusCode.Forbidden);
/// <summary>
/// Returns a result indicating that the server can not find the requested resource.
/// Corresponds to HTTP status code 404 Not Found.
/// </summary>
public static Result<T> NotFound(T? value, params string [] messages) {
return new Result<T>(value, false, [..messages], HttpStatusCode.NotFound);
}
public static Result<T> NotFound(T? value, params string [] messages) =>
new (value, false, [..messages], HttpStatusCode.NotFound);
/// <summary>
/// Returns a result indicating that the request could not be completed due to a conflict with the current state of the resource.
/// Corresponds to HTTP status code 409 Conflict.
/// </summary>
public static Result<T> Conflict(T? value, params string [] messages) {
return new Result<T>(value, false, [..messages], HttpStatusCode.Conflict);
}
public static Result<T> Conflict(T? value, params string [] messages) =>
new (value, false, [..messages], HttpStatusCode.Conflict);
/// <summary>
/// Returns a result indicating that the requested resource is no longer available and will not be available again.
/// Corresponds to HTTP status code 410 Gone.
/// </summary>
public static Result<T> Gone(T? value, params string [] messages) {
return new Result<T>(value, false, [..messages], (HttpStatusCode)410); // 410 Gone
}
public static Result<T> Gone(T? value, params string [] messages) =>
new (value, false, [..messages], (HttpStatusCode)410); // 410 Gone
/// <summary>
/// Returns a result indicating that the server cannot process the request because it expects the request to have a defined Content-Length header.
/// Corresponds to HTTP status code 411 Length Required.
/// </summary>
public static Result<T> LengthRequired(T? value, params string [] messages) {
return new Result<T>(value, false, [..messages], (HttpStatusCode)411); // 411 Length Required
}
public static Result<T> LengthRequired(T? value, params string [] messages) =>
new (value, false, [..messages], (HttpStatusCode)411); // 411 Length Required
/// <summary>
/// Returns a result indicating that the server cannot process the request entity because it is too large.
/// Corresponds to HTTP status code 413 Payload Too Large.
/// </summary>
public static Result<T> PayloadTooLarge(T? value, params string [] messages) {
return new Result<T>(value, false, [..messages], (HttpStatusCode)413); // 413 Payload Too Large
}
public static Result<T> PayloadTooLarge(T? value, params string [] messages) =>
new (value, false, [..messages], (HttpStatusCode)413); // 413 Payload Too Large
/// <summary>
/// Returns a result indicating that the server cannot process the request because the URI is too long.
/// Corresponds to HTTP status code 414 URI Too Long.
/// </summary>
public static Result<T> UriTooLong(T? value, params string [] messages) {
return new Result<T>(value, false, [..messages], (HttpStatusCode)414); // 414 URI Too Long
}
public static Result<T> UriTooLong(T? value, params string [] messages) =>
new (value, false, [..messages], (HttpStatusCode)414); // 414 URI Too Long
/// <summary>
/// Returns a result indicating that the server cannot process the request because the media type is unsupported.
/// Corresponds to HTTP status code 415 Unsupported Media Type.
/// </summary>
public static Result<T> UnsupportedMediaType(T? value, params string [] messages) {
return new Result<T>(value, false, [..messages], HttpStatusCode.UnsupportedMediaType);
}
public static Result<T> UnsupportedMediaType(T? value, params string [] messages) =>
new (value, false, [..messages], HttpStatusCode.UnsupportedMediaType);
#endregion
@ -338,153 +298,134 @@ public partial class Result<T> : Result {
/// Returns a result indicating that payment is required to access the requested resource.
/// Corresponds to HTTP status code 402 Payment Required.
/// </summary>
public static Result<T> PaymentRequired(T? value, params string [] messages) {
return new Result<T>(value, false, [..messages], (HttpStatusCode)402); // 402 Payment Required
}
public static Result<T> PaymentRequired(T? value, params string [] messages) =>
new (value, false, [..messages], (HttpStatusCode)402); // 402 Payment Required
/// <summary>
/// Returns a result indicating that the request method is known by the server but is not supported by the target resource.
/// Corresponds to HTTP status code 405 Method Not Allowed.
/// </summary>
public static Result<T> MethodNotAllowed(T? value, params string [] messages) {
return new Result<T>(value, false, [..messages], (HttpStatusCode)405); // 405 Method Not Allowed
}
public static Result<T> MethodNotAllowed(T? value, params string [] messages) =>
new (value, false, [..messages], (HttpStatusCode)405); // 405 Method Not Allowed
/// <summary>
/// Returns a result indicating that the server cannot produce a response matching the list of acceptable values defined in the request headers.
/// Corresponds to HTTP status code 406 Not Acceptable.
/// </summary>
public static Result<T> NotAcceptable(T? value, params string [] messages) {
return new Result<T>(value, false, [..messages], (HttpStatusCode)406); // 406 Not Acceptable
}
public static Result<T> NotAcceptable(T? value, params string [] messages) =>
new (value, false, [..messages], (HttpStatusCode)406); // 406 Not Acceptable
/// <summary>
/// Returns a result indicating that authentication with a proxy is required.
/// Corresponds to HTTP status code 407 Proxy Authentication Required.
/// </summary>
public static Result<T> ProxyAuthenticationRequired(T? value, params string [] messages) {
return new Result<T>(value, false, [..messages], (HttpStatusCode)407); // 407 Proxy Authentication Required
}
public static Result<T> ProxyAuthenticationRequired(T? value, params string [] messages) =>
new (value, false, [..messages], (HttpStatusCode)407); // 407 Proxy Authentication Required
/// <summary>
/// Returns a result indicating that the server timed out waiting for the request.
/// Corresponds to HTTP status code 408 Request Timeout.
/// </summary>
public static Result<T> RequestTimeout(T? value, params string [] messages) {
return new Result<T>(value, false, [..messages], (HttpStatusCode)408); // 408 Request Timeout
}
public static Result<T> RequestTimeout(T? value, params string [] messages) =>
new (value, false, [..messages], (HttpStatusCode)408); // 408 Request Timeout
/// <summary>
/// Returns a result indicating that one or more conditions given in the request header fields evaluated to false.
/// Corresponds to HTTP status code 412 Precondition Failed.
/// </summary>
public static Result<T> PreconditionFailed(T? value, params string [] messages) {
return new Result<T>(value, false, [..messages], (HttpStatusCode)412); // 412 Precondition Failed
}
public static Result<T> PreconditionFailed(T? value, params string [] messages) =>
new (value, false, [..messages], (HttpStatusCode)412); // 412 Precondition Failed
/// <summary>
/// Returns a result indicating that the range specified by the Range header field cannot be fulfilled.
/// Corresponds to HTTP status code 416 Range Not Satisfiable.
/// </summary>
public static Result<T> RangeNotSatisfiable(T? value, params string [] messages) {
return new Result<T>(value, false, [..messages], (HttpStatusCode)416); // 416 Range Not Satisfiable
}
public static Result<T> RangeNotSatisfiable(T? value, params string [] messages) =>
new (value, false, [..messages], (HttpStatusCode)416); // 416 Range Not Satisfiable
/// <summary>
/// Returns a result indicating that the expectation given in the request's Expect header could not be met.
/// Corresponds to HTTP status code 417 Expectation Failed.
/// </summary>
public static Result<T> ExpectationFailed(T? value, params string [] messages) {
return new Result<T>(value, false, [..messages], (HttpStatusCode)417); // 417 Expectation Failed
}
public static Result<T> ExpectationFailed(T? value, params string [] messages) =>
new (value, false, [..messages], (HttpStatusCode)417); // 417 Expectation Failed
/// <summary>
/// Returns a result indicating that the server refuses to brew coffee because it is, permanently, a teapot.
/// Corresponds to HTTP status code 418 I'm a teapot.
/// </summary>
public static Result<T> ImATeapot(T? value, params string [] messages) {
return new Result<T>(value, false, [..messages], (HttpStatusCode)418); // 418 I'm a teapot
}
public static Result<T> ImATeapot(T? value, params string [] messages) =>
new (value, false, [..messages], (HttpStatusCode)418); // 418 I'm a teapot
/// <summary>
/// Returns a result indicating that the request was directed at a server that is not able to produce a response.
/// Corresponds to HTTP status code 421 Misdirected Request.
/// </summary>
public static Result<T> MisdirectedRequest(T? value, params string [] messages) {
return new Result<T>(value, false, [..messages], (HttpStatusCode)421); // 421 Misdirected Request
}
public static Result<T> MisdirectedRequest(T? value, params string [] messages) =>
new (value, false, [..messages], (HttpStatusCode)421); // 421 Misdirected Request
/// <summary>
/// Returns a result indicating that the server cannot process the request due to an illegal request entity.
/// Corresponds to HTTP status code 422 Unprocessable Entity.
/// </summary>
public static Result<T> UnprocessableEntity(T? value, params string [] messages) {
return new Result<T>(value, false, [..messages], (HttpStatusCode)422); // 422 Unprocessable Entity
}
public static Result<T> UnprocessableEntity(T? value, params string [] messages) =>
new (value, false, [..messages], (HttpStatusCode)422); // 422 Unprocessable Entity
/// <summary>
/// Returns a result indicating that access to the target resource is denied because the resource is locked.
/// Corresponds to HTTP status code 423 Locked.
/// </summary>
public static Result<T> Locked(T? value, params string [] messages) {
return new Result<T>(value, false, [..messages], (HttpStatusCode)423); // 423 Locked
}
public static Result<T> Locked(T? value, params string [] messages) =>
new (value, false, [..messages], (HttpStatusCode)423); // 423 Locked
/// <summary>
/// Returns a result indicating that the request failed because it depended on another request and that request failed.
/// Corresponds to HTTP status code 424 Failed Dependency.
/// </summary>
public static Result<T> FailedDependency(T? value, params string [] messages) {
return new Result<T>(value, false, [..messages], (HttpStatusCode)424); // 424 Failed Dependency
}
public static Result<T> FailedDependency(T? value, params string [] messages) =>
new (value, false, [..messages], (HttpStatusCode)424); // 424 Failed Dependency
/// <summary>
/// Returns a result indicating that the server is unwilling to risk processing a request that might be replayed.
/// Corresponds to HTTP status code 425 Too Early.
/// </summary>
public static Result<T> TooEarly(T? value, params string [] messages) {
return new Result<T>(value, false, [..messages], (HttpStatusCode)425); // 425 Too Early
}
public static Result<T> TooEarly(T? value, params string [] messages) =>
new (value, false, [..messages], (HttpStatusCode)425); // 425 Too Early
/// <summary>
/// Returns a result indicating that the server refuses to perform the request using the current protocol.
/// Corresponds to HTTP status code 426 Upgrade Required.
/// </summary>
public static Result<T> UpgradeRequired(T? value, params string [] messages) {
return new Result<T>(value, false, [..messages], (HttpStatusCode)426); // 426 Upgrade Required
}
public static Result<T> UpgradeRequired(T? value, params string [] messages) =>
new (value, false, [..messages], (HttpStatusCode)426); // 426 Upgrade Required
/// <summary>
/// Returns a result indicating that the server requires the request to be conditional.
/// Corresponds to HTTP status code 428 Precondition Required.
/// </summary>
public static Result<T> PreconditionRequired(T? value, params string [] messages) {
return new Result<T>(value, false, [..messages], (HttpStatusCode)428); // 428 Precondition Required
}
public static Result<T> PreconditionRequired(T? value, params string [] messages) =>
new (value, false, [..messages], (HttpStatusCode)428); // 428 Precondition Required
/// <summary>
/// Returns a result indicating that the user has sent too many requests in a given amount of time.
/// Corresponds to HTTP status code 429 Too Many Requests.
/// </summary>
public static Result<T> TooManyRequests(T? value, params string [] messages) {
return new Result<T>(value, false, [..messages], (HttpStatusCode)429); // 429 Too Many Requests
}
public static Result<T> TooManyRequests(T? value, params string [] messages) =>
new (value, false, [..messages], (HttpStatusCode)429); // 429 Too Many Requests
/// <summary>
/// Returns a result indicating that the server is unwilling to process the request because its header fields are too large.
/// Corresponds to HTTP status code 431 Request Header Fields Too Large.
/// </summary>
public static Result<T> RequestHeaderFieldsTooLarge(T? value, params string [] messages) {
return new Result<T>(value, false, [..messages], (HttpStatusCode)431); // 431 Request Header Fields Too Large
}
public static Result<T> RequestHeaderFieldsTooLarge(T? value, params string [] messages) =>
new (value, false, [..messages], (HttpStatusCode)431); // 431 Request Header Fields Too Large
/// <summary>
/// Returns a result indicating that access to the requested resource is denied for legal reasons.
/// Corresponds to HTTP status code 451 Unavailable For Legal Reasons.
/// </summary>
public static Result<T> UnavailableForLegalReasons(T? value, params string [] messages) {
return new Result<T>(value, false, [..messages], (HttpStatusCode)451); // 451 Unavailable For Legal Reasons
}
public static Result<T> UnavailableForLegalReasons(T? value, params string [] messages) =>
new (value, false, [..messages], (HttpStatusCode)451); // 451 Unavailable For Legal Reasons
#endregion
}

View File

@ -0,0 +1,253 @@
using System.Net;
using System.Text.Json;
using Microsoft.AspNetCore.Mvc;
using MaksIT.Core.Extensions;
using MaksIT.Results.Mvc;
namespace MaksIT.Results;
public partial class Result {
/// <summary>
/// Reconstructs a <see cref="Result"/> from an <see cref="IActionResult"/> produced by <see cref="ToActionResult"/>.
/// </summary>
public static Result FromActionResult(IActionResult actionResult) {
ArgumentNullException.ThrowIfNull(actionResult);
switch (actionResult) {
case StatusCodeResult statusCodeResult:
return new (true, [], (HttpStatusCode)statusCodeResult.StatusCode);
case RedirectResult redirectResult:
return new (true, [redirectResult.Location], (HttpStatusCode)redirectResult.StatusCode);
case ObjectResult { Value: ProblemDetails problem } objectResult:
return FromProblemDetails(problem, objectResult.StatusCode);
case ObjectResult objectResult:
var status = (HttpStatusCode)(objectResult.StatusCode ?? (int)HttpStatusCode.OK);
if (IsSuccessStatusCode(status))
return new (true, [], status);
return new (false, ["Response indicated failure without ProblemDetails."], status);
default:
return InternalServerError($"Unsupported IActionResult type: {actionResult.GetType().FullName}.");
}
}
/// <summary>
/// Reconstructs a <see cref="Result"/> from an HTTP response written in the <see cref="ToActionResult"/> wire shape
/// (status-only success, <c>application/problem+json</c> failures).
/// </summary>
public static Task<Result> FromHttpResponseAsync(HttpResponseMessage response, CancellationToken cancellationToken = default) {
ArgumentNullException.ThrowIfNull(response);
return FromHttpResponseCoreAsync(response, cancellationToken);
}
private static async Task<Result> FromHttpResponseCoreAsync(HttpResponseMessage response, CancellationToken cancellationToken) {
var status = response.StatusCode;
if (IsRedirectionStatusCode(status)) {
var location = response.Headers.Location?.ToString();
if (!string.IsNullOrWhiteSpace(location))
return new (true, [location], status);
return new (false, ["Redirect response missing Location header."], status);
}
if (IsSuccessStatusCode(status))
return new (true, [], status);
return await FailFromHttpContentAsync(response, status, cancellationToken).ConfigureAwait(false);
}
internal static bool IsRedirectionStatusCode(HttpStatusCode statusCode) =>
statusCode is HttpStatusCode.MultipleChoices
or HttpStatusCode.MovedPermanently
or HttpStatusCode.Found
or HttpStatusCode.SeeOther
or HttpStatusCode.TemporaryRedirect
or HttpStatusCode.PermanentRedirect;
internal static bool IsSuccessStatusCode(HttpStatusCode statusCode) {
var code = (int)statusCode;
return code is >= 200 and <= 299;
}
internal static Result Create(bool isSuccess, List<string> messages, HttpStatusCode statusCode) =>
new(isSuccess, messages, statusCode);
internal static Result FromProblemDetails(ProblemDetails problem, int? statusCodeOverride) {
var status = (HttpStatusCode)(statusCodeOverride ?? problem.Status ?? (int)HttpStatusCode.InternalServerError);
var messages = ReadProblemMessages(problem);
return Create(false, messages, status);
}
internal static List<string> ReadProblemMessages(ProblemDetails problem) {
if (!string.IsNullOrWhiteSpace(problem.Detail)) {
var parts = problem.Detail.Split("; ", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (parts.Length > 0)
return [.. parts];
return [problem.Detail];
}
if (!string.IsNullOrWhiteSpace(problem.Title))
return [problem.Title];
return ["An error occurred"];
}
internal static async Task<Result> FailFromHttpContentAsync(
HttpResponseMessage response,
HttpStatusCode status,
CancellationToken cancellationToken) {
var body = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
if (TryReadProblemDetails(response.Content.Headers.ContentType?.MediaType, body, out var problem))
return FromProblemDetails(problem, (int)status);
if (string.IsNullOrWhiteSpace(body))
return new (false, [$"HTTP {(int)status}."], status);
return new (false, [Truncate($"HTTP {(int)status}. Response: {body}")], status);
}
internal static bool TryReadProblemDetails(string? mediaType, string body, out ProblemDetails problem) {
problem = null!;
if (string.IsNullOrWhiteSpace(body))
return false;
var isProblemJson = mediaType is not null
&& mediaType.Equals("application/problem+json", StringComparison.OrdinalIgnoreCase);
if (!isProblemJson && !LooksLikeJsonObject(body))
return false;
try {
var parsed = body.ToObject<ProblemDetails>();
if (parsed is null)
return false;
if (string.IsNullOrWhiteSpace(parsed.Detail) && string.IsNullOrWhiteSpace(parsed.Title) && parsed.Status is null)
return false;
problem = parsed;
return true;
}
catch (JsonException) {
return false;
}
}
internal static bool LooksLikeJsonObject(string body) {
var trimmed = body.AsSpan().TrimStart();
return trimmed.Length > 0 && trimmed[0] == '{';
}
internal static string Truncate(string value) {
const int maxLen = 500;
return value.Length <= maxLen ? value : value[..maxLen] + "…";
}
}
public partial class Result<T> {
/// <summary>
/// Reconstructs a <see cref="Result{T}"/> from an <see cref="IActionResult"/> produced by <see cref="ToActionResult"/>.
/// </summary>
public new static Result<T> FromActionResult(IActionResult actionResult) {
ArgumentNullException.ThrowIfNull(actionResult);
switch (actionResult) {
case RedirectResult redirectResult when typeof(T) == typeof(string):
return new ((T)(object)redirectResult.Location, true, [], (HttpStatusCode)redirectResult.StatusCode);
case RedirectResult redirectResult:
return new (default, false, ["Redirect Location cannot be mapped to the requested result type."], (HttpStatusCode)redirectResult.StatusCode);
case StatusCodeResult statusCodeResult:
return new (default, true, [], (HttpStatusCode)statusCodeResult.StatusCode);
case ObjectResult { Value: ProblemDetails problem } objectResult:
var failed = Result.FromProblemDetails(problem, objectResult.StatusCode);
return new (default, false, failed.Messages, failed.StatusCode);
case ObjectResult { Value: T value } objectResult:
return new (value, true, [], (HttpStatusCode)(objectResult.StatusCode ?? (int)HttpStatusCode.OK));
case ObjectResult { Value: not null } objectResult:
var status = (HttpStatusCode)(objectResult.StatusCode ?? (int)HttpStatusCode.OK);
if (!Result.IsSuccessStatusCode(status)) {
var failure = Result.Create(false, ["Response indicated failure without ProblemDetails."], status);
return new (default, false, failure.Messages, failure.StatusCode);
}
try {
var converted = objectResult.Value.ToJson().ToObject<T>();
return new (converted, true, [], status);
}
catch (JsonException ex) {
return new (default, false, [$"Could not map ObjectResult value to {typeof(T).Name}: {ex.Message}"], status);
}
case ObjectResult objectResult:
return new (default, true, [], (HttpStatusCode)(objectResult.StatusCode ?? (int)HttpStatusCode.OK));
default:
return new (default, false, [$"Unsupported IActionResult type: {actionResult.GetType().FullName}."], HttpStatusCode.InternalServerError);
}
}
/// <summary>
/// Reconstructs a <see cref="Result{T}"/> from an HTTP response written in the <see cref="Result.ToActionResult"/> wire shape
/// (JSON body, redirect <c>Location</c>, or <c>application/problem+json</c>).
/// </summary>
public new static Task<Result<T>> FromHttpResponseAsync(HttpResponseMessage response, CancellationToken cancellationToken = default) {
ArgumentNullException.ThrowIfNull(response);
return FromHttpResponseCoreAsync(response, cancellationToken);
}
private static async Task<Result<T>> FromHttpResponseCoreAsync(HttpResponseMessage response, CancellationToken cancellationToken) {
var status = response.StatusCode;
if (Result.IsRedirectionStatusCode(status)) {
var location = response.Headers.Location?.ToString();
if (typeof(T) == typeof(string) && !string.IsNullOrWhiteSpace(location))
return new ((T)(object)location, true, [], status);
return new (default, false, [
string.IsNullOrWhiteSpace(location)
? "Redirect response missing Location header."
: "Redirect Location cannot be mapped to the requested result type."
], status);
}
if (!Result.IsSuccessStatusCode(status)) {
var failed = await Result.FailFromHttpContentAsync(response, status, cancellationToken).ConfigureAwait(false);
return new (default, false, failed.Messages, failed.StatusCode);
}
if (typeof(T) == typeof(object))
return new (default, true, [], status);
var body = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
if (typeof(T) == typeof(string)) {
if (string.IsNullOrWhiteSpace(body))
return new (default, false, ["Response body was empty."], status);
return new ((T)(object)body, true, [], status);
}
if (string.IsNullOrWhiteSpace(body))
return new (default, true, [], status);
try {
var value = body.ToObject<T>();
return new (value, true, [], status);
}
catch (JsonException ex) {
return new (default, false, [$"Could not deserialize response as {typeof(T).Name}: {ex.Message}"], status);
}
}
}

View File

@ -1,6 +1,5 @@
using System.Net;
namespace MaksIT.Results;
public partial class Result {
@ -11,17 +10,15 @@ public partial class Result {
/// Returns a result indicating that the initial part of a request has been received and the client should continue with the request.
/// Corresponds to HTTP status code 100 Continue.
/// </summary>
public static Result Continue(params string [] messages) {
return new Result(true, [..messages], HttpStatusCode.Continue);
}
public static Result Continue(params string [] messages) =>
new (true, [..messages], HttpStatusCode.Continue);
/// <summary>
/// Returns a result indicating that the server is switching to a different protocol as requested by the client.
/// Corresponds to HTTP status code 101 Switching Protocols.
/// </summary>
public static Result SwitchingProtocols(params string [] messages) {
return new Result(true, [..messages], HttpStatusCode.SwitchingProtocols);
}
public static Result SwitchingProtocols(params string [] messages) =>
new (true, [..messages], HttpStatusCode.SwitchingProtocols);
#endregion
@ -31,17 +28,15 @@ public partial class Result {
/// Returns a result indicating that the server has received and is processing the request, but no response is available yet.
/// Corresponds to HTTP status code 102 Processing.
/// </summary>
public static Result Processing(params string [] messages) {
return new Result(true, [..messages], HttpStatusCode.Processing);
}
public static Result Processing(params string [] messages) =>
new (true, [..messages], HttpStatusCode.Processing);
/// <summary>
/// Returns a result indicating that the server is sending information about early hints that may be used by the client to begin preloading resources while the server prepares a final response.
/// Corresponds to HTTP status code 103 Early Hints.
/// </summary>
public static Result EarlyHints(params string [] messages) {
return new Result(true, [..messages], (HttpStatusCode)103); // Early Hints is not defined in HttpStatusCode enum, 103 is the official code
}
public static Result EarlyHints(params string [] messages) =>
new (true, [..messages], (HttpStatusCode)103); // Early Hints is not defined in HttpStatusCode enum, 103 is the official code
#endregion
}
@ -54,17 +49,15 @@ public partial class Result<T> : Result {
/// Returns a result indicating that the initial part of a request has been received and the client should continue with the request.
/// Corresponds to HTTP status code 100 Continue.
/// </summary>
public static Result<T> Continue(T? value, params string [] messages) {
return new Result<T>(value, true, [..messages], HttpStatusCode.Continue);
}
public static Result<T> Continue(T? value, params string [] messages) =>
new (value, true, [..messages], HttpStatusCode.Continue);
/// <summary>
/// Returns a result indicating that the server is switching to a different protocol as requested by the client.
/// Corresponds to HTTP status code 101 Switching Protocols.
/// </summary>
public static Result<T> SwitchingProtocols(T? value, params string [] messages) {
return new Result<T>(value, true, [..messages], HttpStatusCode.SwitchingProtocols);
}
public static Result<T> SwitchingProtocols(T? value, params string [] messages) =>
new (value, true, [..messages], HttpStatusCode.SwitchingProtocols);
#endregion
@ -74,17 +67,15 @@ public partial class Result<T> : Result {
/// Returns a result indicating that the server has received and is processing the request, but no response is available yet.
/// Corresponds to HTTP status code 102 Processing.
/// </summary>
public static Result<T> Processing(T? value, params string [] messages) {
return new Result<T>(value, true, [..messages], HttpStatusCode.Processing);
}
public static Result<T> Processing(T? value, params string [] messages) =>
new (value, true, [..messages], HttpStatusCode.Processing);
/// <summary>
/// Returns a result indicating that the server is sending information about early hints that may be used by the client to begin preloading resources while the server prepares a final response.
/// Corresponds to HTTP status code 103 Early Hints.
/// </summary>
public static Result<T> EarlyHints(T? value, params string [] messages) {
return new Result<T>(value, true, [..messages], (HttpStatusCode)103); // Early Hints is not defined in HttpStatusCode enum, 103 is the official code
}
public static Result<T> EarlyHints(T? value, params string [] messages) =>
new (value, true, [..messages], (HttpStatusCode)103); // Early Hints is not defined in HttpStatusCode enum, 103 is the official code
#endregion
}

View File

@ -1,5 +1,4 @@
using System.Net;
using System.Net;
namespace MaksIT.Results;
@ -9,65 +8,57 @@ public partial class Result {
/// Returns a result indicating that the request has multiple options, and the user or user-agent should select one of them.
/// Corresponds to HTTP status code 300 Multiple Choices.
/// </summary>
public static Result MultipleChoices(params string [] messages) {
return new Result(true, [..messages], HttpStatusCode.MultipleChoices);
}
public static Result MultipleChoices(params string [] messages) =>
new (true, [..messages], HttpStatusCode.MultipleChoices);
/// <summary>
/// Returns a result indicating that the requested resource has been permanently moved to a new URI.
/// Corresponds to HTTP status code 301 Moved Permanently.
/// </summary>
public static Result MovedPermanently(params string [] messages) {
return new Result(true, [..messages], HttpStatusCode.MovedPermanently);
}
public static Result MovedPermanently(params string [] messages) =>
new (true, [..messages], HttpStatusCode.MovedPermanently);
/// <summary>
/// Returns a result indicating that the requested resource resides temporarily under a different URI.
/// Corresponds to HTTP status code 302 Found.
/// </summary>
public static Result Found(params string [] messages) {
return new Result(true, [..messages], HttpStatusCode.Found);
}
public static Result Found(params string [] messages) =>
new (true, [..messages], HttpStatusCode.Found);
/// <summary>
/// Returns a result indicating that the response to the request can be found under another URI using the GET method.
/// Corresponds to HTTP status code 303 See Other.
/// </summary>
public static Result SeeOther(params string [] messages) {
return new Result(true, [..messages], HttpStatusCode.SeeOther);
}
public static Result SeeOther(params string [] messages) =>
new (true, [..messages], HttpStatusCode.SeeOther);
/// <summary>
/// Returns a result indicating that the requested resource has not been modified since the last request.
/// Corresponds to HTTP status code 304 Not Modified.
/// </summary>
public static Result NotModified(params string [] messages) {
return new Result(true, [..messages], HttpStatusCode.NotModified);
}
public static Result NotModified(params string [] messages) =>
new (true, [..messages], HttpStatusCode.NotModified);
/// <summary>
/// Returns a result indicating that the requested resource must be accessed through the proxy given by the location field.
/// Corresponds to HTTP status code 305 Use Proxy.
/// </summary>
public static Result UseProxy(params string [] messages) {
return new Result(true, [..messages], HttpStatusCode.UseProxy);
}
public static Result UseProxy(params string [] messages) =>
new (true, [..messages], HttpStatusCode.UseProxy);
/// <summary>
/// Returns a result indicating that the requested resource resides temporarily under a different URI, but future requests should still use the original URI.
/// Corresponds to HTTP status code 307 Temporary Redirect.
/// </summary>
public static Result TemporaryRedirect(params string [] messages) {
return new Result(true, [..messages], HttpStatusCode.TemporaryRedirect);
}
public static Result TemporaryRedirect(params string [] messages) =>
new (true, [..messages], HttpStatusCode.TemporaryRedirect);
/// <summary>
/// Returns a result indicating that the requested resource has been permanently moved to a new URI, and future references should use the new URI.
/// Corresponds to HTTP status code 308 Permanent Redirect.
/// </summary>
public static Result PermanentRedirect(params string [] messages) {
return new Result(true, [..messages], HttpStatusCode.PermanentRedirect);
}
public static Result PermanentRedirect(params string [] messages) =>
new (true, [..messages], HttpStatusCode.PermanentRedirect);
}
public partial class Result<T> : Result {
@ -76,63 +67,55 @@ public partial class Result<T> : Result {
/// Returns a result indicating that the request has multiple options, and the user or user-agent should select one of them.
/// Corresponds to HTTP status code 300 Multiple Choices.
/// </summary>
public static Result<T> MultipleChoices(T? value, params string [] messages) {
return new Result<T>(value, true, [..messages], HttpStatusCode.MultipleChoices);
}
public static Result<T> MultipleChoices(T? value, params string [] messages) =>
new (value, true, [..messages], HttpStatusCode.MultipleChoices);
/// <summary>
/// Returns a result indicating that the requested resource has been permanently moved to a new URI.
/// Corresponds to HTTP status code 301 Moved Permanently.
/// </summary>
public static Result<T> MovedPermanently(T? value, params string [] messages) {
return new Result<T>(value, true, [..messages], HttpStatusCode.MovedPermanently);
}
public static Result<T> MovedPermanently(T? value, params string [] messages) =>
new (value, true, [..messages], HttpStatusCode.MovedPermanently);
/// <summary>
/// Returns a result indicating that the requested resource resides temporarily under a different URI.
/// Corresponds to HTTP status code 302 Found.
/// </summary>
public static Result<T> Found(T? value, params string [] messages) {
return new Result<T>(value, true, [..messages], HttpStatusCode.Found);
}
public static Result<T> Found(T? value, params string [] messages) =>
new (value, true, [..messages], HttpStatusCode.Found);
/// <summary>
/// Returns a result indicating that the response to the request can be found under another URI using the GET method.
/// Corresponds to HTTP status code 303 See Other.
/// </summary>
public static Result<T> SeeOther(T? value, params string [] messages) {
return new Result<T>(value, true, [..messages], HttpStatusCode.SeeOther);
}
public static Result<T> SeeOther(T? value, params string [] messages) =>
new (value, true, [..messages], HttpStatusCode.SeeOther);
/// <summary>
/// Returns a result indicating that the requested resource has not been modified since the last request.
/// Corresponds to HTTP status code 304 Not Modified.
/// </summary>
public static Result<T> NotModified(T? value, params string [] messages) {
return new Result<T>(value, true, [..messages], HttpStatusCode.NotModified);
}
public static Result<T> NotModified(T? value, params string [] messages) =>
new (value, true, [..messages], HttpStatusCode.NotModified);
/// <summary>
/// Returns a result indicating that the requested resource must be accessed through the proxy given by the location field.
/// Corresponds to HTTP status code 305 Use Proxy.
/// </summary>
public static Result<T> UseProxy(T? value, params string [] messages) {
return new Result<T>(value, true, [..messages], HttpStatusCode.UseProxy);
}
public static Result<T> UseProxy(T? value, params string [] messages) =>
new (value, true, [..messages], HttpStatusCode.UseProxy);
/// <summary>
/// Returns a result indicating that the requested resource resides temporarily under a different URI, but future requests should still use the original URI.
/// Corresponds to HTTP status code 307 Temporary Redirect.
/// </summary>
public static Result<T> TemporaryRedirect(T? value, params string [] messages) {
return new Result<T>(value, true, [..messages], HttpStatusCode.TemporaryRedirect);
}
public static Result<T> TemporaryRedirect(T? value, params string [] messages) =>
new (value, true, [..messages], HttpStatusCode.TemporaryRedirect);
/// <summary>
/// Returns a result indicating that the requested resource has been permanently moved to a new URI, and future references should use the new URI.
/// Corresponds to HTTP status code 308 Permanent Redirect.
/// </summary>
public static Result<T> PermanentRedirect(T? value, params string [] messages) {
return new Result<T>(value, true, [..messages], HttpStatusCode.PermanentRedirect);
}
public static Result<T> PermanentRedirect(T? value, params string [] messages) =>
new (value, true, [..messages], HttpStatusCode.PermanentRedirect);
}

View File

@ -10,49 +10,43 @@ public partial class Result {
/// Returns a result indicating the server encountered an unexpected condition that prevented it from fulfilling the request.
/// Corresponds to HTTP status code 500 Internal Server Error.
/// </summary>
public static Result InternalServerError(params string [] messages) {
return new Result(false, [..messages], HttpStatusCode.InternalServerError);
}
public static Result InternalServerError(params string [] messages) =>
new (false, [..messages], HttpStatusCode.InternalServerError);
/// <summary>
/// Returns a result indicating the server does not support the functionality required to fulfill the request.
/// Corresponds to HTTP status code 501 Not Implemented.
/// </summary>
public static Result NotImplemented(params string [] messages) {
return new Result(false, [..messages], HttpStatusCode.NotImplemented);
}
public static Result NotImplemented(params string [] messages) =>
new (false, [..messages], HttpStatusCode.NotImplemented);
/// <summary>
/// Returns a result indicating the server, while acting as a gateway or proxy, received an invalid response from the upstream server.
/// Corresponds to HTTP status code 502 Bad Gateway.
/// </summary>
public static Result BadGateway(params string [] messages) {
return new Result(false, [..messages], HttpStatusCode.BadGateway);
}
public static Result BadGateway(params string [] messages) =>
new (false, [..messages], HttpStatusCode.BadGateway);
/// <summary>
/// Returns a result indicating the server is currently unable to handle the request due to temporary overload or maintenance of the server.
/// Corresponds to HTTP status code 503 Service Unavailable.
/// </summary>
public static Result ServiceUnavailable(params string [] messages) {
return new Result(false, [..messages], HttpStatusCode.ServiceUnavailable);
}
public static Result ServiceUnavailable(params string [] messages) =>
new (false, [..messages], HttpStatusCode.ServiceUnavailable);
/// <summary>
/// Returns a result indicating the server, while acting as a gateway or proxy, did not receive a timely response from the upstream server.
/// Corresponds to HTTP status code 504 Gateway Timeout.
/// </summary>
public static Result GatewayTimeout(params string [] messages) {
return new Result(false, [..messages], HttpStatusCode.GatewayTimeout);
}
public static Result GatewayTimeout(params string [] messages) =>
new (false, [..messages], HttpStatusCode.GatewayTimeout);
/// <summary>
/// Returns a result indicating the server does not support the HTTP protocol version used in the request.
/// Corresponds to HTTP status code 505 HTTP Version Not Supported.
/// </summary>
public static Result HttpVersionNotSupported(params string [] messages) {
return new Result(false, [..messages], HttpStatusCode.HttpVersionNotSupported);
}
public static Result HttpVersionNotSupported(params string [] messages) =>
new (false, [..messages], HttpStatusCode.HttpVersionNotSupported);
#endregion
@ -62,41 +56,36 @@ public partial class Result {
/// Returns a result indicating the server has an internal configuration error: the chosen variant resource is configured to engage in transparent content negotiation itself, and is therefore not a proper end point in the negotiation process.
/// Corresponds to HTTP status code 506 Variant Also Negotiates.
/// </summary>
public static Result VariantAlsoNegotiates(params string [] messages) {
return new Result(false, [..messages], HttpStatusCode.VariantAlsoNegotiates);
}
public static Result VariantAlsoNegotiates(params string [] messages) =>
new (false, [..messages], HttpStatusCode.VariantAlsoNegotiates);
/// <summary>
/// Returns a result indicating the server is unable to store the representation needed to complete the request.
/// Corresponds to HTTP status code 507 Insufficient Storage.
/// </summary>
public static Result InsufficientStorage(params string [] messages) {
return new Result(false, [..messages], HttpStatusCode.InsufficientStorage);
}
public static Result InsufficientStorage(params string [] messages) =>
new (false, [..messages], HttpStatusCode.InsufficientStorage);
/// <summary>
/// Returns a result indicating the server detected an infinite loop while processing a request with depth: infinity. Usually encountered in WebDAV scenarios.
/// Corresponds to HTTP status code 508 Loop Detected.
/// </summary>
public static Result LoopDetected(params string [] messages) {
return new Result(false, [..messages], HttpStatusCode.LoopDetected);
}
public static Result LoopDetected(params string [] messages) =>
new (false, [..messages], HttpStatusCode.LoopDetected);
/// <summary>
/// Returns a result indicating further extensions to the request are required for the server to fulfill it.
/// Corresponds to HTTP status code 510 Not Extended.
/// </summary>
public static Result NotExtended(params string [] messages) {
return new Result(false, [..messages], HttpStatusCode.NotExtended);
}
public static Result NotExtended(params string [] messages) =>
new (false, [..messages], HttpStatusCode.NotExtended);
/// <summary>
/// Returns a result indicating the client needs to authenticate to gain network access.
/// Corresponds to HTTP status code 511 Network Authentication Required.
/// </summary>
public static Result NetworkAuthenticationRequired(params string [] messages) {
return new Result(false, [..messages], HttpStatusCode.NetworkAuthenticationRequired);
}
public static Result NetworkAuthenticationRequired(params string [] messages) =>
new (false, [..messages], HttpStatusCode.NetworkAuthenticationRequired);
#endregion
}
@ -109,49 +98,43 @@ public partial class Result<T> : Result {
/// Returns a result indicating the server encountered an unexpected condition that prevented it from fulfilling the request.
/// Corresponds to HTTP status code 500 Internal Server Error.
/// </summary>
public static Result<T> InternalServerError(T? value, params string [] messages) {
return new Result<T>(value, false, [..messages], HttpStatusCode.InternalServerError);
}
public static Result<T> InternalServerError(T? value, params string [] messages) =>
new (value, false, [..messages], HttpStatusCode.InternalServerError);
/// <summary>
/// Returns a result indicating the server does not support the functionality required to fulfill the request.
/// Corresponds to HTTP status code 501 Not Implemented.
/// </summary>
public static Result<T> NotImplemented(T? value, params string [] messages) {
return new Result<T>(value, false, [..messages], HttpStatusCode.NotImplemented);
}
public static Result<T> NotImplemented(T? value, params string [] messages) =>
new (value, false, [..messages], HttpStatusCode.NotImplemented);
/// <summary>
/// Returns a result indicating the server, while acting as a gateway or proxy, received an invalid response from the upstream server.
/// Corresponds to HTTP status code 502 Bad Gateway.
/// </summary>
public static Result<T> BadGateway(T? value, params string [] messages) {
return new Result<T>(value, false, [..messages], HttpStatusCode.BadGateway);
}
public static Result<T> BadGateway(T? value, params string [] messages) =>
new (value, false, [..messages], HttpStatusCode.BadGateway);
/// <summary>
/// Returns a result indicating the server is currently unable to handle the request due to temporary overload or maintenance of the server.
/// Corresponds to HTTP status code 503 Service Unavailable.
/// </summary>
public static Result<T> ServiceUnavailable(T? value, params string [] messages) {
return new Result<T>(value, false, [..messages], HttpStatusCode.ServiceUnavailable);
}
public static Result<T> ServiceUnavailable(T? value, params string [] messages) =>
new (value, false, [..messages], HttpStatusCode.ServiceUnavailable);
/// <summary>
/// Returns a result indicating the server, while acting as a gateway or proxy, did not receive a timely response from the upstream server.
/// Corresponds to HTTP status code 504 Gateway Timeout.
/// </summary>
public static Result<T> GatewayTimeout(T? value, params string [] messages) {
return new Result<T>(value, false, [..messages], HttpStatusCode.GatewayTimeout);
}
public static Result<T> GatewayTimeout(T? value, params string [] messages) =>
new (value, false, [..messages], HttpStatusCode.GatewayTimeout);
/// <summary>
/// Returns a result indicating the server does not support the HTTP protocol version used in the request.
/// Corresponds to HTTP status code 505 HTTP Version Not Supported.
/// </summary>
public static Result<T> HttpVersionNotSupported(T? value, params string [] messages) {
return new Result<T>(value, false, [..messages], HttpStatusCode.HttpVersionNotSupported);
}
public static Result<T> HttpVersionNotSupported(T? value, params string [] messages) =>
new (value, false, [..messages], HttpStatusCode.HttpVersionNotSupported);
#endregion
@ -161,41 +144,36 @@ public partial class Result<T> : Result {
/// Returns a result indicating the server has an internal configuration error: the chosen variant resource is configured to engage in transparent content negotiation itself, and is therefore not a proper end point in the negotiation process.
/// Corresponds to HTTP status code 506 Variant Also Negotiates.
/// </summary>
public static Result<T> VariantAlsoNegotiates(T? value, params string [] messages) {
return new Result<T>(value, false, [..messages], HttpStatusCode.VariantAlsoNegotiates);
}
public static Result<T> VariantAlsoNegotiates(T? value, params string [] messages) =>
new (value, false, [..messages], HttpStatusCode.VariantAlsoNegotiates);
/// <summary>
/// Returns a result indicating the server is unable to store the representation needed to complete the request.
/// Corresponds to HTTP status code 507 Insufficient Storage.
/// </summary>
public static Result<T> InsufficientStorage(T? value, params string [] messages) {
return new Result<T>(value, false, [..messages], HttpStatusCode.InsufficientStorage);
}
public static Result<T> InsufficientStorage(T? value, params string [] messages) =>
new (value, false, [..messages], HttpStatusCode.InsufficientStorage);
/// <summary>
/// Returns a result indicating the server detected an infinite loop while processing a request with depth: infinity. Usually encountered in WebDAV scenarios.
/// Corresponds to HTTP status code 508 Loop Detected.
/// </summary>
public static Result<T> LoopDetected(T? value, params string [] messages) {
return new Result<T>(value, false, [..messages], HttpStatusCode.LoopDetected);
}
public static Result<T> LoopDetected(T? value, params string [] messages) =>
new (value, false, [..messages], HttpStatusCode.LoopDetected);
/// <summary>
/// Returns a result indicating further extensions to the request are required for the server to fulfill it.
/// Corresponds to HTTP status code 510 Not Extended.
/// </summary>
public static Result<T> NotExtended(T? value, params string [] messages) {
return new Result<T>(value, false, [..messages], HttpStatusCode.NotExtended);
}
public static Result<T> NotExtended(T? value, params string [] messages) =>
new (value, false, [..messages], HttpStatusCode.NotExtended);
/// <summary>
/// Returns a result indicating the client needs to authenticate to gain network access.
/// Corresponds to HTTP status code 511 Network Authentication Required.
/// </summary>
public static Result<T> NetworkAuthenticationRequired(T? value, params string [] messages) {
return new Result<T>(value, false, [..messages], HttpStatusCode.NetworkAuthenticationRequired);
}
public static Result<T> NetworkAuthenticationRequired(T? value, params string [] messages) =>
new (value, false, [..messages], HttpStatusCode.NetworkAuthenticationRequired);
#endregion
}

View File

@ -1,6 +1,5 @@
using System.Net;
namespace MaksIT.Results;
public partial class Result {
@ -11,57 +10,50 @@ public partial class Result {
/// Returns a result indicating the request was successful and the server returned the requested data.
/// Corresponds to HTTP status code 200 OK.
/// </summary>
public static Result Ok(params string[] messages) {
return new Result(true, [..messages], HttpStatusCode.OK);
}
public static Result Ok(params string[] messages) =>
new (true, [..messages], HttpStatusCode.OK);
/// <summary>
/// Returns a result indicating the request was successful and a new resource was created.
/// Corresponds to HTTP status code 201 Created.
/// </summary>
public static Result Created(params string[] messages) {
return new Result(true, [..messages], HttpStatusCode.Created);
}
public static Result Created(params string[] messages) =>
new (true, [..messages], HttpStatusCode.Created);
/// <summary>
/// Returns a result indicating the request has been accepted for processing, but the processing is not yet complete.
/// Corresponds to HTTP status code 202 Accepted.
/// </summary>
public static Result Accepted(params string[] messages) {
return new Result(true, [..messages], HttpStatusCode.Accepted);
}
public static Result Accepted(params string[] messages) =>
new (true, [..messages], HttpStatusCode.Accepted);
/// <summary>
/// Returns a result indicating the request was successful but the response contains metadata from a source other than the origin server.
/// Corresponds to HTTP status code 203 Non-Authoritative Information.
/// </summary>
public static Result NonAuthoritativeInformation(params string[] messages) {
return new Result(true, [..messages], HttpStatusCode.NonAuthoritativeInformation);
}
public static Result NonAuthoritativeInformation(params string[] messages) =>
new (true, [..messages], HttpStatusCode.NonAuthoritativeInformation);
/// <summary>
/// Returns a result indicating the request was successful but there is no content to send in the response.
/// Corresponds to HTTP status code 204 No Content.
/// </summary>
public static Result NoContent(params string[] messages) {
return new Result(true, [..messages], HttpStatusCode.NoContent);
}
public static Result NoContent(params string[] messages) =>
new (true, [..messages], HttpStatusCode.NoContent);
/// <summary>
/// Returns a result indicating the request was successful, but the user-agent should reset the document view that caused the request.
/// Corresponds to HTTP status code 205 Reset Content.
/// </summary>
public static Result ResetContent(params string[] messages) {
return new Result(true, [..messages], HttpStatusCode.ResetContent);
}
public static Result ResetContent(params string[] messages) =>
new (true, [..messages], HttpStatusCode.ResetContent);
/// <summary>
/// Returns a result indicating the request was successful and the server is delivering only part of the resource due to a range header sent by the client.
/// Corresponds to HTTP status code 206 Partial Content.
/// </summary>
public static Result PartialContent(params string[] messages) {
return new Result(true, [..messages], HttpStatusCode.PartialContent);
}
public static Result PartialContent(params string[] messages) =>
new (true, [..messages], HttpStatusCode.PartialContent);
#endregion
@ -71,25 +63,22 @@ public partial class Result {
/// Returns a result indicating the request was successful and the response contains multiple status codes, typically used for WebDAV.
/// Corresponds to HTTP status code 207 Multi-Status.
/// </summary>
public static Result MultiStatus(params string[] messages) {
return new Result(true, [..messages], HttpStatusCode.MultiStatus);
}
public static Result MultiStatus(params string[] messages) =>
new (true, [..messages], HttpStatusCode.MultiStatus);
/// <summary>
/// Returns a result indicating the request was successful and the information has already been reported in a previous response.
/// Corresponds to HTTP status code 208 Already Reported.
/// </summary>
public static Result AlreadyReported(params string[] messages) {
return new Result(true, [..messages], HttpStatusCode.AlreadyReported);
}
public static Result AlreadyReported(params string[] messages) =>
new (true, [..messages], HttpStatusCode.AlreadyReported);
/// <summary>
/// Returns a result indicating the request was successful and the server fulfilled the request for the resource using the delta encoding method.
/// Corresponds to HTTP status code 226 IM Used.
/// </summary>
public static Result IMUsed(params string[] messages) {
return new Result(true, [..messages], (HttpStatusCode)226); // 226 is the official status code for IM Used
}
public static Result IMUsed(params string[] messages) =>
new (true, [..messages], (HttpStatusCode)226); // 226 is the official status code for IM Used
#endregion
}
@ -101,57 +90,50 @@ public partial class Result<T> : Result {
/// Returns a result indicating the request was successful and the server returned the requested data.
/// Corresponds to HTTP status code 200 OK.
/// </summary>
public static Result<T> Ok(T? value, params string[] messages) {
return new Result<T>(value, true, [..messages], HttpStatusCode.OK);
}
public static Result<T> Ok(T? value, params string[] messages) =>
new (value, true, [..messages], HttpStatusCode.OK);
/// <summary>
/// Returns a result indicating the request was successful and a new resource was created.
/// Corresponds to HTTP status code 201 Created.
/// </summary>
public static Result<T> Created(T? value, params string[] messages) {
return new Result<T>(value, true, [..messages], HttpStatusCode.Created);
}
public static Result<T> Created(T? value, params string[] messages) =>
new (value, true, [..messages], HttpStatusCode.Created);
/// <summary>
/// Returns a result indicating the request has been accepted for processing, but the processing is not yet complete.
/// Corresponds to HTTP status code 202 Accepted.
/// </summary>
public static Result<T> Accepted(T? value, params string[] messages) {
return new Result<T>(value, true, [..messages], HttpStatusCode.Accepted);
}
public static Result<T> Accepted(T? value, params string[] messages) =>
new (value, true, [..messages], HttpStatusCode.Accepted);
/// <summary>
/// Returns a result indicating the request was successful but the response contains metadata from a source other than the origin server.
/// Corresponds to HTTP status code 203 Non-Authoritative Information.
/// </summary>
public static Result<T> NonAuthoritativeInformation(T? value, params string[] messages) {
return new Result<T>(value, true, [..messages], HttpStatusCode.NonAuthoritativeInformation);
}
public static Result<T> NonAuthoritativeInformation(T? value, params string[] messages) =>
new (value, true, [..messages], HttpStatusCode.NonAuthoritativeInformation);
/// <summary>
/// Returns a result indicating the request was successful but there is no content to send in the response.
/// Corresponds to HTTP status code 204 No Content.
/// </summary>
public static Result<T> NoContent(T? value, params string[] messages) {
return new Result<T>(value, true, [..messages], HttpStatusCode.NoContent);
}
public static Result<T> NoContent(T? value, params string[] messages) =>
new (value, true, [..messages], HttpStatusCode.NoContent);
/// <summary>
/// Returns a result indicating the request was successful, but the user-agent should reset the document view that caused the request.
/// Corresponds to HTTP status code 205 Reset Content.
/// </summary>
public static Result<T> ResetContent(T? value, params string[] messages) {
return new Result<T>(value, true, [..messages], HttpStatusCode.ResetContent);
}
public static Result<T> ResetContent(T? value, params string[] messages) =>
new (value, true, [..messages], HttpStatusCode.ResetContent);
/// <summary>
/// Returns a result indicating the request was successful and the server is delivering only part of the resource due to a range header sent by the client.
/// Corresponds to HTTP status code 206 Partial Content.
/// </summary>
public static Result<T> PartialContent(T? value, params string[] messages) {
return new Result<T>(value, true, [..messages], HttpStatusCode.PartialContent);
}
public static Result<T> PartialContent(T? value, params string[] messages) =>
new (value, true, [..messages], HttpStatusCode.PartialContent);
#endregion
@ -161,25 +143,22 @@ public partial class Result<T> : Result {
/// Returns a result indicating the request was successful and the response contains multiple status codes, typically used for WebDAV.
/// Corresponds to HTTP status code 207 Multi-Status.
/// </summary>
public static Result<T> MultiStatus(T? value, params string[] messages) {
return new Result<T>(value, true, [..messages], HttpStatusCode.MultiStatus);
}
public static Result<T> MultiStatus(T? value, params string[] messages) =>
new (value, true, [..messages], HttpStatusCode.MultiStatus);
/// <summary>
/// Returns a result indicating the request was successful and the information has already been reported in a previous response.
/// Corresponds to HTTP status code 208 Already Reported.
/// </summary>
public static Result<T> AlreadyReported(T? value, params string[] messages) {
return new Result<T>(value, true, [..messages], HttpStatusCode.AlreadyReported);
}
public static Result<T> AlreadyReported(T? value, params string[] messages) =>
new (value, true, [..messages], HttpStatusCode.AlreadyReported);
/// <summary>
/// Returns a result indicating the request was successful and the server fulfilled the request for the resource using the delta encoding method.
/// Corresponds to HTTP status code 226 IM Used.
/// </summary>
public static Result<T> IMUsed(T? value, params string[] messages) {
return new Result<T>(value, true, [..messages], (HttpStatusCode)226); // 226 is the official status code for IM Used
}
public static Result<T> IMUsed(T? value, params string[] messages) =>
new (value, true, [..messages], (HttpStatusCode)226); // 226 is the official status code for IM Used
#endregion
}

View File

@ -2,7 +2,6 @@
using Microsoft.AspNetCore.Mvc;
using MaksIT.Results.Mvc;
namespace MaksIT.Results;
public partial class Result {
@ -12,7 +11,7 @@ public partial class Result {
protected Result(bool isSuccess, List<string> messages, HttpStatusCode statusCode) {
IsSuccess = isSuccess;
Messages = messages ?? new List<string>();
Messages = messages ?? [];
StatusCode = statusCode;
}
@ -20,9 +19,8 @@ public partial class Result {
/// Converts the current Result{T} to a non-generic Result.
/// </summary>
/// <returns>A non-generic Result object.</returns>
public Result ToResult() {
return new Result(IsSuccess, Messages, StatusCode);
}
public Result ToResult() =>
new (IsSuccess, Messages, StatusCode);
/// <summary>
/// Converts this Result into a Result of another type while retaining the same success status, messages, and status code.
@ -30,9 +28,8 @@ public partial class Result {
/// <typeparam name="U">The target type for the Result.</typeparam>
/// <param name="value">The new value of type U for the converted Result.</param>
/// <returns>A Result of type U.</returns>
public Result<U?> ToResultOfType<U>(U? value) {
return new Result<U?>(value, IsSuccess, Messages, StatusCode);
}
public Result<U?> ToResultOfType<U>(U? value) =>
new(value, IsSuccess, Messages, StatusCode);
/// <summary>
/// Converts the current Result to an IActionResult.
@ -49,18 +46,15 @@ public partial class Result {
Detail = string.Join("; ", Messages),
Instance = null // You can customize the instance URI if needed
};
return new ObjectResult(problemDetails) { StatusCode = (int)StatusCode };
}
}
}
public partial class Result<T> : Result {
public T? Value { get; }
public Result(T? value, bool isSuccess, List<string> messages, HttpStatusCode statusCode)
: base(isSuccess, messages, statusCode) {
Value = value;
}
public partial class Result<T>(T? value, bool isSuccess, List<string> messages, HttpStatusCode statusCode)
: Result(isSuccess, messages, statusCode) {
public T? Value { get; } = value;
/// <summary>
/// Converts this Result<T> to a Result<U> while retaining success status, messages, and status code.
@ -68,9 +62,8 @@ public partial class Result<T> : Result {
/// <typeparam name="U">The target type for the Result.</typeparam>
/// <param name="newValueFunc">A function to transform the current value to a new value of type U.</param>
/// <returns>A Result<U> containing the transformed value.</returns>
public Result<U?> ToResultOfType<U>(Func<T?, U?> newValueFunc) {
return new Result<U?>(newValueFunc(Value), IsSuccess, Messages, StatusCode);
}
public Result<U?> ToResultOfType<U>(Func<T?, U?> newValueFunc) =>
new (newValueFunc(Value), IsSuccess, Messages, StatusCode);
/// <summary>
/// Converts the current Result<T> to an IActionResult.
@ -79,7 +72,7 @@ public partial class Result<T> : Result {
public override IActionResult ToActionResult() {
if (IsSuccess) {
if (IsRedirectionStatusCode(StatusCode) && Value is string location && !string.IsNullOrWhiteSpace(location))
return new RedirectResult(location);
return new RedirectResult(location, (int)StatusCode);
if (Value is not null)
return new ObjectResult(Value) { StatusCode = (int)StatusCode };
@ -90,12 +83,4 @@ public partial class Result<T> : Result {
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;
}

View File

@ -1,69 +0,0 @@
#requires -Version 7.0
#requires -PSEdition Core
<#
.SYNOPSIS
Discovers existing .NET NuGet package artifacts.
.DESCRIPTION
Scans artifactsDir for .nupkg/.snupkg matching the release version and populates shared
context (packageFile, symbolsPackageFile, releaseArchiveInputs) for downstream plugins.
#>
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
$srcDir = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
$pluginSupportModulePath = Join-Path $srcDir 'modules/Engine/PluginSupport.psm1'
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
Import-Module $pluginSupportModulePath -Force -Global -ErrorAction Stop
}
}
function Invoke-Plugin {
param(
[Parameter(Mandatory = $true)]
$Settings
)
Import-PluginDependency -ModuleName 'Logging' -RequiredCommand 'Write-Log'
Import-PluginDependency -ModuleName 'DotNetArtifactSupport' -RequiredCommand 'Resolve-DotNetPackageArtifacts'
Import-PluginDependency -ModuleName 'EngineContext' -RequiredCommand 'Set-EngineFact'
$pluginSettings = $Settings
$sharedSettings = $Settings.context
$scriptDir = $sharedSettings.scriptDir
$version = $sharedSettings.version
if ($Settings.PSObject.Properties['artifactsDir'] -and -not [string]::IsNullOrWhiteSpace([string]$Settings.artifactsDir)) {
$artifactsDirectory = [System.IO.Path]::GetFullPath((Join-Path $scriptDir ([string]$Settings.artifactsDir)))
Set-EngineState -Context $sharedSettings -Name 'artifactsDirectory' -Value $artifactsDirectory
Set-EngineState -Context $sharedSettings -Name 'releaseDir' -Value $artifactsDirectory
}
else {
$artifactsDirectory = $sharedSettings.artifactsDirectory
}
if ([string]::IsNullOrWhiteSpace($artifactsDirectory)) {
throw 'DiscoverDotNetPackageArtifacts requires artifactsDir in plugin settings or artifactsDirectory on shared context.'
}
Write-Log -Level 'STEP' -Message "Discovering NuGet package artifacts in $artifactsDirectory ..."
$resolved = Resolve-DotNetPackageArtifacts -ArtifactsDirectory $artifactsDirectory -Version $version
Write-Log -Level 'OK' -Message " Package ready: $($resolved.PackageFile.FullName)"
if ($resolved.SymbolsPackageFile) {
Write-Log -Level 'OK' -Message " Symbols package ready: $($resolved.SymbolsPackageFile.FullName)"
}
else {
Write-Log -Level 'WARN' -Message " Symbols package (.snupkg) not found for version $version."
}
Set-EngineFact -Context $sharedSettings -Namespace 'dotnet' -Name 'packageFile' -Value $resolved.PackageFile -Overwrite Replace -LegacyProperty 'packageFile'
Set-EngineFact -Context $sharedSettings -Namespace 'dotnet' -Name 'symbolsPackageFile' -Value $resolved.SymbolsPackageFile -Overwrite Replace -LegacyProperty 'symbolsPackageFile'
Set-EngineFact -Context $sharedSettings -Namespace 'release' -Name 'archiveInputs' -Value $resolved.ReleaseArchiveInputs -Overwrite Replace -LegacyProperty 'releaseArchiveInputs'
}
function Get-PluginMetadata {
[pscustomobject]@{ mutatesRemote = $false }
}
Export-ModuleMember -Function Invoke-Plugin, Get-PluginMetadata

View File

@ -1,86 +0,0 @@
#requires -Version 7.0
#requires -PSEdition Core
<#
.SYNOPSIS
.NET NuGet publish plugin.
.DESCRIPTION
This plugin publishes the package artifact from shared runtime
context to the configured NuGet feed using the configured API key.
#>
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
$srcDir = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
$pluginSupportModulePath = Join-Path $srcDir "modules/Engine/PluginSupport.psm1"
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
Import-Module $pluginSupportModulePath -Force -Global -ErrorAction Stop
}
}
function Invoke-Plugin {
param(
[Parameter(Mandatory = $true)]
$Settings
)
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
Import-PluginDependency -ModuleName "ScriptConfig" -RequiredCommand "Assert-Command"
$pluginSettings = $Settings
$sharedSettings = $Settings.context
$packageFile = $sharedSettings.packageFile
$dryRun = Test-PluginSkipsRemoteMutation -Plugin $pluginSettings -SharedSettings $sharedSettings
$nugetSecret = Resolve-PluginSecretName -PluginSettings $pluginSettings -PropertyName 'nugetSecret' -PluginDisplayName 'DotNetNuGet' -Required
Assert-Command dotnet
if (-not $packageFile) {
throw "DotNetNuGet plugin requires a NuGet package artifact. Ensure DotNetPack produced a .nupkg before running DotNetNuGet."
}
if ($dryRun) {
$nugetSource = if ([string]::IsNullOrWhiteSpace($pluginSettings.source)) {
"https://api.nuget.org/v3/index.json"
}
else {
$pluginSettings.source
}
Write-Log -Level "INFO" -Message "Dry run: would push $($packageFile.FullName) to $nugetSource"
return
}
$nugetKey = Get-RepoUtilsSecretSlot -Name $nugetSecret -Settings $sharedSettings
if ([string]::IsNullOrWhiteSpace($nugetKey)) {
throw "NuGet API key is not set. Set RepoUtilsSecrets slot '$nugetSecret' (nugetSecret)."
}
$nugetSource = if ([string]::IsNullOrWhiteSpace($pluginSettings.source)) {
"https://api.nuget.org/v3/index.json"
}
else {
$pluginSettings.source
}
Write-Log -Level "STEP" -Message "Pushing package to NuGet feed..."
dotnet nuget push $packageFile.FullName -k $nugetKey -s $nugetSource --skip-duplicate
if ($LASTEXITCODE -ne 0) {
throw "Failed to push the package to NuGet feed."
}
Write-Log -Level "OK" -Message " NuGet push completed."
Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Add-EnginePublishCompletion"
Add-EnginePublishCompletion -Context $sharedSettings -Publisher 'DotNetNuGet'
}
function Get-PluginMetadata {
[pscustomobject]@{ mutatesRemote = $true }
}
Export-ModuleMember -Function Invoke-Plugin, Get-PluginMetadata

View File

@ -1,199 +0,0 @@
#requires -Version 7.0
#requires -PSEdition Core
<#
.SYNOPSIS
.NET publish plugin for producing application release artifacts.
.DESCRIPTION
This plugin publishes configured .NET projects into the artifacts directory
and appends those publish folders to shared archive inputs so later plugins
can zip them next to any earlier pack outputs. Existing NuGet package facts
(packageFile) are left unchanged.
Optional runtimeIdentifiers (or runtimeIdentifier) produce per-RID folders
under artifactsDir/<ProjectName>/<rid> for desktop installers (Windows MSI /
Linux Flatpak). When RIDs are set, selfContained defaults to true.
#>
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
$srcDir = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
$pluginSupportModulePath = Join-Path $srcDir "modules/Engine/PluginSupport.psm1"
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
Import-Module $pluginSupportModulePath -Force -Global -ErrorAction Stop
}
}
function Get-DotNetPublishRuntimeIdentifiers {
param(
[Parameter(Mandatory = $true)]
$PluginSettings
)
$rids = [System.Collections.Generic.List[string]]::new()
if ($PluginSettings.PSObject.Properties['runtimeIdentifiers'] -and $null -ne $PluginSettings.runtimeIdentifiers) {
foreach ($item in @($PluginSettings.runtimeIdentifiers)) {
$value = [string]$item
if (-not [string]::IsNullOrWhiteSpace($value)) {
$rids.Add($value.Trim())
}
}
}
elseif ($PluginSettings.PSObject.Properties['runtimeIdentifier'] -and -not [string]::IsNullOrWhiteSpace([string]$PluginSettings.runtimeIdentifier)) {
$rids.Add(([string]$PluginSettings.runtimeIdentifier).Trim())
}
return @($rids)
}
function Invoke-Plugin {
param(
[Parameter(Mandatory = $true)]
$Settings
)
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
Import-PluginDependency -ModuleName "ScriptConfig" -RequiredCommand "Assert-Command"
Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Set-EngineFact"
Import-PluginDependency -ModuleName "ExternalCommandSupport" -RequiredCommand "Invoke-ExternalCommand"
$pluginSettings = $Settings
$sharedSettings = $Settings.context
$scriptDir = $sharedSettings.scriptDir
$projectFiles = @()
Assert-Command dotnet
if ($pluginSettings.PSObject.Properties['projectFiles'] -and $null -ne $pluginSettings.projectFiles) {
$projectFiles = @(Resolve-RelativePaths -Value $pluginSettings.projectFiles -BasePath $scriptDir)
}
else {
$fromFact = Get-EngineFact -Context $sharedSettings -Namespace 'dotnet' -Name 'projectFiles' -LegacyProperty @('projectFiles')
if ($null -ne $fromFact) {
$projectFiles = @($fromFact)
}
elseif ($sharedSettings.PSObject.Properties['projectFiles'] -and $null -ne $sharedSettings.projectFiles) {
$projectFiles = @($sharedSettings.projectFiles)
}
}
if ($projectFiles.Count -eq 0) {
throw "DotNetPublish plugin requires projectFiles in plugin settings or projectFiles on shared context."
}
if ($pluginSettings.PSObject.Properties['artifactsDir'] -and -not [string]::IsNullOrWhiteSpace([string]$pluginSettings.artifactsDir)) {
$artifactsDirectory = [System.IO.Path]::GetFullPath((Join-Path $scriptDir ([string]$pluginSettings.artifactsDir)))
Set-EngineState -Context $sharedSettings -Name 'artifactsDirectory' -Value $artifactsDirectory
Set-EngineState -Context $sharedSettings -Name 'releaseDir' -Value $artifactsDirectory
}
else {
$artifactsDirectory = $sharedSettings.artifactsDirectory
}
if ([string]::IsNullOrWhiteSpace([string]$artifactsDirectory)) {
throw "DotNetPublish plugin requires artifactsDir in plugin settings or artifactsDirectory on shared context."
}
if (!(Test-Path $artifactsDirectory)) {
New-Item -ItemType Directory -Path $artifactsDirectory | Out-Null
}
$existing = Get-EngineFact -Context $sharedSettings -Namespace 'release' -Name 'archiveInputs' -LegacyProperty @('releaseArchiveInputs')
$archiveInputs = [System.Collections.Generic.List[object]]::new()
if ($null -ne $existing) {
foreach ($item in @($existing)) {
if ($null -ne $item) {
$archiveInputs.Add($item)
}
}
}
$runtimeIdentifiers = @(Get-DotNetPublishRuntimeIdentifiers -PluginSettings $pluginSettings)
$selfContained = $null
if ($pluginSettings.PSObject.Properties['selfContained'] -and $null -ne $pluginSettings.selfContained) {
$selfContained = [bool]$pluginSettings.selfContained
}
elseif ($runtimeIdentifiers.Count -gt 0) {
$selfContained = $true
}
$publishSingleFile = $false
if ($pluginSettings.PSObject.Properties['publishSingleFile'] -and $null -ne $pluginSettings.publishSingleFile) {
$publishSingleFile = [bool]$pluginSettings.publishSingleFile
}
$additionalArguments = @()
if ($pluginSettings.PSObject.Properties['additionalPublishArguments'] -and $null -ne $pluginSettings.additionalPublishArguments) {
$additionalArguments = @($pluginSettings.additionalPublishArguments | ForEach-Object { [string]$_ } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
}
$publishOutputs = [System.Collections.Generic.List[object]]::new()
$ridLoop = @($null)
if ($runtimeIdentifiers.Count -gt 0) {
$ridLoop = @($runtimeIdentifiers)
}
foreach ($publishProjectPath in $projectFiles) {
$projectName = [System.IO.Path]::GetFileNameWithoutExtension($publishProjectPath)
$projectRoot = Join-Path $artifactsDirectory $projectName
foreach ($runtimeIdentifier in $ridLoop) {
$publishDir = if ([string]::IsNullOrWhiteSpace([string]$runtimeIdentifier)) {
$projectRoot
}
else {
Join-Path $projectRoot ([string]$runtimeIdentifier)
}
if (Test-Path $publishDir) {
Remove-Item -Path $publishDir -Recurse -Force
}
$ridLabel = if ([string]::IsNullOrWhiteSpace([string]$runtimeIdentifier)) { 'current runtime' } else { [string]$runtimeIdentifier }
Write-Log -Level "STEP" -Message "Publishing release artifact ($projectName / $ridLabel)..."
$dotnetPublishArguments = @(
'publish', $publishProjectPath, '-c', 'Release', '-o', $publishDir, '--nologo'
)
if (-not [string]::IsNullOrWhiteSpace([string]$runtimeIdentifier)) {
$dotnetPublishArguments += @('-r', [string]$runtimeIdentifier)
}
if ($null -ne $selfContained) {
if ($selfContained) {
$dotnetPublishArguments += '--self-contained'
}
else {
$dotnetPublishArguments += '--no-self-contained'
}
}
if ($publishSingleFile) {
$dotnetPublishArguments += '-p:PublishSingleFile=true'
}
if ($additionalArguments.Count -gt 0) {
$dotnetPublishArguments += $additionalArguments
}
Invoke-ExternalCommand -Name dotnet -ArgumentList $dotnetPublishArguments | Out-Null
$publishedItems = @(Get-ChildItem -Path $publishDir -Force -ErrorAction SilentlyContinue)
if ($publishedItems.Count -eq 0) {
throw "dotnet publish completed, but no files were produced in: $publishDir"
}
Write-Log -Level "OK" -Message " Published artifact ready: $publishDir"
$archiveInputs.Add($publishDir)
$publishOutputs.Add([pscustomobject]@{
projectName = $projectName
runtimeIdentifier = [string]$runtimeIdentifier
directory = $publishDir
})
}
}
Set-EngineFact -Context $sharedSettings -Namespace 'release' -Name 'archiveInputs' -Value @($archiveInputs) -Overwrite Replace -LegacyProperty 'releaseArchiveInputs'
Set-EngineFact -Context $sharedSettings -Namespace 'dotnet' -Name 'publishOutputs' -Value @($publishOutputs) -Overwrite Replace
}
Export-ModuleMember -Function Invoke-Plugin, Get-DotNetPublishRuntimeIdentifiers

View File

@ -1,78 +0,0 @@
#requires -Version 7.0
#requires -PSEdition Core
<#
.SYNOPSIS
Collects coverage metrics from an existing test results directory.
.DESCRIPTION
Stack-agnostic parser for recovered container or host test output. Auto-detects .NET
Cobertura (coverage.cobertura.xml) or Jest (coverage-summary.json) and publishes the
same shared-context keys as DotNetTest / NpmJestTest for QualityGate.
Use after DockerContainerBuilder / PodmanContainerBuilder when tests run inside a container image.
#>
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
$srcDir = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
$pluginSupportModulePath = Join-Path $srcDir 'modules/Engine/PluginSupport.psm1'
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
Import-Module $pluginSupportModulePath -Force -Global -ErrorAction Stop
}
}
function Invoke-Plugin {
param(
[Parameter(Mandatory = $true)]
$Settings
)
Import-PluginDependency -ModuleName 'Logging' -RequiredCommand 'Write-Log'
Import-PluginDependency -ModuleName 'TestRunner' -RequiredCommand 'Get-CoverageFromResultsDirectory'
Import-PluginDependency -ModuleName 'TestRunner' -RequiredCommand 'Publish-CoverageMetricsToSharedContext'
$pluginSettings = $Settings
$sharedSettings = $Settings.context
$scriptDir = $sharedSettings.scriptDir
$resultsDirSetting = $pluginSettings.resultsDir
if ([string]::IsNullOrWhiteSpace($resultsDirSetting)) {
if ($sharedSettings.PSObject.Properties.Name -contains 'testResultsDirectory' -and $sharedSettings.testResultsDirectory) {
$resultsDirSetting = $sharedSettings.testResultsDirectory
}
else {
throw "CollectCoverage requires 'resultsDir' in plugin settings or testResultsDirectory on shared context (from ContainerBuilder)."
}
}
$resultsDirectory = if ([System.IO.Path]::IsPathRooted([string]$resultsDirSetting)) {
[string]$resultsDirSetting
}
else {
[System.IO.Path]::GetFullPath((Join-Path $scriptDir ([string]$resultsDirSetting)))
}
$format = 'auto'
if ($pluginSettings.PSObject.Properties.Name -contains 'format' -and -not [string]::IsNullOrWhiteSpace([string]$pluginSettings.format)) {
$format = [string]$pluginSettings.format
}
Write-Log -Level 'STEP' -Message "Collecting coverage from $resultsDirectory ..."
$testResult = Get-CoverageFromResultsDirectory -ResultsDirectory $resultsDirectory -Format $format -Silent:$true
if (-not $testResult.Success) {
throw $testResult.Error
}
Publish-CoverageMetricsToSharedContext -SharedSettings $sharedSettings -TestResult $testResult
$formatLabel = if ($testResult.CoverageFormat) { $testResult.CoverageFormat } else { 'unknown' }
Write-Log -Level 'OK' -Message " Coverage collected ($formatLabel)."
Write-Log -Level 'INFO' -Message " Line Coverage: $($testResult.LineRate)%"
Write-Log -Level 'INFO' -Message " Branch Coverage: $($testResult.BranchRate)%"
Write-Log -Level 'INFO' -Message " Method Coverage: $($testResult.MethodRate)%"
}
function Get-PluginMetadata {
[pscustomobject]@{ mutatesRemote = $false }
}
Export-ModuleMember -Function Invoke-Plugin, Get-PluginMetadata

View File

@ -1,52 +0,0 @@
#requires -Version 7.0
#requires -PSEdition Core
<#
.SYNOPSIS
Probes docker/podman availability and sets mandatory shared engine state for the pipeline.
.DESCRIPTION
Must be the first enabled plugin. Sets containerEngineProbeCompleted, activeContainerEngine,
dockerEngineAvailable, podmanEngineAvailable, and compose CLI fields on shared context.
Fails the pipeline when no usable engine is found.
#>
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
$srcDir = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
$pluginSupportModulePath = Join-Path $srcDir 'modules/Engine/PluginSupport.psm1'
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
Import-Module $pluginSupportModulePath -Force -Global -ErrorAction Stop
}
}
function Invoke-Plugin {
param(
[Parameter(Mandatory = $true)]
$Settings
)
Import-PluginDependency -ModuleName 'Logging' -RequiredCommand 'Write-Log'
Import-PluginDependency -ModuleName 'ContainerEngineSupport' -RequiredCommand 'Resolve-ActiveContainerEngineState'
$shared = $Settings.context
Write-Log -Level 'STEP' -Message 'Probing container engine availability...'
$state = Resolve-ActiveContainerEngineState
Set-ContainerEngineSharedState -SharedSettings $shared -State $state
$display = Get-ContainerEngineDisplayName -Engine (Get-SharedContainerEngineInvocation -SharedSettings $shared)
Write-Log -Level 'OK' -Message " Active engine: $display"
Write-Log -Level 'INFO' -Message " docker CLI available: $($state.DockerEngineAvailable); podman CLI available: $($state.PodmanEngineAvailable)"
if ([string]::IsNullOrWhiteSpace([string]$state.ComposeExecutable)) {
Write-Log -Level 'INFO' -Message ' Compose CLI: not available (Compose plugins will fail if enabled).'
}
else {
Write-Log -Level 'INFO' -Message " Compose CLI: $($state.ComposeExecutable) $($state.ComposeBaseArgs -join ' ')"
}
}
function Get-PluginMetadata {
[pscustomobject]@{ mutatesRemote = $false }
}
Export-ModuleMember -Function Invoke-Plugin, Get-PluginMetadata

View File

@ -1,124 +0,0 @@
#requires -Version 7.0
#requires -PSEdition Core
<#
.SYNOPSIS
Discovers package artifacts produced by pack plugins or container build pipelines.
.DESCRIPTION
Stack-agnostic scanner for artifactsDir. Detects .NET .nupkg/.snupkg or npm .tgz files
and populates shared context (packageFile, releaseAssetPaths, releaseArchiveInputs).
#>
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
$srcDir = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
$pluginSupportModulePath = Join-Path $srcDir 'modules/Engine/PluginSupport.psm1'
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
Import-Module $pluginSupportModulePath -Force -Global -ErrorAction Stop
}
}
function Invoke-Plugin {
param(
[Parameter(Mandatory = $true)]
$Settings
)
Import-PluginDependency -ModuleName 'Logging' -RequiredCommand 'Write-Log'
Import-PluginDependency -ModuleName 'DotNetArtifactSupport' -RequiredCommand 'Resolve-DotNetPackageArtifacts'
$pluginSettings = $Settings
$sharedSettings = $Settings.context
$scriptDir = $sharedSettings.scriptDir
$version = $sharedSettings.version
if ($Settings.PSObject.Properties['artifactsDir'] -and -not [string]::IsNullOrWhiteSpace([string]$Settings.artifactsDir)) {
$artifactsDirectory = [System.IO.Path]::GetFullPath((Join-Path $scriptDir ([string]$Settings.artifactsDir)))
if (Get-Command Set-EngineState -ErrorAction SilentlyContinue) {
Set-EngineState -Context $sharedSettings -Name 'artifactsDirectory' -Value $artifactsDirectory
Set-EngineState -Context $sharedSettings -Name 'releaseDir' -Value $artifactsDirectory
}
else {
$sharedSettings | Add-Member -NotePropertyName artifactsDirectory -NotePropertyValue $artifactsDirectory -Force
$sharedSettings | Add-Member -NotePropertyName releaseDir -NotePropertyValue $artifactsDirectory -Force
}
}
else {
$artifactsDirectory = $sharedSettings.artifactsDirectory
}
if ([string]::IsNullOrWhiteSpace($artifactsDirectory)) {
throw 'DiscoverPackageArtifacts requires artifactsDir in plugin settings or artifactsDirectory on shared context.'
}
$nupkgCandidates = @(Get-ChildItem -Path $artifactsDirectory -Filter '*.nupkg' -ErrorAction SilentlyContinue | Where-Object {
$_.Name -notlike '*.symbols.nupkg' -and $_.Name -notlike '*.snupkg'
})
$tgzCandidates = @(Get-ChildItem -Path $artifactsDirectory -Filter '*.tgz' -ErrorAction SilentlyContinue)
if ($nupkgCandidates.Count -gt 0 -and $tgzCandidates.Count -gt 0) {
throw "DiscoverPackageArtifacts found both NuGet and npm artifacts in $artifactsDirectory. Use one package stack per repository."
}
if ($nupkgCandidates.Count -gt 0) {
if ([string]::IsNullOrWhiteSpace([string]$version)) {
throw 'DiscoverPackageArtifacts requires release version on shared context when discovering .nupkg artifacts.'
}
Write-Log -Level 'STEP' -Message "Discovering NuGet package artifacts in $artifactsDirectory ..."
$resolved = Resolve-DotNetPackageArtifacts -ArtifactsDirectory $artifactsDirectory -Version $version
Write-Log -Level 'OK' -Message " Package ready: $($resolved.PackageFile.FullName)"
if ($resolved.SymbolsPackageFile) {
Write-Log -Level 'OK' -Message " Symbols package ready: $($resolved.SymbolsPackageFile.FullName)"
}
else {
Write-Log -Level 'WARN' -Message " Symbols package (.snupkg) not found for version $version."
}
$sharedSettings | Add-Member -NotePropertyName packageFile -NotePropertyValue $resolved.PackageFile -Force
$sharedSettings | Add-Member -NotePropertyName symbolsPackageFile -NotePropertyValue $resolved.SymbolsPackageFile -Force
$sharedSettings | Add-Member -NotePropertyName releaseArchiveInputs -NotePropertyValue $resolved.ReleaseArchiveInputs -Force
$sharedSettings | Add-Member -NotePropertyName releaseAssetPaths -NotePropertyValue $resolved.ReleaseArchiveInputs -Force
if (Get-Command Set-EngineFact -ErrorAction SilentlyContinue) {
Set-EngineFact -Context $sharedSettings -Namespace 'dotnet' -Name 'packageFile' -Value $resolved.PackageFile -Overwrite Replace -LegacyProperty 'packageFile'
Set-EngineFact -Context $sharedSettings -Namespace 'dotnet' -Name 'symbolsPackageFile' -Value $resolved.SymbolsPackageFile -Overwrite Replace -LegacyProperty 'symbolsPackageFile'
Set-EngineFact -Context $sharedSettings -Namespace 'release' -Name 'archiveInputs' -Value $resolved.ReleaseArchiveInputs -Overwrite Replace -LegacyProperty 'releaseArchiveInputs'
Set-EngineFact -Context $sharedSettings -Namespace 'release' -Name 'assetPaths' -Value $resolved.ReleaseArchiveInputs -Overwrite Replace -LegacyProperty 'releaseAssetPaths'
}
return
}
if ($tgzCandidates.Count -gt 0) {
Import-PluginDependency -ModuleName 'NpmArtifactSupport' -RequiredCommand 'Resolve-NpmPackageArtifacts'
Write-Log -Level 'STEP' -Message "Discovering npm package artifacts in $artifactsDirectory ..."
$resolved = Resolve-NpmPackageArtifacts -ArtifactsDirectory $artifactsDirectory -Version $version
foreach ($tarball in $resolved.PackageFiles) {
Write-Log -Level 'OK' -Message " Package ready: $($tarball.FullName)"
}
$sharedSettings | Add-Member -NotePropertyName packageFile -NotePropertyValue $resolved.PackageFile -Force
$sharedSettings | Add-Member -NotePropertyName releaseArchiveInputs -NotePropertyValue $resolved.ReleaseArchiveInputs -Force
$sharedSettings | Add-Member -NotePropertyName releaseAssetPaths -NotePropertyValue $resolved.ReleaseAssetPaths -Force
$sharedSettings | Add-Member -NotePropertyName releaseDir -NotePropertyValue $artifactsDirectory -Force
if (Get-Command Set-EngineFact -ErrorAction SilentlyContinue) {
Set-EngineFact -Context $sharedSettings -Namespace 'npm' -Name 'packageFile' -Value $resolved.PackageFile -Overwrite Replace -LegacyProperty 'packageFile'
Set-EngineFact -Context $sharedSettings -Namespace 'release' -Name 'archiveInputs' -Value $resolved.ReleaseArchiveInputs -Overwrite Replace -LegacyProperty 'releaseArchiveInputs'
Set-EngineFact -Context $sharedSettings -Namespace 'release' -Name 'assetPaths' -Value $resolved.ReleaseAssetPaths -Overwrite Replace -LegacyProperty 'releaseAssetPaths'
if (Get-Command Set-EngineState -ErrorAction SilentlyContinue) {
Set-EngineState -Context $sharedSettings -Name 'releaseDir' -Value $artifactsDirectory
}
}
return
}
throw "DiscoverPackageArtifacts found no .nupkg or .tgz files in: $artifactsDirectory"
}
function Get-PluginMetadata {
[pscustomobject]@{ mutatesRemote = $false }
}
Export-ModuleMember -Function Invoke-Plugin, Get-PluginMetadata

View File

@ -1,84 +0,0 @@
#requires -Version 7.0
#requires -PSEdition Core
<#
.SYNOPSIS
Loads release version from a repo-root VERSION file into shared context.
.DESCRIPTION
Reads a single-line semver from the configured versionFilePath (default
repo-root VERSION), including optional prerelease (0.1.0-alpha.1). Useful for
repositories without .csproj or package.json version metadata. Declares
providesVersion = $true so the engine can discover it as the single release
version source.
#>
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
$srcDir = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
$pluginSupportModulePath = Join-Path $srcDir "modules/Engine/PluginSupport.psm1"
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
Import-Module $pluginSupportModulePath -Force -Global -ErrorAction Stop
}
}
function Get-VersionFileSemverInternal {
param(
[Parameter(Mandatory = $true)]
[string]$VersionFilePath
)
if (-not (Test-Path $VersionFilePath -PathType Leaf)) {
throw "FileReleaseVersion: VERSION file not found at '$VersionFilePath'."
}
$version = (Get-Content -Path $VersionFilePath -Raw -Encoding UTF8).Trim()
if ([string]::IsNullOrWhiteSpace($version)) {
throw "FileReleaseVersion: VERSION file is empty at '$VersionFilePath'."
}
$version = $version -replace '^[vV]', ''
Import-PluginDependency -ModuleName "ChangelogSupport" -RequiredCommand "Test-ReleaseSemver"
if (-not (Test-ReleaseSemver -Version $version)) {
throw "FileReleaseVersion: version '$version' in '$VersionFilePath' is not a valid semver (X.Y.Z or X.Y.Z-prerelease)."
}
return $version
}
function Get-PluginMetadata {
return [pscustomobject]@{ providesVersion = $true }
}
function Invoke-Plugin {
param(
[Parameter(Mandatory = $true)]
$Settings
)
Import-PluginDependency -ModuleName "Logging" -RequiredCommand "Write-Log"
Import-PluginDependency -ModuleName "EngineContext" -RequiredCommand "Set-EngineState"
Import-PluginDependency -ModuleName "ChangelogSupport" -RequiredCommand "Test-ReleaseSemver"
$shared = $Settings.context
$versionFileSetting = if ($Settings.versionFilePath) {
$Settings.versionFilePath
}
else {
'..\..\..\VERSION'
}
$versionFilePaths = @(Resolve-RelativePaths -Value $versionFileSetting -BasePath $shared.scriptDir)
if ($versionFilePaths.Count -eq 0) {
throw "FileReleaseVersion plugin requires 'versionFilePath' (repo-root VERSION file) in scriptSettings.json."
}
$versionFilePath = $versionFilePaths[0]
Write-Log -Level "INFO" -Message "Reading version from VERSION file (versionFilePath)..."
$version = Get-VersionFileSemverInternal -VersionFilePath $versionFilePath
Set-EngineState -Context $shared -Name 'version' -Value $version
Set-EngineFact -Context $shared -Namespace 'release' -Name 'versionFilePath' -Value $versionFilePath -Overwrite Replace -LegacyProperty 'versionFilePath'
Write-Log -Level "OK" -Message " Release version loaded by FileReleaseVersion plugin: $version"
}
Export-ModuleMember -Function Invoke-Plugin, Get-PluginMetadata

View File

@ -1,238 +0,0 @@
#requires -Version 7.0
#requires -PSEdition Core
<#
.SYNOPSIS
Pester test plugin for the RepoUtils test engine.
.DESCRIPTION
Runs the RepoUtils Pester suite and publishes normalized coverage metrics on the
shared engine context for QualityGate.
#>
if (-not (Get-Command Import-PluginDependency -ErrorAction SilentlyContinue)) {
$srcDir = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
$pluginSupportModulePath = Join-Path $srcDir 'modules/Engine/PluginSupport.psm1'
if (Test-Path $pluginSupportModulePath -PathType Leaf) {
Import-Module $pluginSupportModulePath -Force -Global -ErrorAction Stop
}
}
function Get-JaCoCoCoverageMetrics {
param(
[Parameter(Mandatory = $true)]
[string]$ReportPath
)
if (-not (Test-Path -LiteralPath $ReportPath -PathType Leaf)) {
return [PSCustomObject]@{
Success = $false
Error = "JaCoCo coverage report not found at: $ReportPath"
}
}
[xml]$reportXml = Get-Content -LiteralPath $ReportPath -Raw -Encoding UTF8
$reportNode = $reportXml.report
if ($null -eq $reportNode) {
return [PSCustomObject]@{
Success = $false
Error = "Invalid JaCoCo report (missing root <report>): $ReportPath"
}
}
function Get-CounterRate {
param(
[Parameter(Mandatory = $true)]
[string]$CounterType
)
$counter = @($reportNode.counter) | Where-Object { $_.type -eq $CounterType } | Select-Object -First 1
if ($null -eq $counter) {
return 0.0
}
$missed = [long]$counter.missed
$covered = [long]$counter.covered
$total = $missed + $covered
if ($total -le 0) {
return 0.0
}
return [math]::Round(($covered / $total) * 100, 1)
}
function Get-CounterTotal {
param(
[Parameter(Mandatory = $true)]
[string]$CounterType
)
$counter = @($reportNode.counter) | Where-Object { $_.type -eq $CounterType } | Select-Object -First 1
if ($null -eq $counter) {
return 0
}
return [long]$counter.missed + [long]$counter.covered
}
function Get-CounterCovered {
param(
[Parameter(Mandatory = $true)]
[string]$CounterType
)
$counter = @($reportNode.counter) | Where-Object { $_.type -eq $CounterType } | Select-Object -First 1
if ($null -eq $counter) {
return 0
}
return [long]$counter.covered
}
return [PSCustomObject]@{
Success = $true
LineRate = Get-CounterRate -CounterType 'LINE'
BranchRate = Get-CounterRate -CounterType 'BRANCH'
MethodRate = Get-CounterRate -CounterType 'METHOD'
TotalMethods = Get-CounterTotal -CounterType 'METHOD'
CoveredMethods = Get-CounterCovered -CounterType 'METHOD'
CoverageFile = $ReportPath
CoverageFiles = @($ReportPath)
}
}
function Import-PesterModuleIfNeeded {
if (-not (Get-Module -ListAvailable -Name Pester | Where-Object { $_.Version.Major -ge 5 })) {
Write-Log -Level 'INFO' -Message 'Installing Pester 5...'
Install-Module Pester -MinimumVersion 5.5.0 -MaximumVersion 5.99.99 -Scope CurrentUser -Force -SkipPublisherCheck
}
Import-Module Pester -MinimumVersion 5.5.0 -MaximumVersion 5.99.99 -Force
}
function Invoke-Plugin {
param(
[Parameter(Mandatory = $true)]
$Settings
)
Import-PluginDependency -ModuleName 'Logging' -RequiredCommand 'Write-Log'
Import-PluginDependency -ModuleName 'EngineContext' -RequiredCommand 'Resolve-RelativePaths'
$pluginSettings = $Settings
$sharedSettings = $Settings.context
$scriptDir = $sharedSettings.scriptDir
if (-not $pluginSettings.testsDir) {
throw "PesterTest plugin requires 'testsDir' in scriptSettings.json."
}
$testsDirs = @(Resolve-RelativePaths -Value $pluginSettings.testsDir -BasePath $scriptDir)
$testsDir = $testsDirs[0]
$configFileSetting = 'pester.config.ps1'
if (-not [string]::IsNullOrWhiteSpace([string]$pluginSettings.configFile)) {
$configFileSetting = [string]$pluginSettings.configFile
}
$configPaths = @(Resolve-RelativePaths -Value $configFileSetting -BasePath $scriptDir)
$configPath = $configPaths[0]
if (-not (Test-Path -LiteralPath $testsDir -PathType Container)) {
throw "Pester tests directory not found at: $testsDir"
}
if (-not (Test-Path -LiteralPath $configPath -PathType Leaf)) {
throw "Pester config not found at: $configPath"
}
Import-PesterModuleIfNeeded
Write-Log -Level 'STEP' -Message 'Running Pester tests...'
Write-Log -Level 'INFO' -Message " Tests: $testsDir"
Write-Log -Level 'INFO' -Message " Config: $configPath"
Push-Location $testsDir
try {
$config = & $configPath
if ($null -eq $config) {
throw "Pester config did not return a configuration object: $configPath"
}
$config.Run.PassThru = $true
$config.Run.Exit = $false
$resultsDirSetting = [string]$pluginSettings.resultsDir
if ([string]::IsNullOrWhiteSpace($resultsDirSetting)) {
$resultsDirSetting = '..\..\..\test-results'
}
$resultsDirectory = [System.IO.Path]::GetFullPath((Join-Path $scriptDir $resultsDirSetting))
New-Item -ItemType Directory -Path $resultsDirectory -Force | Out-Null
# Point Pester report outputs at the resolved results dir (overrides pester.config relative paths).
if ($null -ne $config.TestResult -and $config.TestResult.Enabled) {
$config.TestResult.OutputPath = Join-Path $resultsDirectory 'pester-results.xml'
}
if ($null -ne $config.CodeCoverage -and $config.CodeCoverage.Enabled) {
$config.CodeCoverage.OutputPath = Join-Path $resultsDirectory 'coverage.xml'
}
Write-Log -Level 'INFO' -Message " Results: $resultsDirectory"
$result = Invoke-Pester -Configuration $config
if ($result.FailedCount -gt 0) {
throw "Pester tests failed: $($result.FailedCount) failed, $($result.PassedCount) passed, $($result.SkippedCount) skipped."
}
$coveragePath = Join-Path $resultsDirectory 'coverage.xml'
$coverageMetrics = $null
if (Test-Path -LiteralPath $coveragePath -PathType Leaf) {
$coverageMetrics = Get-JaCoCoCoverageMetrics -ReportPath $coveragePath
if (-not $coverageMetrics.Success) {
throw $coverageMetrics.Error
}
}
else {
$coverageMetrics = [PSCustomObject]@{
Success = $true
LineRate = 0.0
BranchRate = 0.0
MethodRate = 0.0
TotalMethods = 0
CoveredMethods = 0
CoverageFile = $null
CoverageFiles = @()
}
}
$testResult = [PSCustomObject]@{
Success = $true
LineRate = $coverageMetrics.LineRate
BranchRate = $coverageMetrics.BranchRate
MethodRate = $coverageMetrics.MethodRate
TotalMethods = $coverageMetrics.TotalMethods
CoveredMethods = $coverageMetrics.CoveredMethods
CoverageFile = $coverageMetrics.CoverageFile
CoverageFiles = @($coverageMetrics.CoverageFiles)
ResultsDirectory = $resultsDirectory
PesterResult = $result
}
Import-PluginDependency -ModuleName 'TestRunner' -RequiredCommand 'Publish-CoverageMetricsToSharedContext'
Publish-CoverageMetricsToSharedContext -SharedSettings $sharedSettings -TestResult $testResult
Write-Log -Level 'OK' -Message " All tests passed! ($($result.PassedCount) passed)"
Write-Log -Level 'INFO' -Message " Line Coverage: $($testResult.LineRate)%"
Write-Log -Level 'INFO' -Message " Branch Coverage: $($testResult.BranchRate)%"
Write-Log -Level 'INFO' -Message " Method Coverage: $($testResult.MethodRate)%"
}
finally {
Pop-Location
}
}
function Get-PluginMetadata {
[pscustomobject]@{ mutatesRemote = $false }
}
Export-ModuleMember -Function Invoke-Plugin, Get-PluginMetadata, Get-JaCoCoCoverageMetrics