(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.
|
||||
|
||||
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).
|
||||
|
||||
## 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
|
||||
|
||||
### Added
|
||||
|
||||
192
CONTRIBUTING.md
192
CONTRIBUTING.md
@ -1,31 +1,30 @@
|
||||
# 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
|
||||
|
||||
1. Fork the repository.
|
||||
2. Clone your fork locally.
|
||||
3. Create a feature branch.
|
||||
4. Implement and test your changes.
|
||||
5. Submit a pull request to `main`.
|
||||
1. Fork the repository
|
||||
2. Clone your fork locally
|
||||
3. Create a new branch for your changes
|
||||
4. Make your changes
|
||||
5. Submit a pull request
|
||||
|
||||
## Development Setup
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- .NET 8 SDK or later
|
||||
- .NET 10 SDK or later
|
||||
- Git
|
||||
- PowerShell 7+ (recommended for utility scripts)
|
||||
|
||||
### Build
|
||||
### Building the Project
|
||||
|
||||
```bash
|
||||
cd src
|
||||
dotnet build MaksIT.Results.sln
|
||||
dotnet build MaksIT.Results.slnx
|
||||
```
|
||||
|
||||
### Test
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
cd src
|
||||
@ -34,9 +33,9 @@ dotnet test MaksIT.Results.Tests
|
||||
|
||||
## Commit Message Format
|
||||
|
||||
Use:
|
||||
This project uses the following commit message format:
|
||||
|
||||
```text
|
||||
```
|
||||
(type): description
|
||||
```
|
||||
|
||||
@ -46,72 +45,177 @@ Use:
|
||||
|------|-------------|
|
||||
| `(feature):` | New feature or enhancement |
|
||||
| `(bugfix):` | Bug fix |
|
||||
| `(refactor):` | Refactoring without behavior change |
|
||||
| `(chore):` | Maintenance tasks (dependencies, tooling, docs) |
|
||||
| `(refactor):` | Code refactoring without functional changes |
|
||||
| `(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
|
||||
|
||||
- Use lowercase in the description.
|
||||
- Keep it concise and specific.
|
||||
- Do not end with a period.
|
||||
- Use lowercase for the description
|
||||
- Keep the description concise but descriptive
|
||||
- No period at the end of the description
|
||||
|
||||
## Pull Request Checklist
|
||||
## Code Style
|
||||
|
||||
1. Ensure build and tests pass.
|
||||
2. Update `README.md` if behavior or usage changed.
|
||||
3. Update `CHANGELOG.md` under the target version.
|
||||
4. Keep changes scoped and explain rationale in the PR description.
|
||||
- Follow standard C# naming conventions
|
||||
- Use XML documentation comments for public APIs
|
||||
- Keep methods focused and single-purpose
|
||||
- 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
|
||||
|
||||
This project follows [Semantic Versioning](https://semver.org/):
|
||||
|
||||
- **MAJOR**: breaking API changes
|
||||
- **MINOR**: backward-compatible features
|
||||
- **PATCH**: backward-compatible fixes
|
||||
- **MAJOR** - Breaking changes
|
||||
- **MINOR** - New features (backward compatible)
|
||||
- **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/`.
|
||||
|
||||
**Usage:**
|
||||
```powershell
|
||||
.\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
|
||||
.\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
|
||||
.\utils\Force-AmendTaggedCommit\Force-AmendTaggedCommit.ps1 # Amend and force push
|
||||
.\utils\Force-AmendTaggedCommit\Force-AmendTaggedCommit.ps1 -DryRun # Preview without changes
|
||||
```
|
||||
|
||||
**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\Force-AmendTaggedCommit\Force-AmendTaggedCommit.ps1 -DryRun
|
||||
.\utils\Release-Package\Release-Package.ps1
|
||||
```
|
||||
|
||||
Warning: this rewrites git history.
|
||||
|
||||
## License
|
||||
|
||||
By contributing, you agree that your contributions are licensed under the terms in `LICENSE.md`.
|
||||
|
||||
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).
|
||||
- Built-in conversion to `IActionResult` via `ToActionResult()`.
|
||||
- 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
|
||||
|
||||
@ -88,6 +88,19 @@ public sealed record UserDto(Guid Id, string Name);
|
||||
- `detail` = joined `Messages`
|
||||
- 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
|
||||
|
||||
- Informational: `Result.Continue(...)`, `Result.SwitchingProtocols(...)`, `Result.Processing(...)`, etc.
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
@ -15,7 +15,9 @@
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<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">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<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>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
@ -8,7 +8,7 @@
|
||||
|
||||
<!-- NuGet package metadata -->
|
||||
<PackageId>MaksIT.Results</PackageId>
|
||||
<Version>2.0.0</Version>
|
||||
<Version>2.0.1</Version>
|
||||
<Authors>Maksym Sadovnychyy</Authors>
|
||||
<Company>MAKS-IT</Company>
|
||||
<Product>MaksIT.Results</Product>
|
||||
@ -23,6 +23,8 @@
|
||||
|
||||
<ItemGroup>
|
||||
<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>
|
||||
|
||||
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.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace MaksIT.Results.Mvc;
|
||||
|
||||
@ -14,6 +16,10 @@ public class ObjectResult(object? value) : IActionResult {
|
||||
public async Task ExecuteResultAsync(ActionContext context) {
|
||||
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) {
|
||||
response.StatusCode = StatusCode.Value;
|
||||
}
|
||||
@ -31,7 +37,7 @@ public class ObjectResult(object? value) : IActionResult {
|
||||
response.Body,
|
||||
Value,
|
||||
Value?.GetType() ?? typeof(object),
|
||||
_jsonSerializerOptions
|
||||
jsonOptions
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user