(bugfix): objectresult uses app json options and handles null request services; add tests, reorg test layout, update readme changelog contributing, bump to 2.0.1
This commit is contained in:
parent
95f0462495
commit
e4d2eb9574
12
CHANGELOG.md
12
CHANGELOG.md
@ -1,10 +1,20 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## v2.0.1 - 2026-03-08
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Tests for `AddJsonOptions` integration: `ObjectResult` respects `JsonSerializerOptions.DefaultIgnoreCondition` (e.g. `WhenWritingNull`) when configured via `AddControllers().AddJsonOptions(...)`.
|
||||||
|
- Test project reorganized to mirror library structure: `ResultTests`, `ResultToActionResultTests`, and `Mvc/ObjectResultTests`.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- `ObjectResult` now uses the app-configured JSON options from `IOptions<JsonOptions>` when serializing response bodies; previously it always used internal defaults.
|
||||||
|
- `ObjectResult` no longer throws when `HttpContext.RequestServices` is null (e.g. in unit tests without a service provider).
|
||||||
|
|
||||||
## v2.0.0 - 2026-02-22
|
## v2.0.0 - 2026-02-22
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
192
CONTRIBUTING.md
192
CONTRIBUTING.md
@ -1,31 +1,30 @@
|
|||||||
# Contributing to MaksIT.Results
|
# Contributing to MaksIT.Results
|
||||||
|
|
||||||
Thank you for your interest in contributing to `MaksIT.Results`.
|
Thank you for your interest in contributing to MaksIT.Results! This document provides guidelines for contributing to the project.
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
1. Fork the repository.
|
1. Fork the repository
|
||||||
2. Clone your fork locally.
|
2. Clone your fork locally
|
||||||
3. Create a feature branch.
|
3. Create a new branch for your changes
|
||||||
4. Implement and test your changes.
|
4. Make your changes
|
||||||
5. Submit a pull request to `main`.
|
5. Submit a pull request
|
||||||
|
|
||||||
## Development Setup
|
## Development Setup
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
- .NET 8 SDK or later
|
- .NET 10 SDK or later
|
||||||
- Git
|
- Git
|
||||||
- PowerShell 7+ (recommended for utility scripts)
|
|
||||||
|
|
||||||
### Build
|
### Building the Project
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd src
|
cd src
|
||||||
dotnet build MaksIT.Results.sln
|
dotnet build MaksIT.Results.slnx
|
||||||
```
|
```
|
||||||
|
|
||||||
### Test
|
### Running Tests
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd src
|
cd src
|
||||||
@ -34,9 +33,9 @@ dotnet test MaksIT.Results.Tests
|
|||||||
|
|
||||||
## Commit Message Format
|
## Commit Message Format
|
||||||
|
|
||||||
Use:
|
This project uses the following commit message format:
|
||||||
|
|
||||||
```text
|
```
|
||||||
(type): description
|
(type): description
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -46,71 +45,176 @@ Use:
|
|||||||
|------|-------------|
|
|------|-------------|
|
||||||
| `(feature):` | New feature or enhancement |
|
| `(feature):` | New feature or enhancement |
|
||||||
| `(bugfix):` | Bug fix |
|
| `(bugfix):` | Bug fix |
|
||||||
| `(refactor):` | Refactoring without behavior change |
|
| `(refactor):` | Code refactoring without functional changes |
|
||||||
| `(chore):` | Maintenance tasks (dependencies, tooling, docs) |
|
| `(perf):` | Performance improvement without changing behavior |
|
||||||
|
| `(test):` | Add or update tests |
|
||||||
|
| `(docs):` | Documentation-only changes |
|
||||||
|
| `(build):` | Build system, dependencies, packaging, or project file changes |
|
||||||
|
| `(ci):` | CI/CD pipeline or automation changes |
|
||||||
|
| `(style):` | Formatting or non-functional code style changes |
|
||||||
|
| `(revert):` | Revert a previous commit |
|
||||||
|
| `(chore):` | General maintenance tasks that do not fit the types above |
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
```
|
||||||
|
(feature): add support for custom json options in object result
|
||||||
|
(bugfix): fix objectresult using app json options when request services null
|
||||||
|
(refactor): simplify result to action result conversion
|
||||||
|
(perf): reduce allocations in problem details serialization
|
||||||
|
(test): add coverage for addjsonoptions whenwritingnull
|
||||||
|
(docs): clarify json options in readme
|
||||||
|
(build): update package metadata in MaksIT.Results.csproj
|
||||||
|
(ci): update GitHub Actions workflow for .NET 10
|
||||||
|
(style): normalize using directives in mvc tests
|
||||||
|
(revert): revert breaking change in toactionresult behavior
|
||||||
|
(chore): update copyright year to 2026
|
||||||
|
```
|
||||||
|
|
||||||
### Guidelines
|
### Guidelines
|
||||||
|
|
||||||
- Use lowercase in the description.
|
- Use lowercase for the description
|
||||||
- Keep it concise and specific.
|
- Keep the description concise but descriptive
|
||||||
- Do not end with a period.
|
- No period at the end of the description
|
||||||
|
|
||||||
## Pull Request Checklist
|
## Code Style
|
||||||
|
|
||||||
1. Ensure build and tests pass.
|
- Follow standard C# naming conventions
|
||||||
2. Update `README.md` if behavior or usage changed.
|
- Use XML documentation comments for public APIs
|
||||||
3. Update `CHANGELOG.md` under the target version.
|
- Keep methods focused and single-purpose
|
||||||
4. Keep changes scoped and explain rationale in the PR description.
|
- Write unit tests for new functionality
|
||||||
|
|
||||||
|
## Pull Request Process
|
||||||
|
|
||||||
|
1. Ensure all tests pass
|
||||||
|
2. Update documentation if needed
|
||||||
|
3. Update CHANGELOG.md with your changes under the appropriate version section
|
||||||
|
4. Submit your pull request against the `main` branch
|
||||||
|
|
||||||
## Versioning
|
## Versioning
|
||||||
|
|
||||||
This project follows [Semantic Versioning](https://semver.org/):
|
This project follows [Semantic Versioning](https://semver.org/):
|
||||||
|
|
||||||
- **MAJOR**: breaking API changes
|
- **MAJOR** - Breaking changes
|
||||||
- **MINOR**: backward-compatible features
|
- **MINOR** - New features (backward compatible)
|
||||||
- **PATCH**: backward-compatible fixes
|
- **PATCH** - Bug fixes (backward compatible)
|
||||||
|
|
||||||
## Utility Scripts
|
## Release Process
|
||||||
|
|
||||||
Scripts are located under `utils/`.
|
The release process is automated via PowerShell scripts in the `utils/` directory.
|
||||||
|
|
||||||
### Generate Coverage Badges
|
### Prerequisites
|
||||||
|
|
||||||
|
- Docker Desktop running (for Linux tests)
|
||||||
|
- GitHub CLI (`gh`) installed
|
||||||
|
- Environment variables configured:
|
||||||
|
- `NUGET_MAKS_IT` - NuGet.org API key
|
||||||
|
- `GITHUB_MAKS_IT_COM` - GitHub Personal Access Token (needs `repo` scope)
|
||||||
|
|
||||||
|
### Release Scripts Overview
|
||||||
|
|
||||||
|
| Script | Purpose |
|
||||||
|
|--------|---------|
|
||||||
|
| `Generate-CoverageBadges.ps1` | Runs tests with coverage and generates SVG badges in `assets/badges/` |
|
||||||
|
| `Release-Package.ps1` | Build, test, and publish to NuGet.org and GitHub |
|
||||||
|
| `Force-AmendTaggedCommit.ps1` | Fix mistakes in tagged commits |
|
||||||
|
|
||||||
|
### Release Workflow
|
||||||
|
|
||||||
|
1. **Update version** in `MaksIT.Results/MaksIT.Results.csproj`
|
||||||
|
|
||||||
|
2. **Update CHANGELOG.md** with your changes under the target version
|
||||||
|
|
||||||
|
3. **Review and commit** all changes:
|
||||||
|
```bash
|
||||||
|
git add -A
|
||||||
|
git commit -m "(chore): release v2.x.x"
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Create version tag**:
|
||||||
|
```bash
|
||||||
|
git tag v2.x.x
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Run release script**:
|
||||||
|
```powershell
|
||||||
|
.\utils\Release-Package\Release-Package.ps1 # Full release
|
||||||
|
.\utils\Release-Package\Release-Package.ps1 -DryRun # Test without publishing
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Generate-CoverageBadges.ps1
|
||||||
|
|
||||||
Runs tests with coverage and generates SVG badges in `assets/badges/`.
|
Runs tests with coverage and generates SVG badges in `assets/badges/`.
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
```powershell
|
```powershell
|
||||||
.\utils\Generate-CoverageBadges\Generate-CoverageBadges.ps1
|
.\utils\Generate-CoverageBadges\Generate-CoverageBadges.ps1
|
||||||
```
|
```
|
||||||
|
|
||||||
Configuration: `utils/Generate-CoverageBadges/scriptsettings.json`
|
**Configuration:** `utils/Generate-CoverageBadges/scriptsettings.json`
|
||||||
|
|
||||||
### Release NuGet Package
|
---
|
||||||
|
|
||||||
Builds, tests, packs, and publishes to NuGet and GitHub release flows.
|
### Release-Package.ps1
|
||||||
|
|
||||||
|
Builds, tests, packs, and publishes the package to NuGet.org and GitHub.
|
||||||
|
|
||||||
|
**What it does:**
|
||||||
|
1. Validates prerequisites and environment
|
||||||
|
2. Builds and tests the project
|
||||||
|
3. Creates NuGet package (.nupkg and .snupkg)
|
||||||
|
4. Pushes to NuGet.org
|
||||||
|
5. Creates GitHub release with assets
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
```powershell
|
```powershell
|
||||||
.\utils\Release-NuGetPackage\Release-NuGetPackage.ps1
|
.\utils\Release-Package\Release-Package.ps1 # Full release
|
||||||
|
.\utils\Release-Package\Release-Package.ps1 -DryRun # Test without publishing
|
||||||
```
|
```
|
||||||
|
|
||||||
Prerequisites:
|
**Configuration:** `utils/Release-Package/scriptsettings.json`
|
||||||
|
|
||||||
- Docker Desktop (for Linux test validation)
|
---
|
||||||
- GitHub CLI (`gh`)
|
|
||||||
- environment variable `NUGET_MAKS_IT`
|
|
||||||
- environment variable `GITHUB_MAKS_IT_COM`
|
|
||||||
|
|
||||||
Configuration: `utils/Release-NuGetPackage/scriptsettings.json`
|
### Force-AmendTaggedCommit.ps1
|
||||||
|
|
||||||
### Force Amend Tagged Commit
|
Fixes mistakes in the last tagged commit by amending it and force-pushing.
|
||||||
|
|
||||||
Amends the latest tagged commit and force-pushes updated branch and tag.
|
**When to use:**
|
||||||
|
- You noticed an error after committing and tagging
|
||||||
|
- Need to add forgotten files to the release commit
|
||||||
|
- Need to fix a typo in the release
|
||||||
|
|
||||||
|
**What it does:**
|
||||||
|
1. Verifies the last commit has an associated tag
|
||||||
|
2. Stages all pending changes
|
||||||
|
3. Amends the latest commit (keeps existing message)
|
||||||
|
4. Deletes and recreates the tag on the amended commit
|
||||||
|
5. Force pushes the branch and tag to origin
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
```powershell
|
```powershell
|
||||||
.\utils\Force-AmendTaggedCommit\Force-AmendTaggedCommit.ps1
|
.\utils\Force-AmendTaggedCommit\Force-AmendTaggedCommit.ps1 # Amend and force push
|
||||||
.\utils\Force-AmendTaggedCommit\Force-AmendTaggedCommit.ps1 -DryRun
|
.\utils\Force-AmendTaggedCommit\Force-AmendTaggedCommit.ps1 -DryRun # Preview without changes
|
||||||
```
|
```
|
||||||
|
|
||||||
Warning: this rewrites git history.
|
**Warning:** This rewrites history. Only use on commits that haven't been pulled by others.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Fixing a Failed Release
|
||||||
|
|
||||||
|
If the release partially failed (e.g., NuGet succeeded but GitHub failed):
|
||||||
|
|
||||||
|
1. **Re-run the release script** if it supports skipping already-completed steps
|
||||||
|
2. **If you need to fix the commit content:**
|
||||||
|
```powershell
|
||||||
|
# Make your fixes, then:
|
||||||
|
.\utils\Force-AmendTaggedCommit\Force-AmendTaggedCommit.ps1
|
||||||
|
.\utils\Release-Package\Release-Package.ps1
|
||||||
|
```
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
15
README.md
15
README.md
@ -10,7 +10,7 @@
|
|||||||
- Static factory methods for common and extended HTTP status codes (1xx, 2xx, 3xx, 4xx, 5xx).
|
- Static factory methods for common and extended HTTP status codes (1xx, 2xx, 3xx, 4xx, 5xx).
|
||||||
- Built-in conversion to `IActionResult` via `ToActionResult()`.
|
- Built-in conversion to `IActionResult` via `ToActionResult()`.
|
||||||
- RFC 7807-style error payloads for failures (`application/problem+json`).
|
- RFC 7807-style error payloads for failures (`application/problem+json`).
|
||||||
- Camel-case JSON serialization for response bodies.
|
- Camel-case JSON serialization for response bodies; respects app-configured `JsonSerializerOptions` (e.g. `AddJsonOptions` with `DefaultIgnoreCondition.WhenWritingNull`).
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@ -88,6 +88,19 @@ public sealed record UserDto(Guid Id, string Name);
|
|||||||
- `detail` = joined `Messages`
|
- `detail` = joined `Messages`
|
||||||
- content type `application/problem+json`
|
- content type `application/problem+json`
|
||||||
|
|
||||||
|
## JSON options
|
||||||
|
|
||||||
|
`ObjectResult` uses the same `JsonSerializerOptions` as your app when you configure them with `AddJsonOptions`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
builder.Services.AddControllers()
|
||||||
|
.AddJsonOptions(options => {
|
||||||
|
options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
If no options are registered, a default (camel-case) serializer is used.
|
||||||
|
|
||||||
## Status Code Factories
|
## Status Code Factories
|
||||||
|
|
||||||
- Informational: `Result.Continue(...)`, `Result.SwitchingProtocols(...)`, `Result.Processing(...)`, etc.
|
- Informational: `Result.Continue(...)`, `Result.SwitchingProtocols(...)`, `Result.Processing(...)`, etc.
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
@ -15,7 +15,9 @@
|
|||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.3.9" />
|
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.3.9" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.3" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.3" />
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.3.0" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
|
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
|||||||
67
src/MaksIT.Results.Tests/Mvc/ObjectResultTests.cs
Normal file
67
src/MaksIT.Results.Tests/Mvc/ObjectResultTests.cs
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
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;
|
||||||
|
|
||||||
|
public class ObjectResultTests {
|
||||||
|
[Fact]
|
||||||
|
public async Task ExecuteResultAsync_SerializesToCamelCaseJson() {
|
||||||
|
var testObject = new TestPascalCase { FirstName = "John", LastName = "Doe" };
|
||||||
|
var objectResult = new ObjectResult(testObject);
|
||||||
|
var context = new DefaultHttpContext();
|
||||||
|
var memoryStream = new MemoryStream();
|
||||||
|
context.Response.Body = memoryStream;
|
||||||
|
var actionContext = new ActionContext { HttpContext = context };
|
||||||
|
|
||||||
|
await objectResult.ExecuteResultAsync(actionContext);
|
||||||
|
|
||||||
|
memoryStream.Seek(0, SeekOrigin.Begin);
|
||||||
|
var json = await new StreamReader(memoryStream).ReadToEndAsync(TestContext.Current.CancellationToken);
|
||||||
|
Assert.Contains("\"firstName\"", json);
|
||||||
|
Assert.Contains("\"lastName\"", json);
|
||||||
|
Assert.DoesNotContain("\"FirstName\"", json);
|
||||||
|
Assert.DoesNotContain("\"LastName\"", json);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ExecuteResultAsync_WhenJsonOptionsWhenWritingNull_OmitsNullProperties() {
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
services.AddOptions<JsonOptions>().Configure(o => {
|
||||||
|
o.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
|
||||||
|
o.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
|
||||||
|
});
|
||||||
|
var serviceProvider = services.BuildServiceProvider();
|
||||||
|
|
||||||
|
var testObject = new TestWithNulls { Id = 1, Name = "Test", Optional = null };
|
||||||
|
var objectResult = new ObjectResult(testObject);
|
||||||
|
var context = new DefaultHttpContext { RequestServices = serviceProvider };
|
||||||
|
var memoryStream = new MemoryStream();
|
||||||
|
context.Response.Body = memoryStream;
|
||||||
|
var actionContext = new ActionContext { HttpContext = context };
|
||||||
|
|
||||||
|
await objectResult.ExecuteResultAsync(actionContext);
|
||||||
|
|
||||||
|
memoryStream.Seek(0, SeekOrigin.Begin);
|
||||||
|
var json = await new StreamReader(memoryStream).ReadToEndAsync(TestContext.Current.CancellationToken);
|
||||||
|
Assert.Contains("\"id\"", json);
|
||||||
|
Assert.Contains("\"name\"", json);
|
||||||
|
Assert.Contains("\"Test\"", json);
|
||||||
|
Assert.DoesNotContain("\"optional\"", json);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestPascalCase {
|
||||||
|
public required string FirstName { get; set; }
|
||||||
|
public required string LastName { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestWithNulls {
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Name { get; set; } = "";
|
||||||
|
public string? Optional { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
65
src/MaksIT.Results.Tests/ResultTests.cs
Normal file
65
src/MaksIT.Results.Tests/ResultTests.cs
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
using System.Net;
|
||||||
|
|
||||||
|
namespace MaksIT.Results.Tests;
|
||||||
|
|
||||||
|
public class ResultTests {
|
||||||
|
[Fact]
|
||||||
|
public void Ok_ShouldReturnSuccess() {
|
||||||
|
var message = "Operation successful";
|
||||||
|
|
||||||
|
var result = Result.Ok(message);
|
||||||
|
|
||||||
|
Assert.True(result.IsSuccess);
|
||||||
|
Assert.Contains(message, result.Messages);
|
||||||
|
Assert.Equal(HttpStatusCode.OK, result.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BadRequest_ShouldReturnFailure() {
|
||||||
|
var message = "Invalid request";
|
||||||
|
|
||||||
|
var result = Result.BadRequest(message);
|
||||||
|
|
||||||
|
Assert.False(result.IsSuccess);
|
||||||
|
Assert.Contains(message, result.Messages);
|
||||||
|
Assert.Equal(HttpStatusCode.BadRequest, result.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Generic_Ok_ShouldReturnSuccessWithValue() {
|
||||||
|
var value = 42;
|
||||||
|
var message = "Operation successful";
|
||||||
|
|
||||||
|
var result = Result<int>.Ok(value, message);
|
||||||
|
|
||||||
|
Assert.True(result.IsSuccess);
|
||||||
|
Assert.Equal(value, result.Value);
|
||||||
|
Assert.Contains(message, result.Messages);
|
||||||
|
Assert.Equal(HttpStatusCode.OK, result.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Generic_NotFound_ShouldReturnFailureWithNullValue() {
|
||||||
|
var message = "Resource not found";
|
||||||
|
|
||||||
|
var result = Result<string>.NotFound(null, message);
|
||||||
|
|
||||||
|
Assert.False(result.IsSuccess);
|
||||||
|
Assert.Null(result.Value);
|
||||||
|
Assert.Contains(message, result.Messages);
|
||||||
|
Assert.Equal(HttpStatusCode.NotFound, result.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ToResultOfType_ShouldTransformValue() {
|
||||||
|
var initialValue = 42;
|
||||||
|
var transformedValue = "42";
|
||||||
|
var result = Result<int>.Ok(initialValue);
|
||||||
|
|
||||||
|
var transformedResult = result.ToResultOfType(value => value.ToString());
|
||||||
|
|
||||||
|
Assert.True(transformedResult.IsSuccess);
|
||||||
|
Assert.Equal(transformedValue, transformedResult.Value);
|
||||||
|
Assert.Equal(result.StatusCode, transformedResult.StatusCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
48
src/MaksIT.Results.Tests/ResultToActionResultTests.cs
Normal file
48
src/MaksIT.Results.Tests/ResultToActionResultTests.cs
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
using System.Net;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using MaksIT.Results.Mvc;
|
||||||
|
|
||||||
|
namespace MaksIT.Results.Tests;
|
||||||
|
|
||||||
|
public class ResultToActionResultTests {
|
||||||
|
[Fact]
|
||||||
|
public void ToActionResult_WhenSuccess_ReturnsStatusCodeResult() {
|
||||||
|
var result = Result.Ok("Operation successful");
|
||||||
|
|
||||||
|
var actionResult = result.ToActionResult();
|
||||||
|
|
||||||
|
Assert.IsType<StatusCodeResult>(actionResult);
|
||||||
|
var statusCodeResult = (StatusCodeResult)actionResult;
|
||||||
|
Assert.Equal((int)HttpStatusCode.OK, statusCodeResult.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ToActionResult_WhenFailure_ReturnsObjectResultWithProblemDetails() {
|
||||||
|
var errorMessage = "An error occurred";
|
||||||
|
var result = Result.BadRequest(errorMessage);
|
||||||
|
|
||||||
|
var actionResult = result.ToActionResult();
|
||||||
|
|
||||||
|
Assert.IsType<ObjectResult>(actionResult);
|
||||||
|
var objectResult = (ObjectResult)actionResult;
|
||||||
|
Assert.Equal((int)HttpStatusCode.BadRequest, objectResult.StatusCode);
|
||||||
|
Assert.IsType<ProblemDetails>(objectResult.Value);
|
||||||
|
var problemDetails = (ProblemDetails)objectResult.Value!;
|
||||||
|
Assert.Equal((int)HttpStatusCode.BadRequest, problemDetails.Status);
|
||||||
|
Assert.Equal("An error occurred", problemDetails.Title);
|
||||||
|
Assert.Equal(errorMessage, problemDetails.Detail);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ToActionResult_WhenGenericSuccessWithValue_ReturnsObjectResultWithValue() {
|
||||||
|
var value = new { Id = 1, Name = "Test" };
|
||||||
|
var result = Result<object>.Ok(value);
|
||||||
|
|
||||||
|
var actionResult = result.ToActionResult();
|
||||||
|
|
||||||
|
Assert.IsType<ObjectResult>(actionResult);
|
||||||
|
var objectResult = (ObjectResult)actionResult;
|
||||||
|
Assert.Equal((int)HttpStatusCode.OK, objectResult.StatusCode);
|
||||||
|
Assert.Equal(value, objectResult.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,166 +0,0 @@
|
|||||||
using System.Net;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using MaksIT.Results.Mvc;
|
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.Results.Tests;
|
|
||||||
public class ResultTests {
|
|
||||||
[Fact]
|
|
||||||
public void Result_Ok_ShouldReturnSuccess() {
|
|
||||||
// Arrange
|
|
||||||
var message = "Operation successful";
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = Result.Ok(message);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.True(result.IsSuccess);
|
|
||||||
Assert.Contains(message, result.Messages);
|
|
||||||
Assert.Equal(HttpStatusCode.OK, result.StatusCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Result_BadRequest_ShouldReturnFailure() {
|
|
||||||
// Arrange
|
|
||||||
var message = "Invalid request";
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = Result.BadRequest(message);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.False(result.IsSuccess);
|
|
||||||
Assert.Contains(message, result.Messages);
|
|
||||||
Assert.Equal(HttpStatusCode.BadRequest, result.StatusCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Result_Generic_Ok_ShouldReturnSuccessWithValue() {
|
|
||||||
// Arrange
|
|
||||||
var value = 42;
|
|
||||||
var message = "Operation successful";
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = Result<int>.Ok(value, message);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.True(result.IsSuccess);
|
|
||||||
Assert.Equal(value, result.Value);
|
|
||||||
Assert.Contains(message, result.Messages);
|
|
||||||
Assert.Equal(HttpStatusCode.OK, result.StatusCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Result_Generic_NotFound_ShouldReturnFailureWithNullValue() {
|
|
||||||
// Arrange
|
|
||||||
var message = "Resource not found";
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = Result<string>.NotFound(null, message);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.False(result.IsSuccess);
|
|
||||||
Assert.Null(result.Value);
|
|
||||||
Assert.Contains(message, result.Messages);
|
|
||||||
Assert.Equal(HttpStatusCode.NotFound, result.StatusCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Result_ToResultOfType_ShouldTransformValue() {
|
|
||||||
// Arrange
|
|
||||||
var initialValue = 42;
|
|
||||||
var transformedValue = "42";
|
|
||||||
var result = Result<int>.Ok(initialValue);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var transformedResult = result.ToResultOfType(value => value.ToString());
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.True(transformedResult.IsSuccess);
|
|
||||||
Assert.Equal(transformedValue, transformedResult.Value);
|
|
||||||
Assert.Equal(result.StatusCode, transformedResult.StatusCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Result_ToActionResult_ShouldReturnStatusCodeResult() {
|
|
||||||
// Arrange
|
|
||||||
var result = Result.Ok("Operation successful");
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var actionResult = result.ToActionResult();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.IsType<StatusCodeResult>(actionResult);
|
|
||||||
var statusCodeResult = actionResult as StatusCodeResult;
|
|
||||||
Assert.NotNull(statusCodeResult);
|
|
||||||
Assert.Equal((int)HttpStatusCode.OK, statusCodeResult.StatusCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Result_ToActionResult_ShouldReturnObjectResultForFailure() {
|
|
||||||
// Arrange
|
|
||||||
var errorMessage = "An error occurred";
|
|
||||||
var result = Result.BadRequest(errorMessage);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var actionResult = result.ToActionResult();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.IsType<ObjectResult>(actionResult);
|
|
||||||
var objectResult = actionResult as ObjectResult;
|
|
||||||
Assert.NotNull(objectResult);
|
|
||||||
Assert.Equal((int)HttpStatusCode.BadRequest, objectResult.StatusCode);
|
|
||||||
Assert.IsType<ProblemDetails>(objectResult.Value);
|
|
||||||
var problemDetails = objectResult.Value as ProblemDetails;
|
|
||||||
Assert.NotNull(problemDetails);
|
|
||||||
Assert.Equal((int)HttpStatusCode.BadRequest, problemDetails.Status);
|
|
||||||
Assert.Equal("An error occurred", problemDetails.Title);
|
|
||||||
Assert.Equal(errorMessage, problemDetails.Detail);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Result_Generic_ToActionResult_ShouldReturnObjectResultWithValue() {
|
|
||||||
// Arrange
|
|
||||||
var value = new { Id = 1, Name = "Test" };
|
|
||||||
var result = Result<object>.Ok(value);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var actionResult = result.ToActionResult();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.IsType<ObjectResult>(actionResult);
|
|
||||||
var objectResult = actionResult as ObjectResult;
|
|
||||||
Assert.NotNull(objectResult);
|
|
||||||
Assert.Equal((int)HttpStatusCode.OK, objectResult.StatusCode);
|
|
||||||
Assert.Equal(value, objectResult.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task ObjectResult_ShouldSerializeToCamelCaseJson() {
|
|
||||||
// Arrange
|
|
||||||
var testObject = new TestPascalCase { FirstName = "John", LastName = "Doe" };
|
|
||||||
var objectResult = new ObjectResult(testObject);
|
|
||||||
var context = new DefaultHttpContext();
|
|
||||||
var memoryStream = new MemoryStream();
|
|
||||||
context.Response.Body = memoryStream;
|
|
||||||
var actionContext = new ActionContext {
|
|
||||||
HttpContext = context
|
|
||||||
};
|
|
||||||
|
|
||||||
// Act
|
|
||||||
await objectResult.ExecuteResultAsync(actionContext);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
memoryStream.Seek(0, SeekOrigin.Begin);
|
|
||||||
var json = await new StreamReader(memoryStream).ReadToEndAsync();
|
|
||||||
Assert.Contains("\"firstName\"", json);
|
|
||||||
Assert.Contains("\"lastName\"", json);
|
|
||||||
Assert.DoesNotContain("\"FirstName\"", json);
|
|
||||||
Assert.DoesNotContain("\"LastName\"", json);
|
|
||||||
}
|
|
||||||
|
|
||||||
private class TestPascalCase {
|
|
||||||
public string FirstName { get; set; }
|
|
||||||
public string LastName { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
<!-- NuGet package metadata -->
|
<!-- NuGet package metadata -->
|
||||||
<PackageId>MaksIT.Results</PackageId>
|
<PackageId>MaksIT.Results</PackageId>
|
||||||
<Version>2.0.0</Version>
|
<Version>2.0.1</Version>
|
||||||
<Authors>Maksym Sadovnychyy</Authors>
|
<Authors>Maksym Sadovnychyy</Authors>
|
||||||
<Company>MAKS-IT</Company>
|
<Company>MAKS-IT</Company>
|
||||||
<Product>MaksIT.Results</Product>
|
<Product>MaksIT.Results</Product>
|
||||||
@ -23,11 +23,13 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Abstractions" Version="2.3.9" />
|
<PackageReference Include="Microsoft.AspNetCore.Mvc.Abstractions" Version="2.3.9" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.3" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.3" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Include="..\..\LICENSE.md" Pack="true" PackagePath="\" Link="LICENSE.md" />
|
<None Include="..\..\LICENSE.md" Pack="true" PackagePath="\" Link="LICENSE.md" />
|
||||||
<None Include="..\..\README.md" Pack="true" PackagePath="\" Link="README.md" />
|
<None Include="..\..\README.md" Pack="true" PackagePath="\" Link="README.md" />
|
||||||
<None Include="..\..\CHANGELOG.md" Pack="true" PackagePath="\" Link="CHANGELOG.md" />
|
<None Include="..\..\CHANGELOG.md" Pack="true" PackagePath="\" Link="CHANGELOG.md" />
|
||||||
|
|
||||||
<None Include="..\..\assets\badges\**\*" Link="assets\badges\%(RecursiveDir)%(Filename)%(Extension)">
|
<None Include="..\..\assets\badges\**\*" Link="assets\badges\%(RecursiveDir)%(Filename)%(Extension)">
|
||||||
|
|||||||
36
src/MaksIT.Results/Mvc/JsonOptions.cs
Normal file
36
src/MaksIT.Results/Mvc/JsonOptions.cs
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace MaksIT.Results.Mvc;
|
||||||
|
|
||||||
|
//
|
||||||
|
// Summary:
|
||||||
|
// Options to configure Microsoft.AspNetCore.Mvc.Formatters.SystemTextJsonInputFormatter
|
||||||
|
// and Microsoft.AspNetCore.Mvc.Formatters.SystemTextJsonOutputFormatter.
|
||||||
|
public class JsonOptions {
|
||||||
|
public JsonOptions() {
|
||||||
|
JsonSerializerOptions = new JsonSerializerOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Summary:
|
||||||
|
// Gets or sets a flag to determine whether error messages from JSON deserialization
|
||||||
|
// by the Microsoft.AspNetCore.Mvc.Formatters.SystemTextJsonInputFormatter will
|
||||||
|
// be added to the Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateDictionary. If
|
||||||
|
// false, a generic error message will be used instead.
|
||||||
|
//
|
||||||
|
// Value:
|
||||||
|
// The default value is true.
|
||||||
|
//
|
||||||
|
// Remarks:
|
||||||
|
// Error messages in the Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateDictionary
|
||||||
|
// are often communicated to clients, either in HTML or using Microsoft.AspNetCore.Mvc.BadRequestObjectResult.
|
||||||
|
// In effect, this setting controls whether clients can receive detailed error messages
|
||||||
|
// about submitted JSON data.
|
||||||
|
public bool AllowInputFormatterExceptionMessages { get; set; }
|
||||||
|
//
|
||||||
|
// Summary:
|
||||||
|
// Gets the System.Text.Json.JsonSerializerOptions used by Microsoft.AspNetCore.Mvc.Formatters.SystemTextJsonInputFormatter
|
||||||
|
// and Microsoft.AspNetCore.Mvc.Formatters.SystemTextJsonOutputFormatter.
|
||||||
|
public JsonSerializerOptions JsonSerializerOptions { get; }
|
||||||
|
}
|
||||||
@ -1,5 +1,7 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace MaksIT.Results.Mvc;
|
namespace MaksIT.Results.Mvc;
|
||||||
|
|
||||||
@ -14,6 +16,10 @@ public class ObjectResult(object? value) : IActionResult {
|
|||||||
public async Task ExecuteResultAsync(ActionContext context) {
|
public async Task ExecuteResultAsync(ActionContext context) {
|
||||||
var response = context.HttpContext.Response;
|
var response = context.HttpContext.Response;
|
||||||
|
|
||||||
|
// Prefer app-configured JSON options (from AddJsonOptions), fall back to default
|
||||||
|
var jsonOptions = context.HttpContext.RequestServices?.GetService<IOptions<JsonOptions>>()?.Value?.JsonSerializerOptions
|
||||||
|
?? _jsonSerializerOptions;
|
||||||
|
|
||||||
if (StatusCode.HasValue) {
|
if (StatusCode.HasValue) {
|
||||||
response.StatusCode = StatusCode.Value;
|
response.StatusCode = StatusCode.Value;
|
||||||
}
|
}
|
||||||
@ -31,7 +37,7 @@ public class ObjectResult(object? value) : IActionResult {
|
|||||||
response.Body,
|
response.Body,
|
||||||
Value,
|
Value,
|
||||||
Value?.GetType() ?? typeof(object),
|
Value?.GetType() ?? typeof(object),
|
||||||
_jsonSerializerOptions
|
jsonOptions
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user