From f3fc197375fc4c1f38acf5413d7c9d39b79a24ff Mon Sep 17 00:00:00 2001
From: sheepla <62412884+sheepla@users.noreply.github.com>
Date: Sat, 2 May 2026 13:45:16 +0900
Subject: [PATCH 1/3] Fix issue #16: OpenAPI response body type
---
...points.Extensions.CsvHelper.csproj.lscache | 1 +
.../Internal/EndpointRegistry.cs | 20 +-
.../AxisEndpoints.Example.csproj.lscache | 1 -
.../AxisEndpoints.Tests.csproj | 1 +
.../AxisEndpoints.Tests.csproj.lscache | 210 ++++++++++++++++--
.../Integration/EndpointIntegrationTests.cs | 15 ++
.../Integration/TestWebApplicationFactory.cs | 3 +
7 files changed, 225 insertions(+), 26 deletions(-)
diff --git a/src/AxisEndpoints.Extensions.CsvHelper/AxisEndpoints.Extensions.CsvHelper.csproj.lscache b/src/AxisEndpoints.Extensions.CsvHelper/AxisEndpoints.Extensions.CsvHelper.csproj.lscache
index 1a6c8de..6484baa 100644
--- a/src/AxisEndpoints.Extensions.CsvHelper/AxisEndpoints.Extensions.CsvHelper.csproj.lscache
+++ b/src/AxisEndpoints.Extensions.CsvHelper/AxisEndpoints.Extensions.CsvHelper.csproj.lscache
@@ -58,6 +58,7 @@ CsvBindingException.cs
CsvBindingExceptionFilter.cs
CsvRequest.cs
CsvResponse.cs
+ICsvBindingErrors.cs
obj/Debug/net10.0/
.NETCoreApp,Version=v10.0.AssemblyAttributes.cs
AxisEndpoints.Extensions.CsvHelper.AssemblyInfo.cs
diff --git a/src/AxisEndpoints/Internal/EndpointRegistry.cs b/src/AxisEndpoints/Internal/EndpointRegistry.cs
index 81aaaec..ef0b329 100644
--- a/src/AxisEndpoints/Internal/EndpointRegistry.cs
+++ b/src/AxisEndpoints/Internal/EndpointRegistry.cs
@@ -474,13 +474,14 @@ EndpointConfiguration config
// When TResult is Response
, register Produces(200) automatically from the
// inferred response type. When TResult is IResult, skip auto-registration — the
// caller is responsible for declaring response shapes via ProducesSuccess/ProducesError.
+ var openApiResponseType = GetOpenApiResponseType(config.ResponseType);
var isIResult =
- config.ResponseType == typeof(IResult)
- || typeof(IResult).IsAssignableFrom(config.ResponseType);
+ openApiResponseType == typeof(IResult)
+ || typeof(IResult).IsAssignableFrom(openApiResponseType);
if (!isIResult)
{
- routeBuilder.Produces(200, config.ResponseType);
+ routeBuilder.Produces(200, openApiResponseType);
}
}
@@ -490,6 +491,19 @@ EndpointConfiguration config
}
}
+ private static Type GetOpenApiResponseType(Type responseType)
+ {
+ if (
+ responseType.IsGenericType
+ && responseType.GetGenericTypeDefinition() == typeof(Response<>)
+ )
+ {
+ return responseType.GetGenericArguments()[0];
+ }
+
+ return responseType;
+ }
+
private static void ApplyGroupMetadata(
RouteGroupBuilder groupBuilder,
EndpointGroupConfiguration config
diff --git a/tests/AxisEndpoints.Example/AxisEndpoints.Example.csproj.lscache b/tests/AxisEndpoints.Example/AxisEndpoints.Example.csproj.lscache
index 96b621f..5c8a91b 100644
--- a/tests/AxisEndpoints.Example/AxisEndpoints.Example.csproj.lscache
+++ b/tests/AxisEndpoints.Example/AxisEndpoints.Example.csproj.lscache
@@ -73,7 +73,6 @@ Features/
FindById/
FindByIdEndpoint.cs
FindByIdRequest.cs
- FindUserByIdRequest.cs
ImportFromCsv/ImportFromCsvEndpoint.cs
List/
ListUsersEndpoint.cs
diff --git a/tests/AxisEndpoints.Tests/AxisEndpoints.Tests.csproj b/tests/AxisEndpoints.Tests/AxisEndpoints.Tests.csproj
index 1fb715b..dbf6f0e 100644
--- a/tests/AxisEndpoints.Tests/AxisEndpoints.Tests.csproj
+++ b/tests/AxisEndpoints.Tests/AxisEndpoints.Tests.csproj
@@ -9,6 +9,7 @@
+
diff --git a/tests/AxisEndpoints.Tests/AxisEndpoints.Tests.csproj.lscache b/tests/AxisEndpoints.Tests/AxisEndpoints.Tests.csproj.lscache
index a5c9fa1..adf0896 100644
--- a/tests/AxisEndpoints.Tests/AxisEndpoints.Tests.csproj.lscache
+++ b/tests/AxisEndpoints.Tests/AxisEndpoints.Tests.csproj.lscache
@@ -39,7 +39,7 @@ TemporaryDependencyNodeTargetIdentifier=net10.0
/define:TRACE;DEBUG;NET;NET10_0;NETCOREAPP;NET5_0_OR_GREATER;NET6_0_OR_GREATER;NET7_0_OR_GREATER;NET8_0_OR_GREATER;NET9_0_OR_GREATER;NET10_0_OR_GREATER;NETCOREAPP1_0_OR_GREATER;NETCOREAPP1_1_OR_GREATER;NETCOREAPP2_0_OR_GREATER;NETCOREAPP2_1_OR_GREATER;NETCOREAPP2_2_OR_GREATER;NETCOREAPP3_0_OR_GREATER;NETCOREAPP3_1_OR_GREATER
/highentropyva+
/nullable:enable
-/features:"InterceptorsNamespaces=;Microsoft.Extensions.Validation.Generated"
+/features:"InterceptorsNamespaces=;Microsoft.AspNetCore.OpenApi.Generated;;Microsoft.Extensions.Validation.Generated"
/debug+
/debug:portable
/filealign:512
@@ -54,14 +54,186 @@ TemporaryDependencyNodeTargetIdentifier=net10.0
/warnaserror+:NU1605,SYSLIB0011
[sourceFiles]
-/microsoft.net.test.sdk/17.14.1/build/net8.0/Microsoft.NET.Test.Sdk.Program.cs
+../../.dotnet-cli/.nuget/packages/microsoft.net.test.sdk/17.14.1/build/net8.0/Microsoft.NET.Test.Sdk.Program.cs
+Integration/
+ EndpointIntegrationTests.cs
+ TestEndpoints.cs
+ TestWebApplicationFactory.cs
obj/Debug/net10.0/
.NETCoreApp,Version=v10.0.AssemblyAttributes.cs
AxisEndpoints.Tests.AssemblyInfo.cs
AxisEndpoints.Tests.GlobalUsings.g.cs
-UnitTest1.cs
+Unit/
+ EndpointConfigurationTests.cs
+ EndpointGroupConfigurationTests.cs
+ ResponseExecutorTests.cs
+ ServiceRegistrationTests.cs
[metadataReferences]
+../../.dotnet-cli/.nuget/packages/
+ fluentassertions/8.9.0/lib/net6.0/FluentAssertions.dll
+ microsoft.aspnetcore.mvc.testing/10.0.7/lib/net10.0/Microsoft.AspNetCore.Mvc.Testing.dll
+ microsoft.aspnetcore.openapi/10.0.0/lib/net10.0/Microsoft.AspNetCore.OpenApi.dll
+ microsoft.aspnetcore.testhost/10.0.7/lib/net10.0/Microsoft.AspNetCore.TestHost.dll
+ microsoft.codecoverage/17.14.1/lib/net8.0/Microsoft.VisualStudio.CodeCoverage.Shim.dll
+ microsoft.extensions.configuration.abstractions/10.0.7/lib/net10.0/Microsoft.Extensions.Configuration.Abstractions.dll
+ microsoft.extensions.configuration.binder/10.0.7/lib/net10.0/Microsoft.Extensions.Configuration.Binder.dll
+ microsoft.extensions.configuration.commandline/10.0.7/lib/net10.0/Microsoft.Extensions.Configuration.CommandLine.dll
+ microsoft.extensions.configuration.environmentvariables/10.0.7/lib/net10.0/Microsoft.Extensions.Configuration.EnvironmentVariables.dll
+ microsoft.extensions.configuration.fileextensions/10.0.7/lib/net10.0/Microsoft.Extensions.Configuration.FileExtensions.dll
+ microsoft.extensions.configuration.json/10.0.7/lib/net10.0/Microsoft.Extensions.Configuration.Json.dll
+ microsoft.extensions.configuration.usersecrets/10.0.7/lib/net10.0/Microsoft.Extensions.Configuration.UserSecrets.dll
+ microsoft.extensions.configuration/10.0.7/lib/net10.0/Microsoft.Extensions.Configuration.dll
+ microsoft.extensions.dependencyinjection.abstractions/10.0.7/lib/net10.0/Microsoft.Extensions.DependencyInjection.Abstractions.dll
+ microsoft.extensions.dependencyinjection/10.0.7/lib/net10.0/Microsoft.Extensions.DependencyInjection.dll
+ microsoft.extensions.dependencymodel/10.0.7/lib/net10.0/Microsoft.Extensions.DependencyModel.dll
+ microsoft.extensions.diagnostics.abstractions/10.0.7/lib/net10.0/Microsoft.Extensions.Diagnostics.Abstractions.dll
+ microsoft.extensions.diagnostics/10.0.7/lib/net10.0/Microsoft.Extensions.Diagnostics.dll
+ microsoft.extensions.fileproviders.abstractions/10.0.7/lib/net10.0/Microsoft.Extensions.FileProviders.Abstractions.dll
+ microsoft.extensions.fileproviders.physical/10.0.7/lib/net10.0/Microsoft.Extensions.FileProviders.Physical.dll
+ microsoft.extensions.filesystemglobbing/10.0.7/lib/net10.0/Microsoft.Extensions.FileSystemGlobbing.dll
+ microsoft.extensions.hosting.abstractions/10.0.7/lib/net10.0/Microsoft.Extensions.Hosting.Abstractions.dll
+ microsoft.extensions.hosting/10.0.7/lib/net10.0/Microsoft.Extensions.Hosting.dll
+ microsoft.extensions.logging.abstractions/10.0.7/lib/net10.0/Microsoft.Extensions.Logging.Abstractions.dll
+ microsoft.extensions.logging.configuration/10.0.7/lib/net10.0/Microsoft.Extensions.Logging.Configuration.dll
+ microsoft.extensions.logging.console/10.0.7/lib/net10.0/Microsoft.Extensions.Logging.Console.dll
+ microsoft.extensions.logging.debug/10.0.7/lib/net10.0/Microsoft.Extensions.Logging.Debug.dll
+ microsoft.extensions.logging.eventlog/10.0.7/lib/net10.0/Microsoft.Extensions.Logging.EventLog.dll
+ microsoft.extensions.logging.eventsource/10.0.7/lib/net10.0/Microsoft.Extensions.Logging.EventSource.dll
+ microsoft.extensions.logging/10.0.7/lib/net10.0/Microsoft.Extensions.Logging.dll
+ microsoft.extensions.options.configurationextensions/10.0.7/lib/net10.0/Microsoft.Extensions.Options.ConfigurationExtensions.dll
+ microsoft.extensions.options/10.0.7/lib/net10.0/Microsoft.Extensions.Options.dll
+ microsoft.extensions.primitives/10.0.7/lib/net10.0/Microsoft.Extensions.Primitives.dll
+ microsoft.openapi/2.0.0/lib/net8.0/Microsoft.OpenApi.dll
+ microsoft.testplatform.testhost/17.14.1/lib/net8.0/
+ Microsoft.TestPlatform.CommunicationUtilities.dll
+ Microsoft.TestPlatform.CoreUtilities.dll
+ Microsoft.TestPlatform.CrossPlatEngine.dll
+ Microsoft.TestPlatform.PlatformAbstractions.dll
+ Microsoft.TestPlatform.Utilities.dll
+ Microsoft.VisualStudio.TestPlatform.Common.dll
+ Microsoft.VisualStudio.TestPlatform.ObjectModel.dll
+ testhost.dll
+ newtonsoft.json/13.0.3/lib/net6.0/Newtonsoft.Json.dll
+ system.diagnostics.eventlog/10.0.7/lib/net10.0/System.Diagnostics.EventLog.dll
+ xunit.abstractions/2.0.3/lib/netstandard2.0/xunit.abstractions.dll
+ xunit.assert/2.9.3/lib/net6.0/xunit.assert.dll
+ xunit.extensibility.core/2.9.3/lib/netstandard1.1/xunit.core.dll
+ xunit.extensibility.execution/2.9.3/lib/netstandard1.1/xunit.execution.dotnet.dll
+../../src/AxisEndpoints/obj/Debug/net10.0/ref/AxisEndpoints.dll
+/packs/Microsoft.AspNetCore.App.Ref/10.0.2/ref/net10.0/
+ Microsoft.AspNetCore.Antiforgery.dll
+ Microsoft.AspNetCore.Authentication.Abstractions.dll
+ Microsoft.AspNetCore.Authentication.BearerToken.dll
+ Microsoft.AspNetCore.Authentication.Cookies.dll
+ Microsoft.AspNetCore.Authentication.Core.dll
+ Microsoft.AspNetCore.Authentication.dll
+ Microsoft.AspNetCore.Authentication.OAuth.dll
+ Microsoft.AspNetCore.Authorization.dll
+ Microsoft.AspNetCore.Authorization.Policy.dll
+ Microsoft.AspNetCore.Components.Authorization.dll
+ Microsoft.AspNetCore.Components.dll
+ Microsoft.AspNetCore.Components.Endpoints.dll
+ Microsoft.AspNetCore.Components.Forms.dll
+ Microsoft.AspNetCore.Components.Server.dll
+ Microsoft.AspNetCore.Components.Web.dll
+ Microsoft.AspNetCore.Connections.Abstractions.dll
+ Microsoft.AspNetCore.CookiePolicy.dll
+ Microsoft.AspNetCore.Cors.dll
+ Microsoft.AspNetCore.Cryptography.Internal.dll
+ Microsoft.AspNetCore.Cryptography.KeyDerivation.dll
+ Microsoft.AspNetCore.DataProtection.Abstractions.dll
+ Microsoft.AspNetCore.DataProtection.dll
+ Microsoft.AspNetCore.DataProtection.Extensions.dll
+ Microsoft.AspNetCore.Diagnostics.Abstractions.dll
+ Microsoft.AspNetCore.Diagnostics.dll
+ Microsoft.AspNetCore.Diagnostics.HealthChecks.dll
+ Microsoft.AspNetCore.dll
+ Microsoft.AspNetCore.HostFiltering.dll
+ Microsoft.AspNetCore.Hosting.Abstractions.dll
+ Microsoft.AspNetCore.Hosting.dll
+ Microsoft.AspNetCore.Hosting.Server.Abstractions.dll
+ Microsoft.AspNetCore.Html.Abstractions.dll
+ Microsoft.AspNetCore.Http.Abstractions.dll
+ Microsoft.AspNetCore.Http.Connections.Common.dll
+ Microsoft.AspNetCore.Http.Connections.dll
+ Microsoft.AspNetCore.Http.dll
+ Microsoft.AspNetCore.Http.Extensions.dll
+ Microsoft.AspNetCore.Http.Features.dll
+ Microsoft.AspNetCore.Http.Results.dll
+ Microsoft.AspNetCore.HttpLogging.dll
+ Microsoft.AspNetCore.HttpOverrides.dll
+ Microsoft.AspNetCore.HttpsPolicy.dll
+ Microsoft.AspNetCore.Identity.dll
+ Microsoft.AspNetCore.Localization.dll
+ Microsoft.AspNetCore.Localization.Routing.dll
+ Microsoft.AspNetCore.Metadata.dll
+ Microsoft.AspNetCore.Mvc.Abstractions.dll
+ Microsoft.AspNetCore.Mvc.ApiExplorer.dll
+ Microsoft.AspNetCore.Mvc.Core.dll
+ Microsoft.AspNetCore.Mvc.Cors.dll
+ Microsoft.AspNetCore.Mvc.DataAnnotations.dll
+ Microsoft.AspNetCore.Mvc.dll
+ Microsoft.AspNetCore.Mvc.Formatters.Json.dll
+ Microsoft.AspNetCore.Mvc.Formatters.Xml.dll
+ Microsoft.AspNetCore.Mvc.Localization.dll
+ Microsoft.AspNetCore.Mvc.Razor.dll
+ Microsoft.AspNetCore.Mvc.RazorPages.dll
+ Microsoft.AspNetCore.Mvc.TagHelpers.dll
+ Microsoft.AspNetCore.Mvc.ViewFeatures.dll
+ Microsoft.AspNetCore.OutputCaching.dll
+ Microsoft.AspNetCore.RateLimiting.dll
+ Microsoft.AspNetCore.Razor.dll
+ Microsoft.AspNetCore.Razor.Runtime.dll
+ Microsoft.AspNetCore.RequestDecompression.dll
+ Microsoft.AspNetCore.ResponseCaching.Abstractions.dll
+ Microsoft.AspNetCore.ResponseCaching.dll
+ Microsoft.AspNetCore.ResponseCompression.dll
+ Microsoft.AspNetCore.Rewrite.dll
+ Microsoft.AspNetCore.Routing.Abstractions.dll
+ Microsoft.AspNetCore.Routing.dll
+ Microsoft.AspNetCore.Server.HttpSys.dll
+ Microsoft.AspNetCore.Server.IIS.dll
+ Microsoft.AspNetCore.Server.IISIntegration.dll
+ Microsoft.AspNetCore.Server.Kestrel.Core.dll
+ Microsoft.AspNetCore.Server.Kestrel.dll
+ Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.dll
+ Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.dll
+ Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.dll
+ Microsoft.AspNetCore.Session.dll
+ Microsoft.AspNetCore.SignalR.Common.dll
+ Microsoft.AspNetCore.SignalR.Core.dll
+ Microsoft.AspNetCore.SignalR.dll
+ Microsoft.AspNetCore.SignalR.Protocols.Json.dll
+ Microsoft.AspNetCore.StaticAssets.dll
+ Microsoft.AspNetCore.StaticFiles.dll
+ Microsoft.AspNetCore.WebSockets.dll
+ Microsoft.AspNetCore.WebUtilities.dll
+ Microsoft.Extensions.Caching.Abstractions.dll
+ Microsoft.Extensions.Caching.Memory.dll
+ Microsoft.Extensions.Configuration.Ini.dll
+ Microsoft.Extensions.Configuration.KeyPerFile.dll
+ Microsoft.Extensions.Configuration.Xml.dll
+ Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions.dll
+ Microsoft.Extensions.Diagnostics.HealthChecks.dll
+ Microsoft.Extensions.Features.dll
+ Microsoft.Extensions.FileProviders.Composite.dll
+ Microsoft.Extensions.FileProviders.Embedded.dll
+ Microsoft.Extensions.Http.dll
+ Microsoft.Extensions.Identity.Core.dll
+ Microsoft.Extensions.Identity.Stores.dll
+ Microsoft.Extensions.Localization.Abstractions.dll
+ Microsoft.Extensions.Localization.dll
+ Microsoft.Extensions.Logging.TraceSource.dll
+ Microsoft.Extensions.ObjectPool.dll
+ Microsoft.Extensions.Options.DataAnnotations.dll
+ Microsoft.Extensions.Validation.dll
+ Microsoft.Extensions.WebEncoders.dll
+ Microsoft.JSInterop.dll
+ Microsoft.Net.Http.Headers.dll
+ System.Formats.Cbor.dll
+ System.Security.Cryptography.Xml.dll
+ System.Threading.RateLimiting.dll
/packs/Microsoft.NETCore.App.Ref/10.0.2/ref/net10.0/
Microsoft.CSharp.dll
Microsoft.VisualBasic.Core.dll
@@ -230,24 +402,21 @@ UnitTest1.cs
System.Xml.XPath.dll
System.Xml.XPath.XDocument.dll
WindowsBase.dll
-/
- microsoft.codecoverage/17.14.1/lib/net8.0/Microsoft.VisualStudio.CodeCoverage.Shim.dll
- microsoft.testplatform.testhost/17.14.1/lib/net8.0/
- Microsoft.TestPlatform.CommunicationUtilities.dll
- Microsoft.TestPlatform.CoreUtilities.dll
- Microsoft.TestPlatform.CrossPlatEngine.dll
- Microsoft.TestPlatform.PlatformAbstractions.dll
- Microsoft.TestPlatform.Utilities.dll
- Microsoft.VisualStudio.TestPlatform.Common.dll
- Microsoft.VisualStudio.TestPlatform.ObjectModel.dll
- testhost.dll
- newtonsoft.json/13.0.3/lib/net6.0/Newtonsoft.Json.dll
- xunit.abstractions/2.0.3/lib/netstandard2.0/xunit.abstractions.dll
- xunit.assert/2.9.3/lib/net6.0/xunit.assert.dll
- xunit.extensibility.core/2.9.3/lib/netstandard1.1/xunit.core.dll
- xunit.extensibility.execution/2.9.3/lib/netstandard1.1/xunit.execution.dotnet.dll
[analyzerReferences]
+../../.dotnet-cli/.nuget/packages/
+ microsoft.aspnetcore.openapi/10.0.0/analyzers/dotnet/cs/Microsoft.AspNetCore.OpenApi.SourceGenerators.dll
+ microsoft.extensions.logging.abstractions/10.0.7/analyzers/dotnet/roslyn4.4/cs/Microsoft.Extensions.Logging.Generators.dll
+ microsoft.extensions.options/10.0.7/analyzers/dotnet/roslyn4.4/cs/Microsoft.Extensions.Options.SourceGeneration.dll
+ xunit.analyzers/1.18.0/analyzers/dotnet/cs/
+ xunit.analyzers.dll
+ xunit.analyzers.fixes.dll
+/packs/Microsoft.AspNetCore.App.Ref/10.0.2/analyzers/dotnet/cs/
+ Microsoft.AspNetCore.App.Analyzers.dll
+ Microsoft.AspNetCore.App.CodeFixes.dll
+ Microsoft.AspNetCore.App.SourceGenerators.dll
+ Microsoft.AspNetCore.Components.Analyzers.dll
+ Microsoft.Extensions.Validation.ValidationsGenerator.dll
/packs/Microsoft.NETCore.App.Ref/10.0.2/analyzers/dotnet/cs/
Microsoft.Interop.ComInterfaceGenerator.dll
Microsoft.Interop.JavaScript.JSImportGenerator.dll
@@ -258,9 +427,6 @@ UnitTest1.cs
/sdk/10.0.102/Sdks/Microsoft.NET.Sdk/analyzers/
Microsoft.CodeAnalysis.CSharp.NetAnalyzers.dll
Microsoft.CodeAnalysis.NetAnalyzers.dll
-/xunit.analyzers/1.18.0/analyzers/dotnet/cs/
- xunit.analyzers.dll
- xunit.analyzers.fixes.dll
[analyzerConfigFiles]
/sdk/10.0.102/Sdks/Microsoft.NET.Sdk/analyzers/build/config/analysislevel_10_default.globalconfig
diff --git a/tests/AxisEndpoints.Tests/Integration/EndpointIntegrationTests.cs b/tests/AxisEndpoints.Tests/Integration/EndpointIntegrationTests.cs
index 1cda29f..4a14ae6 100644
--- a/tests/AxisEndpoints.Tests/Integration/EndpointIntegrationTests.cs
+++ b/tests/AxisEndpoints.Tests/Integration/EndpointIntegrationTests.cs
@@ -1,5 +1,6 @@
using System.Net;
using System.Net.Http.Json;
+using System.Text.Json.Nodes;
using FluentAssertions;
namespace AxisEndpoints.Tests.Integration;
@@ -69,4 +70,18 @@ public async Task GetGrouped_Returns200WithGroupPrefix()
body.Should().NotBeNull();
body!.Message.Should().Be("Grouped!");
}
+
+ [Fact]
+ public async Task OpenApi_UsesResponseBodyTypeInsteadOfResponseWrapper()
+ {
+ var document = await _client.GetFromJsonAsync("/openapi/v1.json");
+
+ document.Should().NotBeNull();
+
+ var schema = document!["paths"]?["/hello"]?["get"]?["responses"]?["200"]?["content"]?["application/json"]?["schema"];
+ schema.Should().NotBeNull();
+ schema!["$ref"]!.GetValue().Should().Be("#/components/schemas/HelloResponse");
+
+ document["components"]?["schemas"]?["ResponseOfHelloResponse"].Should().BeNull();
+ }
}
diff --git a/tests/AxisEndpoints.Tests/Integration/TestWebApplicationFactory.cs b/tests/AxisEndpoints.Tests/Integration/TestWebApplicationFactory.cs
index 3798c88..ae6385f 100644
--- a/tests/AxisEndpoints.Tests/Integration/TestWebApplicationFactory.cs
+++ b/tests/AxisEndpoints.Tests/Integration/TestWebApplicationFactory.cs
@@ -2,6 +2,7 @@
using AxisEndpoints.Extensions;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.TestHost;
+using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace AxisEndpoints.Tests.Integration;
@@ -17,9 +18,11 @@ public async Task InitializeAsync()
{
var builder = WebApplication.CreateBuilder();
builder.WebHost.UseTestServer();
+ builder.Services.AddOpenApi();
builder.Services.AddAxisEndpoints(_testAssembly);
_app = builder.Build();
+ _app.MapOpenApi();
_app.MapAxisEndpoints(_testAssembly);
await _app.StartAsync();
From 22d2c79eb2071d696b101da13dcd1a886f399aeb Mon Sep 17 00:00:00 2001
From: sheepla <62412884+sheepla@users.noreply.github.com>
Date: Sat, 2 May 2026 14:05:40 +0900
Subject: [PATCH 2/3] Fix OpenAPI response body schemas
---
.../Internal/EndpointRegistry.cs | 5 +-
.../AxisEndpoints.Example.Tests.csproj | 1 +
.../ExampleWebApplicationFactory.cs | 3 ++
.../OpenApiTests.cs | 47 +++++++++++++++++++
.../Integration/EndpointIntegrationTests.cs | 18 +++++++
5 files changed, 73 insertions(+), 1 deletion(-)
create mode 100644 tests/AxisEndpoints.Example.Tests/OpenApiTests.cs
diff --git a/src/AxisEndpoints/Internal/EndpointRegistry.cs b/src/AxisEndpoints/Internal/EndpointRegistry.cs
index ef0b329..6aa5efa 100644
--- a/src/AxisEndpoints/Internal/EndpointRegistry.cs
+++ b/src/AxisEndpoints/Internal/EndpointRegistry.cs
@@ -475,11 +475,14 @@ EndpointConfiguration config
// inferred response type. When TResult is IResult, skip auto-registration — the
// caller is responsible for declaring response shapes via ProducesSuccess/ProducesError.
var openApiResponseType = GetOpenApiResponseType(config.ResponseType);
+ var isNoBodyResponse =
+ openApiResponseType == typeof(EmptyResponse)
+ || typeof(EmptyResponse).IsAssignableFrom(openApiResponseType);
var isIResult =
openApiResponseType == typeof(IResult)
|| typeof(IResult).IsAssignableFrom(openApiResponseType);
- if (!isIResult)
+ if (!isIResult && !isNoBodyResponse)
{
routeBuilder.Produces(200, openApiResponseType);
}
diff --git a/tests/AxisEndpoints.Example.Tests/AxisEndpoints.Example.Tests.csproj b/tests/AxisEndpoints.Example.Tests/AxisEndpoints.Example.Tests.csproj
index 0d29ef5..dc52533 100644
--- a/tests/AxisEndpoints.Example.Tests/AxisEndpoints.Example.Tests.csproj
+++ b/tests/AxisEndpoints.Example.Tests/AxisEndpoints.Example.Tests.csproj
@@ -9,6 +9,7 @@
+
diff --git a/tests/AxisEndpoints.Example.Tests/ExampleWebApplicationFactory.cs b/tests/AxisEndpoints.Example.Tests/ExampleWebApplicationFactory.cs
index e1190f4..4ff8d02 100644
--- a/tests/AxisEndpoints.Example.Tests/ExampleWebApplicationFactory.cs
+++ b/tests/AxisEndpoints.Example.Tests/ExampleWebApplicationFactory.cs
@@ -4,6 +4,7 @@
using AxisEndpoints.Extensions.CsvHelper;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.TestHost;
+using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace AxisEndpoints.Example.Tests;
@@ -22,10 +23,12 @@ public async Task InitializeAsync()
var builder = WebApplication.CreateBuilder();
builder.WebHost.UseTestServer();
+ builder.Services.AddOpenApi();
builder.Services.AddAxisEndpoints(ExampleAssembly);
builder.Services.AddAxisEndpointsCsvHelper();
_app = builder.Build();
+ _app.MapOpenApi();
_app.MapAxisEndpoints(ExampleAssembly);
await _app.StartAsync();
diff --git a/tests/AxisEndpoints.Example.Tests/OpenApiTests.cs b/tests/AxisEndpoints.Example.Tests/OpenApiTests.cs
new file mode 100644
index 0000000..26ac861
--- /dev/null
+++ b/tests/AxisEndpoints.Example.Tests/OpenApiTests.cs
@@ -0,0 +1,47 @@
+using System.Net.Http.Json;
+using System.Text.Json.Nodes;
+using FluentAssertions;
+
+namespace AxisEndpoints.Example.Tests;
+
+public class OpenApiTests : IClassFixture
+{
+ private readonly HttpClient _client;
+
+ public OpenApiTests(ExampleWebApplicationFactory factory)
+ {
+ _client = factory.Client;
+ }
+
+ [Fact]
+ public async Task OpenApi_UsesBodyTypeForJsonResponseEndpoints()
+ {
+ var document = await _client.GetFromJsonAsync("/openapi/v1.json");
+
+ document.Should().NotBeNull();
+
+ var schema = document!["paths"]?["/health"]?["get"]?["responses"]?["200"]?["content"]?["application/json"]?["schema"];
+ schema.Should().NotBeNull();
+ schema!["$ref"]!.GetValue().Should().Be("#/components/schemas/HealthResponse");
+
+ document["components"]?["schemas"]?["ResponseOfHealthResponse"].Should().BeNull();
+ }
+
+ [Fact]
+ public async Task OpenApi_DoesNotRegisterEmptyResponseAsJsonBody()
+ {
+ var document = await _client.GetFromJsonAsync("/openapi/v1.json");
+
+ document.Should().NotBeNull();
+
+ var responses = document!["paths"]?["/api/users/{id}"]?["delete"]?["responses"]?.AsObject();
+ responses.Should().NotBeNull();
+
+ var successResponse = responses!["200"] ?? responses["204"];
+ successResponse.Should().NotBeNull();
+ successResponse!["content"]?["application/json"]?["schema"].Should().BeNull();
+
+ document["components"]?["schemas"]?["ResponseOfEmptyResponse"].Should().BeNull();
+ document["components"]?["schemas"]?["EmptyResponse"].Should().BeNull();
+ }
+}
diff --git a/tests/AxisEndpoints.Tests/Integration/EndpointIntegrationTests.cs b/tests/AxisEndpoints.Tests/Integration/EndpointIntegrationTests.cs
index 4a14ae6..899f29f 100644
--- a/tests/AxisEndpoints.Tests/Integration/EndpointIntegrationTests.cs
+++ b/tests/AxisEndpoints.Tests/Integration/EndpointIntegrationTests.cs
@@ -84,4 +84,22 @@ public async Task OpenApi_UsesResponseBodyTypeInsteadOfResponseWrapper()
document["components"]?["schemas"]?["ResponseOfHelloResponse"].Should().BeNull();
}
+
+ [Fact]
+ public async Task OpenApi_DoesNotRegisterEmptyResponseAsJsonBody()
+ {
+ var document = await _client.GetFromJsonAsync("/openapi/v1.json");
+
+ document.Should().NotBeNull();
+
+ var responses = document!["paths"]?["/items/{id}"]?["delete"]?["responses"]?.AsObject();
+ responses.Should().NotBeNull();
+
+ var successResponse = responses!["200"] ?? responses["204"];
+ successResponse.Should().NotBeNull();
+ successResponse!["content"]?["application/json"]?["schema"].Should().BeNull();
+
+ document["components"]?["schemas"]?["ResponseOfEmptyResponse"].Should().BeNull();
+ document["components"]?["schemas"]?["EmptyResponse"].Should().BeNull();
+ }
}
From ef44744e62a971047f6a88de13efa2f6ba2623e0 Mon Sep 17 00:00:00 2001
From: sheepla <62412884+sheepla@users.noreply.github.com>
Date: Sat, 2 May 2026 14:16:53 +0900
Subject: [PATCH 3/3] Fix OpenAPI success status metadata
---
src/AxisEndpoints/IEndpointConfiguration.cs | 14 +++++----
.../Internal/EndpointRegistry.cs | 30 +++++++++++++++----
.../OpenApiTests.cs | 11 ++++++-
.../Users/Create/CreateUserEndpoint.cs | 1 +
.../Users/Delete/DeleteUserEndpoint.cs | 2 ++
.../ImportFromCsv/ImportFromCsvEndpoint.cs | 4 ++-
.../Integration/EndpointIntegrationTests.cs | 19 +++++++++++-
.../Integration/TestEndpoints.cs | 22 +++++++++++++-
8 files changed, 87 insertions(+), 16 deletions(-)
diff --git a/src/AxisEndpoints/IEndpointConfiguration.cs b/src/AxisEndpoints/IEndpointConfiguration.cs
index e3a9edb..ef0bcb1 100644
--- a/src/AxisEndpoints/IEndpointConfiguration.cs
+++ b/src/AxisEndpoints/IEndpointConfiguration.cs
@@ -40,14 +40,16 @@ IEndpointConfiguration Group()
IEndpointConfiguration Summary(string summary);
IEndpointConfiguration Description(string description);
- // OpenAPI response metadata for IResult-returning endpoints.
- // When HandleAsync returns Response, the 200 schema is inferred automatically and
- // these methods are not needed. Use them when the return type is IResult to declare the
- // success and error response shapes explicitly.
+ // OpenAPI response metadata.
+ // When HandleAsync returns Response, a 200 success response is inferred automatically.
+ // Use ProducesSuccess to override that default when the endpoint returns a different success
+ // status code such as 201 or 204. When HandleAsync returns IResult, use ProducesSuccess and
+ // ProducesError to declare the response shapes explicitly.
///
- /// Declares a success response for OpenAPI. Use when HandleAsync returns
- /// and the 200 schema cannot be inferred automatically.
+ /// Declares a success response for OpenAPI. Use this to override the default 200 response
+ /// metadata for endpoints or to declare success metadata when
+ /// HandleAsync returns .
///
IEndpointConfiguration ProducesSuccess(HttpStatusCode statusCode = HttpStatusCode.OK);
diff --git a/src/AxisEndpoints/Internal/EndpointRegistry.cs b/src/AxisEndpoints/Internal/EndpointRegistry.cs
index 6aa5efa..463a0d6 100644
--- a/src/AxisEndpoints/Internal/EndpointRegistry.cs
+++ b/src/AxisEndpoints/Internal/EndpointRegistry.cs
@@ -475,22 +475,22 @@ EndpointConfiguration config
// inferred response type. When TResult is IResult, skip auto-registration — the
// caller is responsible for declaring response shapes via ProducesSuccess/ProducesError.
var openApiResponseType = GetOpenApiResponseType(config.ResponseType);
- var isNoBodyResponse =
- openApiResponseType == typeof(EmptyResponse)
- || typeof(EmptyResponse).IsAssignableFrom(openApiResponseType);
var isIResult =
openApiResponseType == typeof(IResult)
|| typeof(IResult).IsAssignableFrom(openApiResponseType);
+ var hasExplicitSuccessResponse = config.ExtraProducesEntries.Any(entry =>
+ entry.StatusCode is >= 200 and < 300
+ );
- if (!isIResult && !isNoBodyResponse)
+ if (!isIResult && !hasExplicitSuccessResponse)
{
- routeBuilder.Produces(200, openApiResponseType);
+ RegisterProduces(routeBuilder, 200, openApiResponseType);
}
}
foreach (var (statusCode, bodyType) in config.ExtraProducesEntries)
{
- routeBuilder.Produces(statusCode, bodyType);
+ RegisterProduces(routeBuilder, statusCode, bodyType);
}
}
@@ -507,6 +507,24 @@ private static Type GetOpenApiResponseType(Type responseType)
return responseType;
}
+ private static void RegisterProduces(
+ RouteHandlerBuilder routeBuilder,
+ int statusCode,
+ Type bodyType
+ )
+ {
+ var isNoBodyResponse =
+ bodyType == typeof(EmptyResponse) || typeof(EmptyResponse).IsAssignableFrom(bodyType);
+
+ if (isNoBodyResponse)
+ {
+ routeBuilder.Produces(statusCode);
+ return;
+ }
+
+ routeBuilder.Produces(statusCode, bodyType);
+ }
+
private static void ApplyGroupMetadata(
RouteGroupBuilder groupBuilder,
EndpointGroupConfiguration config
diff --git a/tests/AxisEndpoints.Example.Tests/OpenApiTests.cs b/tests/AxisEndpoints.Example.Tests/OpenApiTests.cs
index 26ac861..2b24c81 100644
--- a/tests/AxisEndpoints.Example.Tests/OpenApiTests.cs
+++ b/tests/AxisEndpoints.Example.Tests/OpenApiTests.cs
@@ -1,6 +1,7 @@
using System.Net.Http.Json;
using System.Text.Json.Nodes;
using FluentAssertions;
+using AxisEndpoints.Example.Features.Health;
namespace AxisEndpoints.Example.Tests;
@@ -25,6 +26,12 @@ public async Task OpenApi_UsesBodyTypeForJsonResponseEndpoints()
schema!["$ref"]!.GetValue().Should().Be("#/components/schemas/HealthResponse");
document["components"]?["schemas"]?["ResponseOfHealthResponse"].Should().BeNull();
+
+ var createdSchema =
+ document["paths"]?["/api/users"]?["post"]?["responses"]?["201"]?["content"]?["application/json"]?["schema"]
+ ?? document["paths"]?["/api/users/"]?["post"]?["responses"]?["201"]?["content"]?["application/json"]?["schema"];
+ createdSchema.Should().NotBeNull();
+ createdSchema!["$ref"]!.GetValue().Should().Be("#/components/schemas/UserResponse");
}
[Fact]
@@ -37,7 +44,9 @@ public async Task OpenApi_DoesNotRegisterEmptyResponseAsJsonBody()
var responses = document!["paths"]?["/api/users/{id}"]?["delete"]?["responses"]?.AsObject();
responses.Should().NotBeNull();
- var successResponse = responses!["200"] ?? responses["204"];
+ responses!.ContainsKey("200").Should().BeFalse();
+
+ var successResponse = responses["204"];
successResponse.Should().NotBeNull();
successResponse!["content"]?["application/json"]?["schema"].Should().BeNull();
diff --git a/tests/AxisEndpoints.Example/Features/Users/Create/CreateUserEndpoint.cs b/tests/AxisEndpoints.Example/Features/Users/Create/CreateUserEndpoint.cs
index b5c3f26..0303847 100644
--- a/tests/AxisEndpoints.Example/Features/Users/Create/CreateUserEndpoint.cs
+++ b/tests/AxisEndpoints.Example/Features/Users/Create/CreateUserEndpoint.cs
@@ -20,6 +20,7 @@ public void Configure(IEndpointConfiguration config)
.Group()
.Summary("Create a user")
.Description("Creates a new user. Returns 400 if validation fails.")
+ .ProducesSuccess(HttpStatusCode.Created)
.AddFilter();
}
diff --git a/tests/AxisEndpoints.Example/Features/Users/Delete/DeleteUserEndpoint.cs b/tests/AxisEndpoints.Example/Features/Users/Delete/DeleteUserEndpoint.cs
index cf64e1f..8f68ed2 100644
--- a/tests/AxisEndpoints.Example/Features/Users/Delete/DeleteUserEndpoint.cs
+++ b/tests/AxisEndpoints.Example/Features/Users/Delete/DeleteUserEndpoint.cs
@@ -1,4 +1,5 @@
using AxisEndpoints;
+using System.Net;
namespace AxisEndpoints.Example.Features.Users.Delete;
@@ -21,6 +22,7 @@ public void Configure(IEndpointConfiguration config)
"Permanently removes a user. "
+ "In a real application, restrict this to the Admin role via .RequireAuthorization(\"Admin\")."
)
+ .ProducesSuccess(HttpStatusCode.NoContent)
.AllowAnonymous();
}
diff --git a/tests/AxisEndpoints.Example/Features/Users/ImportFromCsv/ImportFromCsvEndpoint.cs b/tests/AxisEndpoints.Example/Features/Users/ImportFromCsv/ImportFromCsvEndpoint.cs
index 907d9be..163e30f 100644
--- a/tests/AxisEndpoints.Example/Features/Users/ImportFromCsv/ImportFromCsvEndpoint.cs
+++ b/tests/AxisEndpoints.Example/Features/Users/ImportFromCsv/ImportFromCsvEndpoint.cs
@@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
+using System.Net;
using AxisEndpoints.Extensions.CsvHelper;
using CsvHelper.Configuration.Attributes;
@@ -50,7 +51,8 @@ public void Configure(IEndpointConfiguration config)
.Description(
"Accepts a CSV file with columns: name, email, role. "
+ "Validates each row and returns a ValidationProblem on failure."
- );
+ )
+ .ProducesSuccess(HttpStatusCode.NoContent);
}
public Task> HandleAsync(
diff --git a/tests/AxisEndpoints.Tests/Integration/EndpointIntegrationTests.cs b/tests/AxisEndpoints.Tests/Integration/EndpointIntegrationTests.cs
index 899f29f..e1ffc77 100644
--- a/tests/AxisEndpoints.Tests/Integration/EndpointIntegrationTests.cs
+++ b/tests/AxisEndpoints.Tests/Integration/EndpointIntegrationTests.cs
@@ -71,6 +71,17 @@ public async Task GetGrouped_Returns200WithGroupPrefix()
body!.Message.Should().Be("Grouped!");
}
+ [Fact]
+ public async Task PostCreatedHello_Returns201WithMessage()
+ {
+ var response = await _client.PostAsync("/hello-created", content: null);
+
+ response.StatusCode.Should().Be(HttpStatusCode.Created);
+ var body = await response.Content.ReadFromJsonAsync();
+ body.Should().NotBeNull();
+ body!.Message.Should().Be("Created!");
+ }
+
[Fact]
public async Task OpenApi_UsesResponseBodyTypeInsteadOfResponseWrapper()
{
@@ -83,6 +94,10 @@ public async Task OpenApi_UsesResponseBodyTypeInsteadOfResponseWrapper()
schema!["$ref"]!.GetValue().Should().Be("#/components/schemas/HelloResponse");
document["components"]?["schemas"]?["ResponseOfHelloResponse"].Should().BeNull();
+
+ var createdSchema = document["paths"]?["/hello-created"]?["post"]?["responses"]?["201"]?["content"]?["application/json"]?["schema"];
+ createdSchema.Should().NotBeNull();
+ createdSchema!["$ref"]!.GetValue().Should().Be("#/components/schemas/HelloResponse");
}
[Fact]
@@ -95,7 +110,9 @@ public async Task OpenApi_DoesNotRegisterEmptyResponseAsJsonBody()
var responses = document!["paths"]?["/items/{id}"]?["delete"]?["responses"]?.AsObject();
responses.Should().NotBeNull();
- var successResponse = responses!["200"] ?? responses["204"];
+ responses!.ContainsKey("200").Should().BeFalse();
+
+ var successResponse = responses["204"];
successResponse.Should().NotBeNull();
successResponse!["content"]?["application/json"]?["schema"].Should().BeNull();
diff --git a/tests/AxisEndpoints.Tests/Integration/TestEndpoints.cs b/tests/AxisEndpoints.Tests/Integration/TestEndpoints.cs
index 56e3245..fc4894f 100644
--- a/tests/AxisEndpoints.Tests/Integration/TestEndpoints.cs
+++ b/tests/AxisEndpoints.Tests/Integration/TestEndpoints.cs
@@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
+using System.Net;
using Microsoft.AspNetCore.Mvc;
namespace AxisEndpoints.Tests.Integration;
@@ -58,7 +59,7 @@ public class DeleteItemEndpoint : IEndpoint(HttpStatusCode.NoContent);
}
public Task> HandleAsync(DeleteItemRequest request, CancellationToken cancel)
@@ -69,6 +70,25 @@ public Task> HandleAsync(DeleteItemRequest request, Canc
public record DeleteItemRequest([property: FromRoute] int Id);
+public class CreatedHelloEndpoint : IEndpoint>
+{
+ public void Configure(IEndpointConfiguration config)
+ {
+ config.Post("/hello-created").ProducesSuccess(HttpStatusCode.Created);
+ }
+
+ public Task> HandleAsync(CancellationToken cancel)
+ {
+ return Task.FromResult(
+ new Response
+ {
+ StatusCode = HttpStatusCode.Created,
+ Body = new HelloResponse("Created!")
+ }
+ );
+ }
+}
+
public class ApiGroup : IEndpointGroup
{
public void Configure(IEndpointGroupConfiguration config)