(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:
Maksym Sadovnychyy 2026-03-08 20:00:01 +01:00
parent 95f0462495
commit e4d2eb9574
11 changed files with 406 additions and 219 deletions

View File

@ -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

View File

@ -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,72 +45,177 @@ 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 # 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
.\utils\Force-AmendTaggedCommit\Force-AmendTaggedCommit.ps1 -DryRun .\utils\Release-Package\Release-Package.ps1
``` ```
Warning: this rewrites git history.
## License ## License
By contributing, you agree that your contributions are licensed under the terms in `LICENSE.md`. By contributing, you agree that your contributions are licensed under the terms in `LICENSE.md`.

View File

@ -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.

View File

@ -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>

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

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

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

View File

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

View File

@ -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,6 +23,8 @@
<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>

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

View File

@ -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
); );
} }
} }