diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index de3b40bc0c..b665b78a1a 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -31,11 +31,11 @@
"rollForward": false
},
"microsoft.openapi.kiota": {
- "version": "1.27.0",
+ "version": "1.28.0",
"commands": [
"kiota"
],
"rollForward": false
}
}
-}
+}
\ No newline at end of file
diff --git a/package-versions.props b/package-versions.props
index 0f2c3f6e7e..4344ce16e2 100644
--- a/package-versions.props
+++ b/package-versions.props
@@ -20,6 +20,7 @@
1.*
9.0.*
9.0.*
+ 0.9.*
14.4.*
13.0.*
4.1.*
diff --git a/src/Examples/GettingStarted/GettingStarted.http b/src/Examples/GettingStarted/GettingStarted.http
new file mode 100644
index 0000000000..271f493a15
--- /dev/null
+++ b/src/Examples/GettingStarted/GettingStarted.http
@@ -0,0 +1,85 @@
+@hostAddress = http://localhost:14141
+
+### Get all books with their authors.
+
+GET {{hostAddress}}/api/books?include=author
+
+### Get the first two books.
+
+GET {{hostAddress}}/api/books?page[size]=2
+
+### Filter books whose title contains whitespace, sort descending by publication year.
+
+GET {{hostAddress}}/api/books?filter=contains(title,'%20')&sort=-publishYear
+
+### Get only the titles of all books.
+
+GET {{hostAddress}}/api/books?fields[books]=title
+
+### Get the names of all people.
+
+GET {{hostAddress}}/api/people?fields[people]=name
+
+### Create a new person.
+
+POST {{hostAddress}}/api/people
+Content-Type: application/vnd.api+json
+
+{
+ "data": {
+ "type": "people",
+ "attributes": {
+ "name": "Alice"
+ }
+ }
+}
+
+### Create a new book, authored by the created person.
+
+POST {{hostAddress}}/api/books
+Content-Type: application/vnd.api+json
+
+{
+ "data": {
+ "type": "books",
+ "attributes": {
+ "title": "Getting started with JSON:API",
+ "publishYear": 2000
+ },
+ "relationships": {
+ "author": {
+ "data": {
+ "type": "people",
+ "id": "4"
+ }
+ }
+ }
+ }
+}
+
+### Change the publication year and author of the book with ID 1.
+
+PATCH {{hostAddress}}/api/books/1
+Content-Type: application/vnd.api+json
+
+{
+ "data": {
+ "type": "books",
+ "id": "1",
+ "attributes": {
+ "publishYear": 1820
+ },
+ "relationships": {
+ "author": {
+ "data": {
+ "type": "people",
+ "id": "4"
+ }
+ }
+ }
+ }
+}
+
+### Delete the book with ID 1.
+
+DELETE {{hostAddress}}/api/books/1
diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/NonJsonApiController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/NonJsonApiController.cs
index aa51110869..8d072b1ec1 100644
--- a/src/Examples/JsonApiDotNetCoreExample/Controllers/NonJsonApiController.cs
+++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/NonJsonApiController.cs
@@ -3,9 +3,13 @@
namespace JsonApiDotNetCoreExample.Controllers;
[Route("[controller]")]
+[Tags("nonJsonApi")]
public sealed class NonJsonApiController : ControllerBase
{
- [HttpGet]
+ [HttpGet(Name = "welcomeGet")]
+ [HttpHead(Name = "welcomeHead")]
+ [EndpointDescription("Returns a single-element JSON array.")]
+ [ProducesResponseType>(StatusCodes.Status200OK, "application/json")]
public IActionResult Get()
{
string[] result = ["Welcome!"];
@@ -14,12 +18,15 @@ public IActionResult Get()
}
[HttpPost]
- public async Task PostAsync()
+ [EndpointDescription("Returns a greeting text, based on your name.")]
+ [Consumes("application/json")]
+ [ProducesResponseType(StatusCodes.Status200OK, "text/plain")]
+ [ProducesResponseType(StatusCodes.Status400BadRequest, "text/plain")]
+ public async Task PostAsync([FromBody] string? name)
{
- using var reader = new StreamReader(Request.Body, leaveOpen: true);
- string name = await reader.ReadToEndAsync();
+ await Task.Yield();
- if (string.IsNullOrEmpty(name))
+ if (string.IsNullOrWhiteSpace(name))
{
return BadRequest("Please send your name.");
}
@@ -29,14 +36,18 @@ public async Task PostAsync()
}
[HttpPut]
- public IActionResult Put([FromBody] string name)
+ [EndpointDescription("Returns another greeting text.")]
+ [ProducesResponseType(StatusCodes.Status200OK, "text/plain")]
+ public IActionResult Put([FromQuery] string? name)
{
string result = $"Hi, {name}";
return Ok(result);
}
[HttpPatch]
- public IActionResult Patch(string name)
+ [EndpointDescription("Wishes you a good day.")]
+ [ProducesResponseType(StatusCodes.Status200OK, "text/plain")]
+ public IActionResult Patch([FromHeader] string? name)
{
string result = $"Good day, {name}";
return Ok(result);
diff --git a/src/Examples/JsonApiDotNetCoreExample/GeneratedSwagger/JsonApiDotNetCoreExample.json b/src/Examples/JsonApiDotNetCoreExample/GeneratedSwagger/JsonApiDotNetCoreExample.json
index 4863000598..15fe87de78 100644
--- a/src/Examples/JsonApiDotNetCoreExample/GeneratedSwagger/JsonApiDotNetCoreExample.json
+++ b/src/Examples/JsonApiDotNetCoreExample/GeneratedSwagger/JsonApiDotNetCoreExample.json
@@ -10,6 +10,143 @@
}
],
"paths": {
+ "/NonJsonApi": {
+ "get": {
+ "tags": [
+ "nonJsonApi"
+ ],
+ "description": "Returns a single-element JSON array.",
+ "operationId": "welcomeGet",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "head": {
+ "tags": [
+ "nonJsonApi"
+ ],
+ "description": "Returns a single-element JSON array.",
+ "operationId": "welcomeHead",
+ "responses": {
+ "200": {
+ "description": "OK"
+ }
+ }
+ },
+ "post": {
+ "tags": [
+ "nonJsonApi"
+ ],
+ "description": "Returns a greeting text, based on your name.",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "text/plain": {
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "content": {
+ "text/plain": {
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ },
+ "put": {
+ "tags": [
+ "nonJsonApi"
+ ],
+ "description": "Returns another greeting text.",
+ "parameters": [
+ {
+ "name": "name",
+ "in": "query",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "text/plain": {
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ },
+ "patch": {
+ "tags": [
+ "nonJsonApi"
+ ],
+ "description": "Wishes you a good day.",
+ "parameters": [
+ {
+ "name": "name",
+ "in": "header",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "text/plain": {
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ },
+ "delete": {
+ "tags": [
+ "nonJsonApi"
+ ],
+ "responses": {
+ "200": {
+ "description": "OK"
+ }
+ }
+ }
+ },
"/api/operations": {
"post": {
"tags": [
diff --git a/src/Examples/JsonApiDotNetCoreExample/JsonApiDotNetCoreExample.http b/src/Examples/JsonApiDotNetCoreExample/JsonApiDotNetCoreExample.http
new file mode 100644
index 0000000000..6ea166d4e3
--- /dev/null
+++ b/src/Examples/JsonApiDotNetCoreExample/JsonApiDotNetCoreExample.http
@@ -0,0 +1,47 @@
+@hostAddress = https://localhost:44340
+
+### Gets all high-priority todo-items, including their owner, assignee and tags.
+
+GET {{hostAddress}}/api/todoItems?include=owner,assignee,tags&filter=equals(priority,'High')
+
+### Creates a todo-item, linking it to an existing owner, assignee and tags.
+
+POST {{hostAddress}}/api/todoItems
+Content-Type: application/vnd.api+json
+
+{
+ "data": {
+ "type": "todoItems",
+ "attributes": {
+ "description": "Create release",
+ "priority": "High",
+ "durationInHours": 1
+ },
+ "relationships": {
+ "owner": {
+ "data": {
+ "type": "people",
+ "id": "1"
+ }
+ },
+ "assignee": {
+ "data": {
+ "type": "people",
+ "id": "1"
+ }
+ },
+ "tags": {
+ "data": [
+ {
+ "type": "tags",
+ "id": "1"
+ },
+ {
+ "type": "tags",
+ "id": "2"
+ }
+ ]
+ }
+ }
+ }
+}
diff --git a/src/Examples/OpenApiKiotaClientExample/GeneratedCode/ExampleApiClient.cs b/src/Examples/OpenApiKiotaClientExample/GeneratedCode/ExampleApiClient.cs
index 15ea897045..4a9cbd037e 100644
--- a/src/Examples/OpenApiKiotaClientExample/GeneratedCode/ExampleApiClient.cs
+++ b/src/Examples/OpenApiKiotaClientExample/GeneratedCode/ExampleApiClient.cs
@@ -10,6 +10,7 @@
using Microsoft.Kiota.Serialization.Multipart;
using Microsoft.Kiota.Serialization.Text;
using OpenApiKiotaClientExample.GeneratedCode.Api;
+using OpenApiKiotaClientExample.GeneratedCode.NonJsonApi;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
@@ -28,6 +29,12 @@ public partial class ExampleApiClient : BaseRequestBuilder
get => new global::OpenApiKiotaClientExample.GeneratedCode.Api.ApiRequestBuilder(PathParameters, RequestAdapter);
}
+ /// The NonJsonApi property
+ public global::OpenApiKiotaClientExample.GeneratedCode.NonJsonApi.NonJsonApiRequestBuilder NonJsonApi
+ {
+ get => new global::OpenApiKiotaClientExample.GeneratedCode.NonJsonApi.NonJsonApiRequestBuilder(PathParameters, RequestAdapter);
+ }
+
///
/// Instantiates a new and sets the default values.
///
diff --git a/src/Examples/OpenApiKiotaClientExample/GeneratedCode/NonJsonApi/NonJsonApiRequestBuilder.cs b/src/Examples/OpenApiKiotaClientExample/GeneratedCode/NonJsonApi/NonJsonApiRequestBuilder.cs
new file mode 100644
index 0000000000..8aa2eb0d39
--- /dev/null
+++ b/src/Examples/OpenApiKiotaClientExample/GeneratedCode/NonJsonApi/NonJsonApiRequestBuilder.cs
@@ -0,0 +1,206 @@
+//
+#nullable enable
+#pragma warning disable CS8625
+#pragma warning disable CS0618
+using Microsoft.Kiota.Abstractions.Extensions;
+using Microsoft.Kiota.Abstractions.Serialization;
+using Microsoft.Kiota.Abstractions;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading.Tasks;
+using System.Threading;
+using System;
+namespace OpenApiKiotaClientExample.GeneratedCode.NonJsonApi
+{
+ ///
+ /// Builds and executes requests for operations under \NonJsonApi
+ ///
+ [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
+ public partial class NonJsonApiRequestBuilder : BaseRequestBuilder
+ {
+ ///
+ /// Instantiates a new and sets the default values.
+ ///
+ /// Path parameters for the request
+ /// The request adapter to use to execute the requests.
+ public NonJsonApiRequestBuilder(Dictionary pathParameters, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/NonJsonApi{?name*}", pathParameters)
+ {
+ }
+
+ ///
+ /// Instantiates a new and sets the default values.
+ ///
+ /// The raw URL to use for the request builder.
+ /// The request adapter to use to execute the requests.
+ public NonJsonApiRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/NonJsonApi{?name*}", rawUrl)
+ {
+ }
+ /// A
+ /// Cancellation token to use when cancelling requests
+ /// Configuration for the request such as headers, query parameters, and middleware options.
+ public async Task DeleteAsync(Action>? requestConfiguration = default, CancellationToken cancellationToken = default)
+ {
+ var requestInfo = ToDeleteRequestInformation(requestConfiguration);
+ return await RequestAdapter.SendPrimitiveAsync(requestInfo, default, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ /// Returns a single-element JSON array.
+ ///
+ /// A List<string>
+ /// Cancellation token to use when cancelling requests
+ /// Configuration for the request such as headers, query parameters, and middleware options.
+ public async Task?> GetAsync(Action>? requestConfiguration = default, CancellationToken cancellationToken = default)
+ {
+ var requestInfo = ToGetRequestInformation(requestConfiguration);
+ var collectionResult = await RequestAdapter.SendPrimitiveCollectionAsync(requestInfo, default, cancellationToken).ConfigureAwait(false);
+ return collectionResult?.AsList();
+ }
+
+ ///
+ /// Returns a single-element JSON array.
+ ///
+ /// A
+ /// Cancellation token to use when cancelling requests
+ /// Configuration for the request such as headers, query parameters, and middleware options.
+ public async Task HeadAsync(Action>? requestConfiguration = default, CancellationToken cancellationToken = default)
+ {
+ var requestInfo = ToHeadRequestInformation(requestConfiguration);
+ return await RequestAdapter.SendPrimitiveAsync(requestInfo, default, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ /// Wishes you a good day.
+ ///
+ /// A
+ /// Cancellation token to use when cancelling requests
+ /// Configuration for the request such as headers, query parameters, and middleware options.
+ public async Task PatchAsync(Action>? requestConfiguration = default, CancellationToken cancellationToken = default)
+ {
+ var requestInfo = ToPatchRequestInformation(requestConfiguration);
+ return await RequestAdapter.SendPrimitiveAsync(requestInfo, default, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ /// Returns a greeting text, based on your name.
+ ///
+ /// A
+ /// The request body
+ /// Cancellation token to use when cancelling requests
+ /// Configuration for the request such as headers, query parameters, and middleware options.
+ public async Task PostAsync(string body, Action>? requestConfiguration = default, CancellationToken cancellationToken = default)
+ {
+ if(string.IsNullOrEmpty(body)) throw new ArgumentNullException(nameof(body));
+ var requestInfo = ToPostRequestInformation(body, requestConfiguration);
+ return await RequestAdapter.SendPrimitiveAsync(requestInfo, default, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ /// Returns another greeting text.
+ ///
+ /// A
+ /// Cancellation token to use when cancelling requests
+ /// Configuration for the request such as headers, query parameters, and middleware options.
+ public async Task PutAsync(Action>? requestConfiguration = default, CancellationToken cancellationToken = default)
+ {
+ var requestInfo = ToPutRequestInformation(requestConfiguration);
+ return await RequestAdapter.SendPrimitiveAsync(requestInfo, default, cancellationToken).ConfigureAwait(false);
+ }
+ /// A
+ /// Configuration for the request such as headers, query parameters, and middleware options.
+ public RequestInformation ToDeleteRequestInformation(Action>? requestConfiguration = default)
+ {
+ var requestInfo = new RequestInformation(Method.DELETE, UrlTemplate, PathParameters);
+ requestInfo.Configure(requestConfiguration);
+ return requestInfo;
+ }
+
+ ///
+ /// Returns a single-element JSON array.
+ ///
+ /// A
+ /// Configuration for the request such as headers, query parameters, and middleware options.
+ public RequestInformation ToGetRequestInformation(Action>? requestConfiguration = default)
+ {
+ var requestInfo = new RequestInformation(Method.GET, UrlTemplate, PathParameters);
+ requestInfo.Configure(requestConfiguration);
+ requestInfo.Headers.TryAdd("Accept", "application/json");
+ return requestInfo;
+ }
+
+ ///
+ /// Returns a single-element JSON array.
+ ///
+ /// A
+ /// Configuration for the request such as headers, query parameters, and middleware options.
+ public RequestInformation ToHeadRequestInformation(Action>? requestConfiguration = default)
+ {
+ var requestInfo = new RequestInformation(Method.HEAD, UrlTemplate, PathParameters);
+ requestInfo.Configure(requestConfiguration);
+ return requestInfo;
+ }
+
+ ///
+ /// Wishes you a good day.
+ ///
+ /// A
+ /// Configuration for the request such as headers, query parameters, and middleware options.
+ public RequestInformation ToPatchRequestInformation(Action>? requestConfiguration = default)
+ {
+ var requestInfo = new RequestInformation(Method.PATCH, UrlTemplate, PathParameters);
+ requestInfo.Configure(requestConfiguration);
+ requestInfo.Headers.TryAdd("Accept", "text/plain;q=0.9");
+ return requestInfo;
+ }
+
+ ///
+ /// Returns a greeting text, based on your name.
+ ///
+ /// A
+ /// The request body
+ /// Configuration for the request such as headers, query parameters, and middleware options.
+ public RequestInformation ToPostRequestInformation(string body, Action>? requestConfiguration = default)
+ {
+ if(string.IsNullOrEmpty(body)) throw new ArgumentNullException(nameof(body));
+ var requestInfo = new RequestInformation(Method.POST, UrlTemplate, PathParameters);
+ requestInfo.Configure(requestConfiguration);
+ requestInfo.Headers.TryAdd("Accept", "text/plain;q=0.9");
+ requestInfo.SetContentFromScalar(RequestAdapter, "application/json", body);
+ return requestInfo;
+ }
+
+ ///
+ /// Returns another greeting text.
+ ///
+ /// A
+ /// Configuration for the request such as headers, query parameters, and middleware options.
+ public RequestInformation ToPutRequestInformation(Action>? requestConfiguration = default)
+ {
+ var requestInfo = new RequestInformation(Method.PUT, UrlTemplate, PathParameters);
+ requestInfo.Configure(requestConfiguration);
+ requestInfo.Headers.TryAdd("Accept", "text/plain;q=0.9");
+ return requestInfo;
+ }
+
+ ///
+ /// Returns a request builder with the provided arbitrary URL. Using this method means any other path or query parameters are ignored.
+ ///
+ /// A
+ /// The raw URL to use for the request builder.
+ public global::OpenApiKiotaClientExample.GeneratedCode.NonJsonApi.NonJsonApiRequestBuilder WithUrl(string rawUrl)
+ {
+ return new global::OpenApiKiotaClientExample.GeneratedCode.NonJsonApi.NonJsonApiRequestBuilder(rawUrl, RequestAdapter);
+ }
+
+ ///
+ /// Returns another greeting text.
+ ///
+ [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
+ public partial class NonJsonApiRequestBuilderPutQueryParameters
+ {
+ [QueryParameter("name")]
+ public string? Name { get; set; }
+ }
+ }
+}
+#pragma warning restore CS0618
diff --git a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/ActionDescriptorExtensions.cs b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/ActionDescriptorExtensions.cs
index 1892aca576..6993d10cdf 100644
--- a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/ActionDescriptorExtensions.cs
+++ b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/ActionDescriptorExtensions.cs
@@ -1,7 +1,6 @@
using System.Reflection;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Controllers;
-using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace JsonApiDotNetCore.OpenApi.Swashbuckle;
@@ -12,22 +11,32 @@ public static MethodInfo GetActionMethod(this ActionDescriptor descriptor)
{
ArgumentNullException.ThrowIfNull(descriptor);
- return ((ControllerActionDescriptor)descriptor).MethodInfo;
- }
+ if (descriptor is ControllerActionDescriptor controllerActionDescriptor)
+ {
+ return controllerActionDescriptor.MethodInfo;
+ }
- public static TFilterMetaData? GetFilterMetadata(this ActionDescriptor descriptor)
- where TFilterMetaData : IFilterMetadata
- {
- ArgumentNullException.ThrowIfNull(descriptor);
+ MethodInfo? methodInfo = descriptor.EndpointMetadata.OfType().FirstOrDefault();
+ ConsistencyGuard.ThrowIf(methodInfo == null);
- return descriptor.FilterDescriptors.Select(filterDescriptor => filterDescriptor.Filter).OfType().FirstOrDefault();
+ return methodInfo;
}
public static ControllerParameterDescriptor? GetBodyParameterDescriptor(this ActionDescriptor descriptor)
{
ArgumentNullException.ThrowIfNull(descriptor);
- return (ControllerParameterDescriptor?)descriptor.Parameters.FirstOrDefault(parameterDescriptor =>
+ ParameterDescriptor? parameterDescriptor = descriptor.Parameters.FirstOrDefault(parameterDescriptor =>
parameterDescriptor.BindingInfo?.BindingSource == BindingSource.Body);
+
+ if (parameterDescriptor != null)
+ {
+ var controllerParameterDescriptor = parameterDescriptor as ControllerParameterDescriptor;
+ ConsistencyGuard.ThrowIf(controllerParameterDescriptor == null);
+
+ return controllerParameterDescriptor;
+ }
+
+ return null;
}
}
diff --git a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/ConfigureMvcOptions.cs b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/ConfigureMvcOptions.cs
index e1e04a2464..73e612fa22 100644
--- a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/ConfigureMvcOptions.cs
+++ b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/ConfigureMvcOptions.cs
@@ -1,7 +1,6 @@
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Middleware;
using Microsoft.AspNetCore.Mvc;
-using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
namespace JsonApiDotNetCore.OpenApi.Swashbuckle;
@@ -9,20 +8,17 @@ namespace JsonApiDotNetCore.OpenApi.Swashbuckle;
internal sealed class ConfigureMvcOptions : IConfigureOptions
{
private readonly IJsonApiRoutingConvention _jsonApiRoutingConvention;
- private readonly OpenApiEndpointConvention _openApiEndpointConvention;
private readonly JsonApiRequestFormatMetadataProvider _jsonApiRequestFormatMetadataProvider;
private readonly IJsonApiOptions _jsonApiOptions;
- public ConfigureMvcOptions(IJsonApiRoutingConvention jsonApiRoutingConvention, OpenApiEndpointConvention openApiEndpointConvention,
- JsonApiRequestFormatMetadataProvider jsonApiRequestFormatMetadataProvider, IJsonApiOptions jsonApiOptions)
+ public ConfigureMvcOptions(IJsonApiRoutingConvention jsonApiRoutingConvention, JsonApiRequestFormatMetadataProvider jsonApiRequestFormatMetadataProvider,
+ IJsonApiOptions jsonApiOptions)
{
ArgumentNullException.ThrowIfNull(jsonApiRoutingConvention);
- ArgumentNullException.ThrowIfNull(openApiEndpointConvention);
ArgumentNullException.ThrowIfNull(jsonApiRequestFormatMetadataProvider);
ArgumentNullException.ThrowIfNull(jsonApiOptions);
_jsonApiRoutingConvention = jsonApiRoutingConvention;
- _openApiEndpointConvention = openApiEndpointConvention;
_jsonApiRequestFormatMetadataProvider = jsonApiRequestFormatMetadataProvider;
_jsonApiOptions = jsonApiOptions;
}
@@ -34,7 +30,6 @@ public void Configure(MvcOptions options)
AddSwashbuckleCliCompatibility(options);
options.InputFormatters.Add(_jsonApiRequestFormatMetadataProvider);
- options.Conventions.Add(_openApiEndpointConvention);
((JsonApiOptions)_jsonApiOptions).IncludeExtensions(OpenApiMediaTypeExtension.OpenApi, OpenApiMediaTypeExtension.RelaxedOpenApi);
}
diff --git a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/ConfigureSwaggerGenOptions.cs b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/ConfigureSwaggerGenOptions.cs
index f3fb5198ca..efef31c7e4 100644
--- a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/ConfigureSwaggerGenOptions.cs
+++ b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/ConfigureSwaggerGenOptions.cs
@@ -1,6 +1,6 @@
-using System.Reflection;
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Middleware;
+using JsonApiDotNetCore.OpenApi.Swashbuckle.JsonApiMetadata.ActionMethods;
using JsonApiDotNetCore.OpenApi.Swashbuckle.JsonApiObjects.AtomicOperations;
using JsonApiDotNetCore.OpenApi.Swashbuckle.JsonApiObjects.ResourceObjects;
using JsonApiDotNetCore.OpenApi.Swashbuckle.SwaggerComponents;
@@ -33,6 +33,8 @@ internal sealed class ConfigureSwaggerGenOptions : IConfigureOptions)
];
+ private static readonly Func> DefaultTagsSelector = new SwaggerGeneratorOptions().TagsSelector;
+
private readonly OpenApiOperationIdSelector _operationIdSelector;
private readonly JsonApiSchemaIdSelector _schemaIdSelector;
private readonly IControllerResourceMapping _controllerResourceMapping;
@@ -142,11 +144,27 @@ private static void IncludeDerivedTypes(ResourceType baseType, List clrTyp
}
}
- private static List GetOpenApiOperationTags(ApiDescription description, IControllerResourceMapping controllerResourceMapping)
+ private static IList GetOpenApiOperationTags(ApiDescription description, IControllerResourceMapping controllerResourceMapping)
{
- MethodInfo actionMethod = description.ActionDescriptor.GetActionMethod();
- ResourceType? resourceType = controllerResourceMapping.GetResourceTypeForController(actionMethod.ReflectedType);
+ var actionMethod = OpenApiActionMethod.Create(description.ActionDescriptor);
+
+ switch (actionMethod)
+ {
+ case AtomicOperationsActionMethod:
+ {
+ return ["operations"];
+ }
+ case JsonApiActionMethod jsonApiActionMethod:
+ {
+ ResourceType? resourceType = controllerResourceMapping.GetResourceTypeForController(jsonApiActionMethod.ControllerType);
+ ConsistencyGuard.ThrowIf(resourceType == null);
- return resourceType == null ? ["operations"] : [resourceType.PublicName];
+ return [resourceType.PublicName];
+ }
+ default:
+ {
+ return DefaultTagsSelector(description);
+ }
+ }
}
}
diff --git a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiActionDescriptorCollectionProvider.cs b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiActionDescriptorCollectionProvider.cs
index 6d63a540cd..72d0dcfab0 100644
--- a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiActionDescriptorCollectionProvider.cs
+++ b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiActionDescriptorCollectionProvider.cs
@@ -1,194 +1,325 @@
+using System.Net;
using System.Reflection;
+using JsonApiDotNetCore.Configuration;
+using JsonApiDotNetCore.Controllers;
using JsonApiDotNetCore.Errors;
using JsonApiDotNetCore.Middleware;
using JsonApiDotNetCore.OpenApi.Swashbuckle.JsonApiMetadata;
+using JsonApiDotNetCore.OpenApi.Swashbuckle.JsonApiMetadata.ActionMethods;
+using JsonApiDotNetCore.OpenApi.Swashbuckle.JsonApiMetadata.Documents;
using JsonApiDotNetCore.OpenApi.Swashbuckle.JsonApiObjects.Documents;
+using JsonApiDotNetCore.Resources.Annotations;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.Routing;
-using Microsoft.Net.Http.Headers;
+using Microsoft.AspNetCore.Routing;
+using Microsoft.Extensions.Logging;
namespace JsonApiDotNetCore.OpenApi.Swashbuckle;
///
-/// Adds JsonApiDotNetCore metadata to s if available. This translates to updating response types in
-/// and performing an expansion for secondary and relationship endpoints. For example:
-/// s and performs endpoint expansion for secondary and relationship
+/// endpoints. For example: /article/{id}/author, /article/{id}/revisions, etc.
/// ]]>
///
-internal sealed class JsonApiActionDescriptorCollectionProvider : IActionDescriptorCollectionProvider
+internal sealed partial class JsonApiActionDescriptorCollectionProvider : IActionDescriptorCollectionProvider
{
- private static readonly string DefaultMediaType = JsonApiMediaType.Default.ToString();
+ private const int FilterScope = 10;
+ private static readonly Type ErrorDocumentType = typeof(ErrorResponseDocument);
private readonly IActionDescriptorCollectionProvider _defaultProvider;
+ private readonly IControllerResourceMapping _controllerResourceMapping;
private readonly JsonApiEndpointMetadataProvider _jsonApiEndpointMetadataProvider;
+ private readonly IJsonApiOptions _options;
+ private readonly ILogger _logger;
public ActionDescriptorCollection ActionDescriptors => GetActionDescriptors();
- public JsonApiActionDescriptorCollectionProvider(IActionDescriptorCollectionProvider defaultProvider,
- JsonApiEndpointMetadataProvider jsonApiEndpointMetadataProvider)
+ public JsonApiActionDescriptorCollectionProvider(IActionDescriptorCollectionProvider defaultProvider, IControllerResourceMapping controllerResourceMapping,
+ JsonApiEndpointMetadataProvider jsonApiEndpointMetadataProvider, IJsonApiOptions options, ILogger logger)
{
ArgumentNullException.ThrowIfNull(defaultProvider);
+ ArgumentNullException.ThrowIfNull(controllerResourceMapping);
ArgumentNullException.ThrowIfNull(jsonApiEndpointMetadataProvider);
+ ArgumentNullException.ThrowIfNull(options);
+ ArgumentNullException.ThrowIfNull(logger);
_defaultProvider = defaultProvider;
+ _controllerResourceMapping = controllerResourceMapping;
_jsonApiEndpointMetadataProvider = jsonApiEndpointMetadataProvider;
+ _options = options;
+ _logger = logger;
}
private ActionDescriptorCollection GetActionDescriptors()
{
- List newDescriptors = _defaultProvider.ActionDescriptors.Items.ToList();
- ActionDescriptor[] endpoints = newDescriptors.Where(IsVisibleJsonApiEndpoint).ToArray();
+ List descriptors = [];
- foreach (ActionDescriptor endpoint in endpoints)
+ foreach (ActionDescriptor descriptor in _defaultProvider.ActionDescriptors.Items)
{
- MethodInfo actionMethod = endpoint.GetActionMethod();
- JsonApiEndpointMetadataContainer endpointMetadataContainer = _jsonApiEndpointMetadataProvider.Get(actionMethod);
+ if (!descriptor.EndpointMetadata.OfType().SelectMany(metadata => metadata.HttpMethods).Any())
+ {
+ // Technically incorrect: when no verbs, the endpoint is exposed on all verbs. But Swashbuckle hides it anyway.
+ continue;
+ }
- List replacementDescriptorsForEndpoint =
- [
- .. AddJsonApiMetadataToAction(endpoint, endpointMetadataContainer.RequestMetadata),
- .. AddJsonApiMetadataToAction(endpoint, endpointMetadataContainer.ResponseMetadata)
- ];
+ var actionMethod = OpenApiActionMethod.Create(descriptor);
+
+ if (actionMethod is CustomJsonApiActionMethod)
+ {
+ // A non-standard action method in a JSON:API controller. Not yet implemented, so skip to prevent downstream crashes.
+ string httpMethods = string.Join(", ", descriptor.EndpointMetadata.OfType().SelectMany(metadata => metadata.HttpMethods));
+ LogSuppressedActionMethod(httpMethods, descriptor.DisplayName);
+
+ continue;
+ }
- if (replacementDescriptorsForEndpoint.Count > 0)
+ if (actionMethod is BuiltinJsonApiActionMethod builtinActionMethod)
{
- newDescriptors.InsertRange(newDescriptors.IndexOf(endpoint), replacementDescriptorsForEndpoint);
- newDescriptors.Remove(endpoint);
+ if (!IsVisibleEndpoint(descriptor))
+ {
+ continue;
+ }
+
+ ResourceType? resourceType = _controllerResourceMapping.GetResourceTypeForController(builtinActionMethod.ControllerType);
+
+ if (builtinActionMethod is JsonApiActionMethod jsonApiActionMethod)
+ {
+ ConsistencyGuard.ThrowIf(resourceType == null);
+
+ if (ShouldSuppressEndpoint(jsonApiActionMethod.Endpoint, resourceType))
+ {
+ continue;
+ }
+ }
+
+ ActionDescriptor[] replacementDescriptors = SetEndpointMetadata(descriptor, builtinActionMethod, resourceType);
+ descriptors.AddRange(replacementDescriptors);
+
+ continue;
}
+
+ descriptors.Add(descriptor);
}
int descriptorVersion = _defaultProvider.ActionDescriptors.Version;
- return new ActionDescriptorCollection(newDescriptors.AsReadOnly(), descriptorVersion);
+ return new ActionDescriptorCollection(descriptors.AsReadOnly(), descriptorVersion);
}
- internal static bool IsVisibleJsonApiEndpoint(ActionDescriptor descriptor)
+ internal static bool IsVisibleEndpoint(ActionDescriptor descriptor)
{
// Only if in a convention ApiExplorer.IsVisible was set to false, the ApiDescriptionActionData will not be present.
- return descriptor is ControllerActionDescriptor controllerAction && controllerAction.Properties.ContainsKey(typeof(ApiDescriptionActionData));
+ return descriptor is ControllerActionDescriptor controllerDescriptor && controllerDescriptor.Properties.ContainsKey(typeof(ApiDescriptionActionData));
}
- private static List AddJsonApiMetadataToAction(ActionDescriptor endpoint, IJsonApiEndpointMetadata? jsonApiEndpointMetadata)
+ private static bool ShouldSuppressEndpoint(JsonApiEndpoints endpoint, ResourceType resourceType)
{
- switch (jsonApiEndpointMetadata)
+ if (!IsEndpointAvailable(endpoint, resourceType))
{
- case PrimaryResponseMetadata primaryMetadata:
- {
- UpdateProducesResponseTypeAttribute(endpoint, primaryMetadata.DocumentType);
- return [];
- }
- case PrimaryRequestMetadata primaryMetadata:
- {
- UpdateBodyParameterDescriptor(endpoint, primaryMetadata.DocumentType, null);
- return [];
- }
- case NonPrimaryEndpointMetadata nonPrimaryEndpointMetadata and (RelationshipResponseMetadata or SecondaryResponseMetadata):
- {
- return Expand(endpoint, nonPrimaryEndpointMetadata,
- (expandedEndpoint, documentType, _) => UpdateProducesResponseTypeAttribute(expandedEndpoint, documentType));
- }
- case NonPrimaryEndpointMetadata nonPrimaryEndpointMetadata and RelationshipRequestMetadata:
- {
- return Expand(endpoint, nonPrimaryEndpointMetadata, UpdateBodyParameterDescriptor);
- }
- case AtomicOperationsRequestMetadata:
- {
- UpdateBodyParameterDescriptor(endpoint, typeof(OperationsRequestDocument), null);
- return [];
- }
- case AtomicOperationsResponseMetadata:
+ return true;
+ }
+
+ if (IsSecondaryOrRelationshipEndpoint(endpoint))
+ {
+ if (resourceType.Relationships.Count == 0)
{
- UpdateProducesResponseTypeAttribute(endpoint, typeof(OperationsResponseDocument));
- return [];
+ return true;
}
- default:
+
+ if (endpoint is JsonApiEndpoints.DeleteRelationship or JsonApiEndpoints.PostRelationship)
{
- return [];
+ return !resourceType.Relationships.OfType().Any();
}
}
+
+ return false;
}
- private static void UpdateProducesResponseTypeAttribute(ActionDescriptor endpoint, Type responseDocumentType)
+ private static bool IsEndpointAvailable(JsonApiEndpoints endpoint, ResourceType resourceType)
{
- ProducesResponseTypeAttribute? attribute = null;
+ JsonApiEndpoints availableEndpoints = GetGeneratedControllerEndpoints(resourceType);
- if (ProducesJsonApiResponseDocument(endpoint))
+ if (availableEndpoints == JsonApiEndpoints.None)
{
- var producesResponse = endpoint.GetFilterMetadata();
-
- if (producesResponse != null)
- {
- attribute = producesResponse;
- }
+ // Auto-generated controllers are disabled, so we can't know what to hide.
+ // It is assumed that a handwritten JSON:API controller only provides action methods for what it supports.
+ // To accomplish that, derive from BaseJsonApiController instead of JsonApiController.
+ return true;
}
- ConsistencyGuard.ThrowIf(attribute == null);
- attribute.Type = responseDocumentType;
+ // For an overridden JSON:API action method in a partial class to show up, it's flag must be turned on in [Resource].
+ // Otherwise, it is considered to be an action method that throws because the endpoint is unavailable.
+ return IncludesEndpoint(endpoint, availableEndpoints);
}
- private static bool ProducesJsonApiResponseDocument(ActionDescriptor endpoint)
+ private static JsonApiEndpoints GetGeneratedControllerEndpoints(ResourceType resourceType)
{
- var produces = endpoint.GetFilterMetadata();
+ var resourceAttribute = resourceType.ClrType.GetCustomAttribute();
+ return resourceAttribute?.GenerateControllerEndpoints ?? JsonApiEndpoints.None;
+ }
+
+ private static bool IncludesEndpoint(JsonApiEndpoints endpoint, JsonApiEndpoints availableEndpoints)
+ {
+ bool? isIncluded = null;
- if (produces != null)
+ if (endpoint == JsonApiEndpoints.GetCollection)
{
- foreach (string contentType in produces.ContentTypes)
- {
- if (MediaTypeHeaderValue.TryParse(contentType, out MediaTypeHeaderValue? headerValue))
- {
- if (headerValue.MediaType.Equals(DefaultMediaType, StringComparison.OrdinalIgnoreCase))
- {
- return true;
- }
- }
- }
+ isIncluded = availableEndpoints.HasFlag(JsonApiEndpoints.GetCollection);
+ }
+ else if (endpoint == JsonApiEndpoints.GetSingle)
+ {
+ isIncluded = availableEndpoints.HasFlag(JsonApiEndpoints.GetSingle);
+ }
+ else if (endpoint == JsonApiEndpoints.GetSecondary)
+ {
+ isIncluded = availableEndpoints.HasFlag(JsonApiEndpoints.GetSecondary);
+ }
+ else if (endpoint == JsonApiEndpoints.GetRelationship)
+ {
+ isIncluded = availableEndpoints.HasFlag(JsonApiEndpoints.GetRelationship);
+ }
+ else if (endpoint == JsonApiEndpoints.Post)
+ {
+ isIncluded = availableEndpoints.HasFlag(JsonApiEndpoints.Post);
+ }
+ else if (endpoint == JsonApiEndpoints.PostRelationship)
+ {
+ isIncluded = availableEndpoints.HasFlag(JsonApiEndpoints.PostRelationship);
+ }
+ else if (endpoint == JsonApiEndpoints.Patch)
+ {
+ isIncluded = availableEndpoints.HasFlag(JsonApiEndpoints.Patch);
+ }
+ else if (endpoint == JsonApiEndpoints.PatchRelationship)
+ {
+ isIncluded = availableEndpoints.HasFlag(JsonApiEndpoints.PatchRelationship);
+ }
+ else if (endpoint == JsonApiEndpoints.Delete)
+ {
+ isIncluded = availableEndpoints.HasFlag(JsonApiEndpoints.Delete);
+ }
+ else if (endpoint == JsonApiEndpoints.DeleteRelationship)
+ {
+ isIncluded = availableEndpoints.HasFlag(JsonApiEndpoints.DeleteRelationship);
}
- return false;
+ ConsistencyGuard.ThrowIf(isIncluded == null);
+ return isIncluded.Value;
}
- private static List Expand(ActionDescriptor genericEndpoint, NonPrimaryEndpointMetadata metadata,
- Action expansionCallback)
+ private static bool IsSecondaryOrRelationshipEndpoint(JsonApiEndpoints endpoint)
{
- List expansion = [];
+ return endpoint is JsonApiEndpoints.GetSecondary or JsonApiEndpoints.GetRelationship or JsonApiEndpoints.PostRelationship or
+ JsonApiEndpoints.PatchRelationship or JsonApiEndpoints.DeleteRelationship;
+ }
- foreach ((string relationshipName, Type documentType) in metadata.DocumentTypesByRelationshipName)
+ private ActionDescriptor[] SetEndpointMetadata(ActionDescriptor descriptor, BuiltinJsonApiActionMethod actionMethod, ResourceType? resourceType)
+ {
+ Dictionary descriptorsByRelationship = [];
+
+ JsonApiEndpointMetadata? endpointMetadata = _jsonApiEndpointMetadataProvider.Get(descriptor);
+
+ switch (endpointMetadata?.RequestMetadata)
{
- if (genericEndpoint.AttributeRouteInfo == null)
+ case AtomicOperationsRequestMetadata atomicOperationsRequestMetadata:
{
- throw new NotSupportedException("Only attribute routing is supported for JsonApiDotNetCore endpoints.");
+ SetConsumes(descriptor, atomicOperationsRequestMetadata.DocumentType, JsonApiMediaType.AtomicOperations);
+ UpdateRequestBodyParameterDescriptor(descriptor, atomicOperationsRequestMetadata.DocumentType, null);
+
+ break;
}
+ case PrimaryRequestMetadata primaryRequestMetadata:
+ {
+ SetConsumes(descriptor, primaryRequestMetadata.DocumentType, JsonApiMediaType.Default);
+ UpdateRequestBodyParameterDescriptor(descriptor, primaryRequestMetadata.DocumentType, null);
+
+ break;
+ }
+ case RelationshipRequestMetadata relationshipRequestMetadata:
+ {
+ ConsistencyGuard.ThrowIf(descriptor.AttributeRouteInfo == null);
+
+ foreach ((RelationshipAttribute relationship, Type documentType) in relationshipRequestMetadata.DocumentTypesByRelationship)
+ {
+ ActionDescriptor relationshipDescriptor = Clone(descriptor);
- ActionDescriptor expandedEndpoint = Clone(genericEndpoint);
+ RemovePathParameter(relationshipDescriptor.Parameters, "relationshipName");
+ ExpandTemplate(relationshipDescriptor.AttributeRouteInfo!, relationship.PublicName);
+ SetConsumes(descriptor, documentType, JsonApiMediaType.Default);
+ UpdateRequestBodyParameterDescriptor(relationshipDescriptor, documentType, relationship.PublicName);
- RemovePathParameter(expandedEndpoint.Parameters, "relationshipName");
+ descriptorsByRelationship[relationship] = relationshipDescriptor;
+ }
+
+ break;
+ }
+ }
+
+ switch (endpointMetadata?.ResponseMetadata)
+ {
+ case AtomicOperationsResponseMetadata atomicOperationsResponseMetadata:
+ {
+ SetProduces(descriptor, atomicOperationsResponseMetadata.DocumentType);
+ SetProducesResponseTypes(descriptor, actionMethod, resourceType, atomicOperationsResponseMetadata.DocumentType);
- ExpandTemplate(expandedEndpoint.AttributeRouteInfo!, relationshipName);
+ break;
+ }
+ case PrimaryResponseMetadata primaryResponseMetadata:
+ {
+ SetProduces(descriptor, primaryResponseMetadata.DocumentType);
+ SetProducesResponseTypes(descriptor, actionMethod, resourceType, primaryResponseMetadata.DocumentType);
+ break;
+ }
+ case NonPrimaryResponseMetadata nonPrimaryResponseMetadata:
+ {
+ foreach ((RelationshipAttribute relationship, Type documentType) in nonPrimaryResponseMetadata.DocumentTypesByRelationship)
+ {
+ SetNonPrimaryResponseMetadata(descriptor, actionMethod, resourceType, descriptorsByRelationship, relationship, documentType);
+ }
- expansionCallback(expandedEndpoint, documentType, relationshipName);
+ break;
+ }
+ case EmptyRelationshipResponseMetadata emptyRelationshipResponseMetadata:
+ {
+ foreach (RelationshipAttribute relationship in emptyRelationshipResponseMetadata.Relationships)
+ {
+ SetNonPrimaryResponseMetadata(descriptor, actionMethod, resourceType, descriptorsByRelationship, relationship, null);
+ }
- expansion.Add(expandedEndpoint);
+ break;
+ }
}
- return expansion;
+ return descriptorsByRelationship.Count == 0 ? [descriptor] : descriptorsByRelationship.Values.ToArray();
}
- private static void UpdateBodyParameterDescriptor(ActionDescriptor endpoint, Type documentType, string? parameterName)
+ private static void SetConsumes(ActionDescriptor descriptor, Type requestType, JsonApiMediaType mediaType)
{
- ControllerParameterDescriptor? requestBodyDescriptor = endpoint.GetBodyParameterDescriptor();
+ // This value doesn't actually appear in the OpenAPI document, but is only used to invoke
+ // JsonApiRequestFormatMetadataProvider.GetSupportedContentTypes(), which determines the actual request content type.
+ string contentType = mediaType.ToString();
+
+ descriptor.FilterDescriptors.Add(new FilterDescriptor(new ConsumesAttribute(requestType, contentType), FilterScope));
+ }
+
+ private static void UpdateRequestBodyParameterDescriptor(ActionDescriptor descriptor, Type documentType, string? parameterName)
+ {
+ ControllerParameterDescriptor? requestBodyDescriptor = descriptor.GetBodyParameterDescriptor();
if (requestBodyDescriptor == null)
{
- MethodInfo actionMethod = endpoint.GetActionMethod();
+ MethodInfo actionMethod = descriptor.GetActionMethod();
throw new InvalidConfigurationException(
$"The action method '{actionMethod}' on type '{actionMethod.ReflectedType?.FullName}' contains no parameter with a [FromBody] attribute.");
}
+ descriptor.EndpointMetadata.Add(new ConsumesAttribute(JsonApiMediaType.Default.ToString()));
+
requestBodyDescriptor.ParameterType = documentType;
requestBodyDescriptor.ParameterInfo = new ParameterInfoWrapper(requestBodyDescriptor.ParameterInfo, documentType, parameterName);
}
@@ -218,8 +349,172 @@ private static void RemovePathParameter(ICollection paramet
parameters.Remove(descriptor);
}
- private static void ExpandTemplate(AttributeRouteInfo route, string expansionParameter)
+ private static void ExpandTemplate(AttributeRouteInfo route, string parameterName)
+ {
+ route.Template = route.Template!.Replace("{relationshipName}", parameterName);
+ }
+
+ private void SetProduces(ActionDescriptor descriptor, Type? documentType)
+ {
+ IReadOnlyList contentTypes = OpenApiContentTypeProvider.Instance.GetResponseContentTypes(documentType);
+
+ if (contentTypes.Count > 0)
+ {
+ descriptor.FilterDescriptors.Add(new FilterDescriptor(new ProducesAttribute(contentTypes[0]), FilterScope));
+ }
+ }
+
+ private void SetProducesResponseTypes(ActionDescriptor descriptor, BuiltinJsonApiActionMethod actionMethod, ResourceType? resourceType, Type? documentType)
+ {
+ foreach (HttpStatusCode statusCode in GetSuccessStatusCodesForActionMethod(actionMethod))
+ {
+ descriptor.FilterDescriptors.Add(documentType == null || StatusCodeHasNoResponseBody(statusCode)
+ ? new FilterDescriptor(new ProducesResponseTypeAttribute(typeof(void), (int)statusCode), FilterScope)
+ : new FilterDescriptor(new ProducesResponseTypeAttribute(documentType, (int)statusCode), FilterScope));
+ }
+
+ string? errorContentType = null;
+
+ if (documentType == null)
+ {
+ IReadOnlyList errorContentTypes = OpenApiContentTypeProvider.Instance.GetResponseContentTypes(ErrorDocumentType);
+ ConsistencyGuard.ThrowIf(errorContentTypes.Count == 0);
+ errorContentType = errorContentTypes[0];
+ }
+
+ foreach (HttpStatusCode statusCode in GetErrorStatusCodesForActionMethod(actionMethod, resourceType))
+ {
+ descriptor.FilterDescriptors.Add(errorContentType != null
+ ? new FilterDescriptor(new ProducesResponseTypeAttribute(ErrorDocumentType, (int)statusCode, errorContentType), FilterScope)
+ : new FilterDescriptor(new ProducesResponseTypeAttribute(ErrorDocumentType, (int)statusCode), FilterScope));
+ }
+ }
+
+ private static HttpStatusCode[] GetSuccessStatusCodesForActionMethod(BuiltinJsonApiActionMethod actionMethod)
+ {
+ HttpStatusCode[]? statusCodes = null;
+
+ if (actionMethod is AtomicOperationsActionMethod)
+ {
+ statusCodes =
+ [
+ HttpStatusCode.OK,
+ HttpStatusCode.NoContent
+ ];
+ }
+ else if (actionMethod is JsonApiActionMethod jsonApiActionMethod)
+ {
+ statusCodes = jsonApiActionMethod.Endpoint switch
+ {
+ JsonApiEndpoints.GetCollection or JsonApiEndpoints.GetSingle or JsonApiEndpoints.GetSecondary or JsonApiEndpoints.GetRelationship =>
+ [
+ HttpStatusCode.OK,
+ HttpStatusCode.NotModified
+ ],
+ JsonApiEndpoints.Post =>
+ [
+ HttpStatusCode.Created,
+ HttpStatusCode.NoContent
+ ],
+ JsonApiEndpoints.Patch =>
+ [
+ HttpStatusCode.OK,
+ HttpStatusCode.NoContent
+ ],
+ JsonApiEndpoints.Delete or JsonApiEndpoints.PostRelationship or JsonApiEndpoints.PatchRelationship or JsonApiEndpoints.DeleteRelationship =>
+ [
+ HttpStatusCode.NoContent
+ ],
+ _ => null
+ };
+ }
+
+ ConsistencyGuard.ThrowIf(statusCodes == null);
+ return statusCodes;
+ }
+
+ private static bool StatusCodeHasNoResponseBody(HttpStatusCode statusCode)
{
- route.Template = route.Template!.Replace("{relationshipName}", expansionParameter);
+ return statusCode is HttpStatusCode.NoContent or HttpStatusCode.NotModified;
}
+
+ private HttpStatusCode[] GetErrorStatusCodesForActionMethod(BuiltinJsonApiActionMethod actionMethod, ResourceType? resourceType)
+ {
+ HttpStatusCode[]? statusCodes = null;
+
+ if (actionMethod is AtomicOperationsActionMethod)
+ {
+ statusCodes =
+ [
+ HttpStatusCode.BadRequest,
+ HttpStatusCode.Forbidden,
+ HttpStatusCode.NotFound,
+ HttpStatusCode.Conflict,
+ HttpStatusCode.UnprocessableEntity
+ ];
+ }
+ else if (actionMethod is JsonApiActionMethod jsonApiActionMethod)
+ {
+ // Condition doesn't apply to atomic operations, because Forbidden is also used when an operation is not accessible.
+ ClientIdGenerationMode clientIdGeneration = resourceType?.ClientIdGeneration ?? _options.ClientIdGeneration;
+
+ statusCodes = jsonApiActionMethod.Endpoint switch
+ {
+ JsonApiEndpoints.GetCollection => [HttpStatusCode.BadRequest],
+ JsonApiEndpoints.GetSingle or JsonApiEndpoints.GetSecondary or JsonApiEndpoints.GetRelationship =>
+ [
+ HttpStatusCode.BadRequest,
+ HttpStatusCode.NotFound
+ ],
+ JsonApiEndpoints.Post when clientIdGeneration == ClientIdGenerationMode.Forbidden =>
+ [
+ HttpStatusCode.BadRequest,
+ HttpStatusCode.Forbidden,
+ HttpStatusCode.NotFound,
+ HttpStatusCode.Conflict,
+ HttpStatusCode.UnprocessableEntity
+ ],
+ JsonApiEndpoints.Post or JsonApiEndpoints.Patch =>
+ [
+ HttpStatusCode.BadRequest,
+ HttpStatusCode.NotFound,
+ HttpStatusCode.Conflict,
+ HttpStatusCode.UnprocessableEntity
+ ],
+ JsonApiEndpoints.Delete => [HttpStatusCode.NotFound],
+ JsonApiEndpoints.PostRelationship or JsonApiEndpoints.PatchRelationship or JsonApiEndpoints.DeleteRelationship =>
+ [
+ HttpStatusCode.BadRequest,
+ HttpStatusCode.NotFound,
+ HttpStatusCode.Conflict,
+ HttpStatusCode.UnprocessableEntity
+ ],
+ _ => null
+ };
+ }
+
+ ConsistencyGuard.ThrowIf(statusCodes == null);
+ return statusCodes;
+ }
+
+ private void SetNonPrimaryResponseMetadata(ActionDescriptor descriptor, BuiltinJsonApiActionMethod actionMethod, ResourceType? resourceType,
+ Dictionary descriptorsByRelationship, RelationshipAttribute relationship, Type? documentType)
+ {
+ ConsistencyGuard.ThrowIf(descriptor.AttributeRouteInfo == null);
+
+ if (!descriptorsByRelationship.TryGetValue(relationship, out ActionDescriptor? relationshipDescriptor))
+ {
+ relationshipDescriptor = Clone(descriptor);
+ RemovePathParameter(relationshipDescriptor.Parameters, "relationshipName");
+ }
+
+ ExpandTemplate(relationshipDescriptor.AttributeRouteInfo!, relationship.PublicName);
+ SetProduces(relationshipDescriptor, documentType);
+ SetProducesResponseTypes(relationshipDescriptor, actionMethod, resourceType, documentType);
+
+ descriptorsByRelationship[relationship] = relationshipDescriptor;
+ }
+
+ [LoggerMessage(Level = LogLevel.Warning, Message = "Hiding unsupported custom JSON:API action method [{HttpMethods}] {ActionMethod} in OpenAPI.")]
+ private partial void LogSuppressedActionMethod(string httpMethods, string? actionMethod);
}
diff --git a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/ActionMethods/AtomicOperationsActionMethod.cs b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/ActionMethods/AtomicOperationsActionMethod.cs
new file mode 100644
index 0000000000..d80a86cd06
--- /dev/null
+++ b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/ActionMethods/AtomicOperationsActionMethod.cs
@@ -0,0 +1,9 @@
+using JsonApiDotNetCore.Controllers;
+
+namespace JsonApiDotNetCore.OpenApi.Swashbuckle.JsonApiMetadata.ActionMethods;
+
+///
+/// The built-in JSON:API operations action method .
+///
+internal sealed class AtomicOperationsActionMethod(Type controllerType)
+ : BuiltinJsonApiActionMethod(controllerType);
diff --git a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/ActionMethods/BuiltinJsonApiActionMethod.cs b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/ActionMethods/BuiltinJsonApiActionMethod.cs
new file mode 100644
index 0000000000..a6374801b3
--- /dev/null
+++ b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/ActionMethods/BuiltinJsonApiActionMethod.cs
@@ -0,0 +1,18 @@
+using JsonApiDotNetCore.Controllers;
+
+namespace JsonApiDotNetCore.OpenApi.Swashbuckle.JsonApiMetadata.ActionMethods;
+
+///
+/// A built-in JSON:API action method on .
+///
+internal abstract class BuiltinJsonApiActionMethod : OpenApiActionMethod
+{
+ public Type ControllerType { get; }
+
+ protected BuiltinJsonApiActionMethod(Type controllerType)
+ {
+ ArgumentNullException.ThrowIfNull(controllerType);
+
+ ControllerType = controllerType;
+ }
+}
diff --git a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/ActionMethods/CustomControllerActionMethod.cs b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/ActionMethods/CustomControllerActionMethod.cs
new file mode 100644
index 0000000000..c5b89f27f1
--- /dev/null
+++ b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/ActionMethods/CustomControllerActionMethod.cs
@@ -0,0 +1,13 @@
+namespace JsonApiDotNetCore.OpenApi.Swashbuckle.JsonApiMetadata.ActionMethods;
+
+///
+/// An action method in a custom controller, unrelated to JSON:API.
+///
+internal sealed class CustomControllerActionMethod : OpenApiActionMethod
+{
+ public static CustomControllerActionMethod Instance { get; } = new();
+
+ private CustomControllerActionMethod()
+ {
+ }
+}
diff --git a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/ActionMethods/CustomJsonApiActionMethod.cs b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/ActionMethods/CustomJsonApiActionMethod.cs
new file mode 100644
index 0000000000..194d623d94
--- /dev/null
+++ b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/ActionMethods/CustomJsonApiActionMethod.cs
@@ -0,0 +1,15 @@
+using JsonApiDotNetCore.Controllers;
+
+namespace JsonApiDotNetCore.OpenApi.Swashbuckle.JsonApiMetadata.ActionMethods;
+
+///
+/// A custom action method on .
+///
+internal sealed class CustomJsonApiActionMethod : OpenApiActionMethod
+{
+ public static CustomJsonApiActionMethod Instance { get; } = new();
+
+ private CustomJsonApiActionMethod()
+ {
+ }
+}
diff --git a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/ActionMethods/JsonApiActionMethod.cs b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/ActionMethods/JsonApiActionMethod.cs
new file mode 100644
index 0000000000..3cbfeff775
--- /dev/null
+++ b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/ActionMethods/JsonApiActionMethod.cs
@@ -0,0 +1,17 @@
+using JsonApiDotNetCore.Controllers;
+
+namespace JsonApiDotNetCore.OpenApi.Swashbuckle.JsonApiMetadata.ActionMethods;
+
+///
+/// One of the built-in JSON:API action methods on .
+///
+internal sealed class JsonApiActionMethod(JsonApiEndpoints endpoint, Type controllerType)
+ : BuiltinJsonApiActionMethod(controllerType)
+{
+ public JsonApiEndpoints Endpoint { get; } = endpoint;
+
+ public override string ToString()
+ {
+ return Endpoint.ToString();
+ }
+}
diff --git a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/ActionMethods/OpenApiActionMethod.cs b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/ActionMethods/OpenApiActionMethod.cs
new file mode 100644
index 0000000000..f8519f414a
--- /dev/null
+++ b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/ActionMethods/OpenApiActionMethod.cs
@@ -0,0 +1,58 @@
+using System.Reflection;
+using JsonApiDotNetCore.Controllers;
+using JsonApiDotNetCore.Middleware;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Abstractions;
+using Microsoft.AspNetCore.Mvc.Routing;
+
+namespace JsonApiDotNetCore.OpenApi.Swashbuckle.JsonApiMetadata.ActionMethods;
+
+internal abstract class OpenApiActionMethod
+{
+ public static OpenApiActionMethod Create(ActionDescriptor descriptor)
+ {
+ ArgumentNullException.ThrowIfNull(descriptor);
+
+ MethodInfo actionMethod = descriptor.GetActionMethod();
+
+ if (IsJsonApiController(actionMethod))
+ {
+ Type? controllerType = actionMethod.ReflectedType;
+ ConsistencyGuard.ThrowIf(controllerType == null);
+
+ if (IsAtomicOperationsController(actionMethod))
+ {
+ var httpPostAttribute = actionMethod.GetCustomAttribute(true);
+
+ if (httpPostAttribute != null)
+ {
+ return new AtomicOperationsActionMethod(controllerType);
+ }
+ }
+ else
+ {
+ IEnumerable httpMethodAttributes = actionMethod.GetCustomAttributes(true);
+ JsonApiEndpoints endpoint = httpMethodAttributes.GetJsonApiEndpoint();
+
+ if (endpoint != JsonApiEndpoints.None)
+ {
+ return new JsonApiActionMethod(endpoint, controllerType);
+ }
+ }
+
+ return CustomJsonApiActionMethod.Instance;
+ }
+
+ return CustomControllerActionMethod.Instance;
+ }
+
+ private static bool IsJsonApiController(MethodInfo controllerAction)
+ {
+ return typeof(CoreJsonApiController).IsAssignableFrom(controllerAction.ReflectedType);
+ }
+
+ private static bool IsAtomicOperationsController(MethodInfo controllerAction)
+ {
+ return typeof(BaseJsonApiOperationsController).IsAssignableFrom(controllerAction.ReflectedType);
+ }
+}
diff --git a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/AtomicOperationsRequestMetadata.cs b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/Documents/AtomicOperationsRequestMetadata.cs
similarity index 51%
rename from src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/AtomicOperationsRequestMetadata.cs
rename to src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/Documents/AtomicOperationsRequestMetadata.cs
index b9b0f44462..9fcfa90989 100644
--- a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/AtomicOperationsRequestMetadata.cs
+++ b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/Documents/AtomicOperationsRequestMetadata.cs
@@ -1,9 +1,13 @@
-namespace JsonApiDotNetCore.OpenApi.Swashbuckle.JsonApiMetadata;
+using JsonApiDotNetCore.OpenApi.Swashbuckle.JsonApiObjects.Documents;
+
+namespace JsonApiDotNetCore.OpenApi.Swashbuckle.JsonApiMetadata.Documents;
internal sealed class AtomicOperationsRequestMetadata : IJsonApiRequestMetadata
{
public static AtomicOperationsRequestMetadata Instance { get; } = new();
+ public Type DocumentType => typeof(OperationsRequestDocument);
+
private AtomicOperationsRequestMetadata()
{
}
diff --git a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/AtomicOperationsResponseMetadata.cs b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/Documents/AtomicOperationsResponseMetadata.cs
similarity index 51%
rename from src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/AtomicOperationsResponseMetadata.cs
rename to src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/Documents/AtomicOperationsResponseMetadata.cs
index 838055c378..f259b76fb4 100644
--- a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/AtomicOperationsResponseMetadata.cs
+++ b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/Documents/AtomicOperationsResponseMetadata.cs
@@ -1,9 +1,13 @@
-namespace JsonApiDotNetCore.OpenApi.Swashbuckle.JsonApiMetadata;
+using JsonApiDotNetCore.OpenApi.Swashbuckle.JsonApiObjects.Documents;
+
+namespace JsonApiDotNetCore.OpenApi.Swashbuckle.JsonApiMetadata.Documents;
internal sealed class AtomicOperationsResponseMetadata : IJsonApiResponseMetadata
{
public static AtomicOperationsResponseMetadata Instance { get; } = new();
+ public Type DocumentType => typeof(OperationsResponseDocument);
+
private AtomicOperationsResponseMetadata()
{
}
diff --git a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/Documents/EmptyRelationshipResponseMetadata.cs b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/Documents/EmptyRelationshipResponseMetadata.cs
new file mode 100644
index 0000000000..3cc784f9c4
--- /dev/null
+++ b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/Documents/EmptyRelationshipResponseMetadata.cs
@@ -0,0 +1,15 @@
+using JsonApiDotNetCore.Resources.Annotations;
+
+namespace JsonApiDotNetCore.OpenApi.Swashbuckle.JsonApiMetadata.Documents;
+
+internal sealed class EmptyRelationshipResponseMetadata : IJsonApiResponseMetadata
+{
+ public IReadOnlyCollection Relationships { get; }
+
+ public EmptyRelationshipResponseMetadata(IReadOnlyCollection relationships)
+ {
+ ArgumentNullException.ThrowIfNull(relationships);
+
+ Relationships = relationships;
+ }
+}
diff --git a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/Documents/IJsonApiRequestMetadata.cs b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/Documents/IJsonApiRequestMetadata.cs
new file mode 100644
index 0000000000..78206521f0
--- /dev/null
+++ b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/Documents/IJsonApiRequestMetadata.cs
@@ -0,0 +1,3 @@
+namespace JsonApiDotNetCore.OpenApi.Swashbuckle.JsonApiMetadata.Documents;
+
+internal interface IJsonApiRequestMetadata;
diff --git a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/Documents/IJsonApiResponseMetadata.cs b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/Documents/IJsonApiResponseMetadata.cs
new file mode 100644
index 0000000000..205e8cb4dc
--- /dev/null
+++ b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/Documents/IJsonApiResponseMetadata.cs
@@ -0,0 +1,3 @@
+namespace JsonApiDotNetCore.OpenApi.Swashbuckle.JsonApiMetadata.Documents;
+
+internal interface IJsonApiResponseMetadata;
diff --git a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/Documents/JsonApiEndpointMetadata.cs b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/Documents/JsonApiEndpointMetadata.cs
new file mode 100644
index 0000000000..4a57c6a686
--- /dev/null
+++ b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/Documents/JsonApiEndpointMetadata.cs
@@ -0,0 +1,7 @@
+namespace JsonApiDotNetCore.OpenApi.Swashbuckle.JsonApiMetadata.Documents;
+
+internal sealed class JsonApiEndpointMetadata(IJsonApiRequestMetadata? requestMetadata, IJsonApiResponseMetadata? responseMetadata)
+{
+ public IJsonApiRequestMetadata? RequestMetadata { get; } = requestMetadata;
+ public IJsonApiResponseMetadata? ResponseMetadata { get; } = responseMetadata;
+}
diff --git a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/Documents/NonPrimaryResponseMetadata.cs b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/Documents/NonPrimaryResponseMetadata.cs
new file mode 100644
index 0000000000..3a4b7ad432
--- /dev/null
+++ b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/Documents/NonPrimaryResponseMetadata.cs
@@ -0,0 +1,15 @@
+using JsonApiDotNetCore.Resources.Annotations;
+
+namespace JsonApiDotNetCore.OpenApi.Swashbuckle.JsonApiMetadata.Documents;
+
+internal class NonPrimaryResponseMetadata : IJsonApiResponseMetadata
+{
+ public IReadOnlyDictionary DocumentTypesByRelationship { get; }
+
+ protected NonPrimaryResponseMetadata(IReadOnlyDictionary documentTypesByRelationship)
+ {
+ ArgumentNullException.ThrowIfNull(documentTypesByRelationship);
+
+ DocumentTypesByRelationship = documentTypesByRelationship;
+ }
+}
diff --git a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/PrimaryRequestMetadata.cs b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/Documents/PrimaryRequestMetadata.cs
similarity index 78%
rename from src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/PrimaryRequestMetadata.cs
rename to src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/Documents/PrimaryRequestMetadata.cs
index 7c224417f1..cbcf6ad587 100644
--- a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/PrimaryRequestMetadata.cs
+++ b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/Documents/PrimaryRequestMetadata.cs
@@ -1,4 +1,4 @@
-namespace JsonApiDotNetCore.OpenApi.Swashbuckle.JsonApiMetadata;
+namespace JsonApiDotNetCore.OpenApi.Swashbuckle.JsonApiMetadata.Documents;
internal sealed class PrimaryRequestMetadata : IJsonApiRequestMetadata
{
diff --git a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/Documents/PrimaryResponseMetadata.cs b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/Documents/PrimaryResponseMetadata.cs
new file mode 100644
index 0000000000..af0761be28
--- /dev/null
+++ b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/Documents/PrimaryResponseMetadata.cs
@@ -0,0 +1,6 @@
+namespace JsonApiDotNetCore.OpenApi.Swashbuckle.JsonApiMetadata.Documents;
+
+internal sealed class PrimaryResponseMetadata(Type? documentType) : IJsonApiResponseMetadata
+{
+ public Type? DocumentType { get; } = documentType;
+}
diff --git a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/Documents/RelationshipRequestMetadata.cs b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/Documents/RelationshipRequestMetadata.cs
new file mode 100644
index 0000000000..71c82337a8
--- /dev/null
+++ b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/Documents/RelationshipRequestMetadata.cs
@@ -0,0 +1,15 @@
+using JsonApiDotNetCore.Resources.Annotations;
+
+namespace JsonApiDotNetCore.OpenApi.Swashbuckle.JsonApiMetadata.Documents;
+
+internal sealed class RelationshipRequestMetadata : IJsonApiRequestMetadata
+{
+ public IReadOnlyDictionary DocumentTypesByRelationship { get; }
+
+ public RelationshipRequestMetadata(IReadOnlyDictionary documentTypesByRelationship)
+ {
+ ArgumentNullException.ThrowIfNull(documentTypesByRelationship);
+
+ DocumentTypesByRelationship = documentTypesByRelationship;
+ }
+}
diff --git a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/Documents/RelationshipResponseMetadata.cs b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/Documents/RelationshipResponseMetadata.cs
new file mode 100644
index 0000000000..14d43cd44e
--- /dev/null
+++ b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/Documents/RelationshipResponseMetadata.cs
@@ -0,0 +1,6 @@
+using JsonApiDotNetCore.Resources.Annotations;
+
+namespace JsonApiDotNetCore.OpenApi.Swashbuckle.JsonApiMetadata.Documents;
+
+internal sealed class RelationshipResponseMetadata(IReadOnlyDictionary documentTypesByRelationship)
+ : NonPrimaryResponseMetadata(documentTypesByRelationship);
diff --git a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/Documents/SecondaryResponseMetadata.cs b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/Documents/SecondaryResponseMetadata.cs
new file mode 100644
index 0000000000..47349ce44e
--- /dev/null
+++ b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/Documents/SecondaryResponseMetadata.cs
@@ -0,0 +1,6 @@
+using JsonApiDotNetCore.Resources.Annotations;
+
+namespace JsonApiDotNetCore.OpenApi.Swashbuckle.JsonApiMetadata.Documents;
+
+internal sealed class SecondaryResponseMetadata(IReadOnlyDictionary documentTypesByRelationship)
+ : NonPrimaryResponseMetadata(documentTypesByRelationship);
diff --git a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/EndpointResolver.cs b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/EndpointResolver.cs
deleted file mode 100644
index e4c074f081..0000000000
--- a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/EndpointResolver.cs
+++ /dev/null
@@ -1,38 +0,0 @@
-using System.Reflection;
-using JsonApiDotNetCore.Controllers;
-using JsonApiDotNetCore.Middleware;
-using Microsoft.AspNetCore.Mvc.Routing;
-
-namespace JsonApiDotNetCore.OpenApi.Swashbuckle.JsonApiMetadata;
-
-internal sealed class EndpointResolver
-{
- public static EndpointResolver Instance { get; } = new();
-
- private EndpointResolver()
- {
- }
-
- public JsonApiEndpoints GetEndpoint(MethodInfo controllerAction)
- {
- ArgumentNullException.ThrowIfNull(controllerAction);
-
- if (!IsJsonApiController(controllerAction))
- {
- return JsonApiEndpoints.None;
- }
-
- IEnumerable httpMethodAttributes = controllerAction.GetCustomAttributes(true);
- return httpMethodAttributes.GetJsonApiEndpoint();
- }
-
- private bool IsJsonApiController(MethodInfo controllerAction)
- {
- return typeof(CoreJsonApiController).IsAssignableFrom(controllerAction.ReflectedType);
- }
-
- public bool IsAtomicOperationsController(MethodInfo controllerAction)
- {
- return typeof(BaseJsonApiOperationsController).IsAssignableFrom(controllerAction.ReflectedType);
- }
-}
diff --git a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/IJsonApiEndpointMetadata.cs b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/IJsonApiEndpointMetadata.cs
deleted file mode 100644
index 01a8247ec5..0000000000
--- a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/IJsonApiEndpointMetadata.cs
+++ /dev/null
@@ -1,3 +0,0 @@
-namespace JsonApiDotNetCore.OpenApi.Swashbuckle.JsonApiMetadata;
-
-internal interface IJsonApiEndpointMetadata;
diff --git a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/IJsonApiRequestMetadata.cs b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/IJsonApiRequestMetadata.cs
deleted file mode 100644
index 86fbddebb6..0000000000
--- a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/IJsonApiRequestMetadata.cs
+++ /dev/null
@@ -1,3 +0,0 @@
-namespace JsonApiDotNetCore.OpenApi.Swashbuckle.JsonApiMetadata;
-
-internal interface IJsonApiRequestMetadata : IJsonApiEndpointMetadata;
diff --git a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/IJsonApiResponseMetadata.cs b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/IJsonApiResponseMetadata.cs
deleted file mode 100644
index 85fb69e856..0000000000
--- a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/IJsonApiResponseMetadata.cs
+++ /dev/null
@@ -1,3 +0,0 @@
-namespace JsonApiDotNetCore.OpenApi.Swashbuckle.JsonApiMetadata;
-
-internal interface IJsonApiResponseMetadata : IJsonApiEndpointMetadata;
diff --git a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/JsonApiEndpointMetadataContainer.cs b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/JsonApiEndpointMetadataContainer.cs
deleted file mode 100644
index 60b7182eb6..0000000000
--- a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/JsonApiEndpointMetadataContainer.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-namespace JsonApiDotNetCore.OpenApi.Swashbuckle.JsonApiMetadata;
-
-///
-/// Metadata available at runtime about a JsonApiDotNetCore endpoint.
-///
-internal sealed class JsonApiEndpointMetadataContainer(IJsonApiRequestMetadata? requestMetadata, IJsonApiResponseMetadata? responseMetadata)
-{
- public IJsonApiRequestMetadata? RequestMetadata { get; } = requestMetadata;
- public IJsonApiResponseMetadata? ResponseMetadata { get; } = responseMetadata;
-}
diff --git a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/JsonApiEndpointMetadataProvider.cs b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/JsonApiEndpointMetadataProvider.cs
index 6fd6f9e42e..dc4d7cdd9c 100644
--- a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/JsonApiEndpointMetadataProvider.cs
+++ b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/JsonApiEndpointMetadataProvider.cs
@@ -1,15 +1,16 @@
-using System.Reflection;
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Controllers;
using JsonApiDotNetCore.Middleware;
+using JsonApiDotNetCore.OpenApi.Swashbuckle.JsonApiMetadata.ActionMethods;
+using JsonApiDotNetCore.OpenApi.Swashbuckle.JsonApiMetadata.Documents;
using JsonApiDotNetCore.OpenApi.Swashbuckle.JsonApiObjects.Documents;
using JsonApiDotNetCore.Resources.Annotations;
+using Microsoft.AspNetCore.Mvc.Abstractions;
namespace JsonApiDotNetCore.OpenApi.Swashbuckle.JsonApiMetadata;
///
-/// Provides JsonApiDotNetCore related metadata for an ASP.NET controller action that can only be computed from the at
-/// runtime.
+/// Provides JsonApiDotNetCore related metadata for an ASP.NET action method that can only be computed from the at runtime.
///
internal sealed class JsonApiEndpointMetadataProvider
{
@@ -25,28 +26,32 @@ public JsonApiEndpointMetadataProvider(IControllerResourceMapping controllerReso
_nonPrimaryDocumentTypeFactory = nonPrimaryDocumentTypeFactory;
}
- public JsonApiEndpointMetadataContainer Get(MethodInfo controllerAction)
+ public JsonApiEndpointMetadata? Get(ActionDescriptor descriptor)
{
- ArgumentNullException.ThrowIfNull(controllerAction);
+ ArgumentNullException.ThrowIfNull(descriptor);
- if (EndpointResolver.Instance.IsAtomicOperationsController(controllerAction))
- {
- return new JsonApiEndpointMetadataContainer(AtomicOperationsRequestMetadata.Instance, AtomicOperationsResponseMetadata.Instance);
- }
-
- JsonApiEndpoints endpoint = EndpointResolver.Instance.GetEndpoint(controllerAction);
+ var actionMethod = OpenApiActionMethod.Create(descriptor);
- if (endpoint == JsonApiEndpoints.None)
+ switch (actionMethod)
{
- throw new NotSupportedException($"Unable to provide metadata for non-JSON:API endpoint '{controllerAction.ReflectedType!.FullName}'.");
+ case AtomicOperationsActionMethod:
+ {
+ return new JsonApiEndpointMetadata(AtomicOperationsRequestMetadata.Instance, AtomicOperationsResponseMetadata.Instance);
+ }
+ case JsonApiActionMethod jsonApiActionMethod:
+ {
+ ResourceType? primaryResourceType = _controllerResourceMapping.GetResourceTypeForController(jsonApiActionMethod.ControllerType);
+ ConsistencyGuard.ThrowIf(primaryResourceType == null);
+
+ IJsonApiRequestMetadata? requestMetadata = GetRequestMetadata(jsonApiActionMethod.Endpoint, primaryResourceType);
+ IJsonApiResponseMetadata? responseMetadata = GetResponseMetadata(jsonApiActionMethod.Endpoint, primaryResourceType);
+ return new JsonApiEndpointMetadata(requestMetadata, responseMetadata);
+ }
+ default:
+ {
+ return null;
+ }
}
-
- ResourceType? primaryResourceType = _controllerResourceMapping.GetResourceTypeForController(controllerAction.ReflectedType);
- ConsistencyGuard.ThrowIf(primaryResourceType == null);
-
- IJsonApiRequestMetadata? requestMetadata = GetRequestMetadata(endpoint, primaryResourceType);
- IJsonApiResponseMetadata? responseMetadata = GetResponseMetadata(endpoint, primaryResourceType);
- return new JsonApiEndpointMetadataContainer(requestMetadata, responseMetadata);
}
private IJsonApiRequestMetadata? GetRequestMetadata(JsonApiEndpoints endpoint, ResourceType primaryResourceType)
@@ -75,14 +80,14 @@ private static PrimaryRequestMetadata GetPatchResourceRequestMetadata(Type resou
return new PrimaryRequestMetadata(documentType);
}
- private RelationshipRequestMetadata GetRelationshipRequestMetadata(IEnumerable relationships, bool ignoreHasOneRelationships)
+ private RelationshipRequestMetadata GetRelationshipRequestMetadata(IReadOnlyCollection relationships, bool ignoreHasOneRelationships)
{
IEnumerable relationshipsOfEndpoint = ignoreHasOneRelationships ? relationships.OfType() : relationships;
- IDictionary requestDocumentTypesByRelationshipName = relationshipsOfEndpoint.ToDictionary(relationship => relationship.PublicName,
+ Dictionary documentTypesByRelationship = relationshipsOfEndpoint.ToDictionary(relationship => relationship,
_nonPrimaryDocumentTypeFactory.GetForRelationshipRequest);
- return new RelationshipRequestMetadata(requestDocumentTypesByRelationshipName);
+ return new RelationshipRequestMetadata(documentTypesByRelationship.AsReadOnly());
}
private IJsonApiResponseMetadata? GetResponseMetadata(JsonApiEndpoints endpoint, ResourceType primaryResourceType)
@@ -91,12 +96,20 @@ private RelationshipRequestMetadata GetRelationshipRequestMetadata(IEnumerable GetPrimaryResponseMetadata(
primaryResourceType.ClrType, endpoint == JsonApiEndpoints.GetCollection),
+ JsonApiEndpoints.Delete => GetEmptyPrimaryResponseMetadata(),
JsonApiEndpoints.GetSecondary => GetSecondaryResponseMetadata(primaryResourceType.Relationships),
JsonApiEndpoints.GetRelationship => GetRelationshipResponseMetadata(primaryResourceType.Relationships),
+ JsonApiEndpoints.PostRelationship or JsonApiEndpoints.PatchRelationship or JsonApiEndpoints.DeleteRelationship =>
+ GetEmptyRelationshipResponseMetadata(primaryResourceType.Relationships, endpoint != JsonApiEndpoints.PatchRelationship),
_ => null
};
}
+ private static PrimaryResponseMetadata GetEmptyPrimaryResponseMetadata()
+ {
+ return new PrimaryResponseMetadata(null);
+ }
+
private static PrimaryResponseMetadata GetPrimaryResponseMetadata(Type resourceClrType, bool endpointReturnsCollection)
{
Type documentOpenType = endpointReturnsCollection ? typeof(CollectionResponseDocument<>) : typeof(PrimaryResponseDocument<>);
@@ -107,17 +120,26 @@ private static PrimaryResponseMetadata GetPrimaryResponseMetadata(Type resourceC
private SecondaryResponseMetadata GetSecondaryResponseMetadata(IEnumerable relationships)
{
- IDictionary responseDocumentTypesByRelationshipName = relationships.ToDictionary(relationship => relationship.PublicName,
+ Dictionary documentTypesByRelationship = relationships.ToDictionary(relationship => relationship,
_nonPrimaryDocumentTypeFactory.GetForSecondaryResponse);
- return new SecondaryResponseMetadata(responseDocumentTypesByRelationshipName);
+ return new SecondaryResponseMetadata(documentTypesByRelationship.AsReadOnly());
}
- private RelationshipResponseMetadata GetRelationshipResponseMetadata(IEnumerable relationships)
+ private RelationshipResponseMetadata GetRelationshipResponseMetadata(IReadOnlyCollection relationships)
{
- IDictionary responseDocumentTypesByRelationshipName = relationships.ToDictionary(relationship => relationship.PublicName,
+ Dictionary documentTypesByRelationship = relationships.ToDictionary(relationship => relationship,
_nonPrimaryDocumentTypeFactory.GetForRelationshipResponse);
- return new RelationshipResponseMetadata(responseDocumentTypesByRelationshipName);
+ return new RelationshipResponseMetadata(documentTypesByRelationship.AsReadOnly());
+ }
+
+ private static EmptyRelationshipResponseMetadata GetEmptyRelationshipResponseMetadata(IReadOnlyCollection relationships,
+ bool ignoreHasOneRelationships)
+ {
+ IReadOnlyCollection relationshipsOfEndpoint =
+ ignoreHasOneRelationships ? relationships.OfType().ToList().AsReadOnly() : relationships;
+
+ return new EmptyRelationshipResponseMetadata(relationshipsOfEndpoint);
}
}
diff --git a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/NonPrimaryEndpointMetadata.cs b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/NonPrimaryEndpointMetadata.cs
deleted file mode 100644
index ed43dc4da8..0000000000
--- a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/NonPrimaryEndpointMetadata.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-namespace JsonApiDotNetCore.OpenApi.Swashbuckle.JsonApiMetadata;
-
-internal abstract class NonPrimaryEndpointMetadata
-{
- public IDictionary DocumentTypesByRelationshipName { get; }
-
- protected NonPrimaryEndpointMetadata(IDictionary documentTypesByRelationshipName)
- {
- ArgumentNullException.ThrowIfNull(documentTypesByRelationshipName);
-
- DocumentTypesByRelationshipName = documentTypesByRelationshipName;
- }
-}
diff --git a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/PrimaryResponseMetadata.cs b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/PrimaryResponseMetadata.cs
deleted file mode 100644
index 2d2590be7d..0000000000
--- a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/PrimaryResponseMetadata.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-namespace JsonApiDotNetCore.OpenApi.Swashbuckle.JsonApiMetadata;
-
-internal sealed class PrimaryResponseMetadata : IJsonApiResponseMetadata
-{
- public Type DocumentType { get; }
-
- public PrimaryResponseMetadata(Type documentType)
- {
- ArgumentNullException.ThrowIfNull(documentType);
-
- DocumentType = documentType;
- }
-}
diff --git a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/RelationshipRequestMetadata.cs b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/RelationshipRequestMetadata.cs
deleted file mode 100644
index e2636da079..0000000000
--- a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/RelationshipRequestMetadata.cs
+++ /dev/null
@@ -1,4 +0,0 @@
-namespace JsonApiDotNetCore.OpenApi.Swashbuckle.JsonApiMetadata;
-
-internal sealed class RelationshipRequestMetadata(IDictionary documentTypesByRelationshipName)
- : NonPrimaryEndpointMetadata(documentTypesByRelationshipName), IJsonApiRequestMetadata;
diff --git a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/RelationshipResponseMetadata.cs b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/RelationshipResponseMetadata.cs
deleted file mode 100644
index 7221dfbe5e..0000000000
--- a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/RelationshipResponseMetadata.cs
+++ /dev/null
@@ -1,4 +0,0 @@
-namespace JsonApiDotNetCore.OpenApi.Swashbuckle.JsonApiMetadata;
-
-internal sealed class RelationshipResponseMetadata(IDictionary documentTypesByRelationshipName)
- : NonPrimaryEndpointMetadata(documentTypesByRelationshipName), IJsonApiResponseMetadata;
diff --git a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/SecondaryResponseMetadata.cs b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/SecondaryResponseMetadata.cs
deleted file mode 100644
index 39b8ce8d4f..0000000000
--- a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/SecondaryResponseMetadata.cs
+++ /dev/null
@@ -1,4 +0,0 @@
-namespace JsonApiDotNetCore.OpenApi.Swashbuckle.JsonApiMetadata;
-
-internal sealed class SecondaryResponseMetadata(IDictionary documentTypesByRelationshipName)
- : NonPrimaryEndpointMetadata(documentTypesByRelationshipName), IJsonApiResponseMetadata;
diff --git a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiRequestFormatMetadataProvider.cs b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiRequestFormatMetadataProvider.cs
index 6def822bd9..e73c0d120e 100644
--- a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiRequestFormatMetadataProvider.cs
+++ b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiRequestFormatMetadataProvider.cs
@@ -1,15 +1,14 @@
using System.Diagnostics;
-using JsonApiDotNetCore.Middleware;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.Formatters;
-using Microsoft.Net.Http.Headers;
namespace JsonApiDotNetCore.OpenApi.Swashbuckle;
+///
+/// Determines the Content-Type used in OpenAPI documents for request bodies of JSON:API endpoints.
+///
internal sealed class JsonApiRequestFormatMetadataProvider : IInputFormatter, IApiRequestFormatMetadataProvider
{
- private static readonly string DefaultMediaType = JsonApiMediaType.Default.ToString();
-
///
public bool CanRead(InputFormatterContext context)
{
@@ -23,20 +22,10 @@ public Task ReadAsync(InputFormatterContext context)
}
///
- public IReadOnlyList GetSupportedContentTypes(string contentType, Type objectType)
+ public IReadOnlyList GetSupportedContentTypes(string? contentType, Type objectType)
{
- ArgumentException.ThrowIfNullOrEmpty(contentType);
ArgumentNullException.ThrowIfNull(objectType);
- if (JsonApiSchemaFacts.IsRequestBodySchemaType(objectType) && MediaTypeHeaderValue.TryParse(contentType, out MediaTypeHeaderValue? headerValue) &&
- headerValue.MediaType.Equals(DefaultMediaType, StringComparison.OrdinalIgnoreCase))
- {
- return new MediaTypeCollection
- {
- headerValue
- };
- }
-
- return [];
+ return OpenApiContentTypeProvider.Instance.GetRequestContentTypes(objectType);
}
}
diff --git a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiSchemaFacts.cs b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiSchemaFacts.cs
index bf91aed0e7..87e8ae24fd 100644
--- a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiSchemaFacts.cs
+++ b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiSchemaFacts.cs
@@ -7,14 +7,24 @@ namespace JsonApiDotNetCore.OpenApi.Swashbuckle;
internal static class JsonApiSchemaFacts
{
- private static readonly Type[] RequestBodySchemaTypes =
+ private static readonly Type[] RequestDocumentSchemaOpenTypes =
[
typeof(CreateRequestDocument<>),
typeof(UpdateRequestDocument<>),
typeof(ToOneInRequest<>),
typeof(NullableToOneInRequest<>),
- typeof(ToManyInRequest<>),
- typeof(OperationsRequestDocument)
+ typeof(ToManyInRequest<>)
+ ];
+
+ private static readonly Type[] ResponseDocumentSchemaOpenTypes =
+ [
+ typeof(CollectionResponseDocument<>),
+ typeof(PrimaryResponseDocument<>),
+ typeof(SecondaryResponseDocument<>),
+ typeof(NullableSecondaryResponseDocument<>),
+ typeof(IdentifierResponseDocument<>),
+ typeof(NullableIdentifierResponseDocument<>),
+ typeof(IdentifierCollectionResponseDocument<>)
];
private static readonly Type[] SchemaTypesHavingNullableDataProperty =
@@ -32,14 +42,26 @@ internal static class JsonApiSchemaFacts
typeof(NullableToOneInResponse<>)
];
- public static bool IsRequestBodySchemaType(Type schemaType)
+ public static bool IsRequestDocumentSchemaType(Type schemaType)
+ {
+ ArgumentNullException.ThrowIfNull(schemaType);
+
+ Type lookupType = schemaType.ConstructedToOpenType();
+ return RequestDocumentSchemaOpenTypes.Contains(lookupType);
+ }
+
+ public static bool IsResponseDocumentSchemaType(Type schemaType)
{
+ ArgumentNullException.ThrowIfNull(schemaType);
+
Type lookupType = schemaType.ConstructedToOpenType();
- return RequestBodySchemaTypes.Contains(lookupType);
+ return ResponseDocumentSchemaOpenTypes.Contains(lookupType);
}
public static bool HasNullableDataProperty(Type schemaType)
{
+ ArgumentNullException.ThrowIfNull(schemaType);
+
// Swashbuckle infers non-nullable because our Data properties are [Required].
Type lookupType = schemaType.ConstructedToOpenType();
@@ -48,6 +70,8 @@ public static bool HasNullableDataProperty(Type schemaType)
public static bool IsRelationshipInResponseType(Type schemaType)
{
+ ArgumentNullException.ThrowIfNull(schemaType);
+
Type lookupType = schemaType.ConstructedToOpenType();
return RelationshipInResponseSchemaTypes.Contains(lookupType);
}
diff --git a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/OpenApiContentTypeProvider.cs b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/OpenApiContentTypeProvider.cs
new file mode 100644
index 0000000000..00b5a5f175
--- /dev/null
+++ b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/OpenApiContentTypeProvider.cs
@@ -0,0 +1,56 @@
+using JsonApiDotNetCore.OpenApi.Swashbuckle.JsonApiObjects.Documents;
+
+namespace JsonApiDotNetCore.OpenApi.Swashbuckle;
+
+///
+/// Determines the Content-Type used in OpenAPI documents for request/response bodies of JSON:API endpoints.
+///
+internal sealed class OpenApiContentTypeProvider
+{
+ public static OpenApiContentTypeProvider Instance { get; } = new();
+
+ private OpenApiContentTypeProvider()
+ {
+ }
+
+ public IReadOnlyList GetRequestContentTypes(Type documentType)
+ {
+ ArgumentNullException.ThrowIfNull(documentType);
+
+ // Don't return multiple media types, see https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/1729#issuecomment-2972032608.
+
+ if (documentType == typeof(OperationsRequestDocument))
+ {
+ return [OpenApiMediaTypes.RelaxedAtomicOperationsWithRelaxedOpenApi.ToString()];
+ }
+
+ if (JsonApiSchemaFacts.IsRequestDocumentSchemaType(documentType))
+ {
+ return [OpenApiMediaTypes.RelaxedOpenApi.ToString()];
+ }
+
+ return [];
+ }
+
+ public IReadOnlyList GetResponseContentTypes(Type? documentType)
+ {
+ // Don't return multiple media types, see https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/1729#issuecomment-2972032608.
+
+ if (documentType == typeof(OperationsResponseDocument))
+ {
+ return [OpenApiMediaTypes.RelaxedAtomicOperationsWithRelaxedOpenApi.ToString()];
+ }
+
+ if (documentType == typeof(ErrorResponseDocument))
+ {
+ return [OpenApiMediaTypes.RelaxedOpenApi.ToString()];
+ }
+
+ if (documentType != null && JsonApiSchemaFacts.IsResponseDocumentSchemaType(documentType))
+ {
+ return [OpenApiMediaTypes.RelaxedOpenApi.ToString()];
+ }
+
+ return [];
+ }
+}
diff --git a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/OpenApiEndpointConvention.cs b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/OpenApiEndpointConvention.cs
deleted file mode 100644
index 75649b85a8..0000000000
--- a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/OpenApiEndpointConvention.cs
+++ /dev/null
@@ -1,348 +0,0 @@
-using System.Net;
-using System.Reflection;
-using JsonApiDotNetCore.Configuration;
-using JsonApiDotNetCore.Controllers;
-using JsonApiDotNetCore.Middleware;
-using JsonApiDotNetCore.OpenApi.Swashbuckle.JsonApiMetadata;
-using JsonApiDotNetCore.OpenApi.Swashbuckle.JsonApiObjects.Documents;
-using JsonApiDotNetCore.Resources.Annotations;
-using Microsoft.AspNetCore.Mvc;
-using Microsoft.AspNetCore.Mvc.ApplicationModels;
-
-namespace JsonApiDotNetCore.OpenApi.Swashbuckle;
-
-///
-/// Sets metadata on controllers for OpenAPI documentation generation by Swagger. Only targets JsonApiDotNetCore controllers.
-///
-internal sealed class OpenApiEndpointConvention : IActionModelConvention
-{
- private readonly IControllerResourceMapping _controllerResourceMapping;
- private readonly IJsonApiOptions _options;
-
- public OpenApiEndpointConvention(IControllerResourceMapping controllerResourceMapping, IJsonApiOptions options)
- {
- ArgumentNullException.ThrowIfNull(controllerResourceMapping);
- ArgumentNullException.ThrowIfNull(options);
-
- _controllerResourceMapping = controllerResourceMapping;
- _options = options;
- }
-
- public void Apply(ActionModel action)
- {
- ArgumentNullException.ThrowIfNull(action);
-
- JsonApiEndpointWrapper endpoint = JsonApiEndpointWrapper.FromActionModel(action);
-
- if (endpoint.IsUnknown)
- {
- // Not a JSON:API controller, or a non-standard action method in a JSON:API controller.
- // None of these are yet implemented, so hide them to avoid downstream crashes.
- action.ApiExplorer.IsVisible = false;
- return;
- }
-
- ResourceType? resourceType = _controllerResourceMapping.GetResourceTypeForController(action.Controller.ControllerType);
-
- if (ShouldSuppressEndpoint(endpoint, resourceType))
- {
- action.ApiExplorer.IsVisible = false;
- return;
- }
-
- SetResponseMetadata(action, endpoint, resourceType);
- SetRequestMetadata(action, endpoint);
- }
-
- private bool ShouldSuppressEndpoint(JsonApiEndpointWrapper endpoint, ResourceType? resourceType)
- {
- if (resourceType == null)
- {
- return false;
- }
-
- if (!IsEndpointAvailable(endpoint.Value, resourceType))
- {
- return true;
- }
-
- if (IsSecondaryOrRelationshipEndpoint(endpoint.Value))
- {
- if (resourceType.Relationships.Count == 0)
- {
- return true;
- }
-
- if (endpoint.Value is JsonApiEndpoints.DeleteRelationship or JsonApiEndpoints.PostRelationship)
- {
- return !resourceType.Relationships.OfType().Any();
- }
- }
-
- return false;
- }
-
- private static bool IsEndpointAvailable(JsonApiEndpoints endpoint, ResourceType resourceType)
- {
- JsonApiEndpoints availableEndpoints = GetGeneratedControllerEndpoints(resourceType);
-
- if (availableEndpoints == JsonApiEndpoints.None)
- {
- // Auto-generated controllers are disabled, so we can't know what to hide.
- // It is assumed that a handwritten JSON:API controller only provides action methods for what it supports.
- // To accomplish that, derive from BaseJsonApiController instead of JsonApiController.
- return true;
- }
-
- // For an overridden JSON:API action method in a partial class to show up, it's flag must be turned on in [Resource].
- // Otherwise, it is considered to be an action method that throws because the endpoint is unavailable.
- return IncludesEndpoint(endpoint, availableEndpoints);
- }
-
- private static bool IncludesEndpoint(JsonApiEndpoints endpoint, JsonApiEndpoints availableEndpoints)
- {
- bool? isIncluded = null;
-
- if (endpoint == JsonApiEndpoints.GetCollection)
- {
- isIncluded = availableEndpoints.HasFlag(JsonApiEndpoints.GetCollection);
- }
- else if (endpoint == JsonApiEndpoints.GetSingle)
- {
- isIncluded = availableEndpoints.HasFlag(JsonApiEndpoints.GetSingle);
- }
- else if (endpoint == JsonApiEndpoints.GetSecondary)
- {
- isIncluded = availableEndpoints.HasFlag(JsonApiEndpoints.GetSecondary);
- }
- else if (endpoint == JsonApiEndpoints.GetRelationship)
- {
- isIncluded = availableEndpoints.HasFlag(JsonApiEndpoints.GetRelationship);
- }
- else if (endpoint == JsonApiEndpoints.Post)
- {
- isIncluded = availableEndpoints.HasFlag(JsonApiEndpoints.Post);
- }
- else if (endpoint == JsonApiEndpoints.PostRelationship)
- {
- isIncluded = availableEndpoints.HasFlag(JsonApiEndpoints.PostRelationship);
- }
- else if (endpoint == JsonApiEndpoints.Patch)
- {
- isIncluded = availableEndpoints.HasFlag(JsonApiEndpoints.Patch);
- }
- else if (endpoint == JsonApiEndpoints.PatchRelationship)
- {
- isIncluded = availableEndpoints.HasFlag(JsonApiEndpoints.PatchRelationship);
- }
- else if (endpoint == JsonApiEndpoints.Delete)
- {
- isIncluded = availableEndpoints.HasFlag(JsonApiEndpoints.Delete);
- }
- else if (endpoint == JsonApiEndpoints.DeleteRelationship)
- {
- isIncluded = availableEndpoints.HasFlag(JsonApiEndpoints.DeleteRelationship);
- }
-
- ConsistencyGuard.ThrowIf(isIncluded == null);
- return isIncluded.Value;
- }
-
- private static JsonApiEndpoints GetGeneratedControllerEndpoints(ResourceType resourceType)
- {
- var resourceAttribute = resourceType.ClrType.GetCustomAttribute();
- return resourceAttribute?.GenerateControllerEndpoints ?? JsonApiEndpoints.None;
- }
-
- private static bool IsSecondaryOrRelationshipEndpoint(JsonApiEndpoints endpoint)
- {
- return endpoint is JsonApiEndpoints.GetSecondary or JsonApiEndpoints.GetRelationship or JsonApiEndpoints.PostRelationship or
- JsonApiEndpoints.PatchRelationship or JsonApiEndpoints.DeleteRelationship;
- }
-
- private void SetResponseMetadata(ActionModel action, JsonApiEndpointWrapper endpoint, ResourceType? resourceType)
- {
- JsonApiMediaType mediaType = GetMediaTypeForEndpoint(endpoint);
- action.Filters.Add(new ProducesAttribute(mediaType.ToString()));
-
- foreach (HttpStatusCode statusCode in GetSuccessStatusCodesForEndpoint(endpoint))
- {
- // The return type is set later by JsonApiActionDescriptorCollectionProvider.
- action.Filters.Add(new ProducesResponseTypeAttribute((int)statusCode));
- }
-
- foreach (HttpStatusCode statusCode in GetErrorStatusCodesForEndpoint(endpoint, resourceType))
- {
- action.Filters.Add(new ProducesResponseTypeAttribute(typeof(ErrorResponseDocument), (int)statusCode));
- }
- }
-
- private JsonApiMediaType GetMediaTypeForEndpoint(JsonApiEndpointWrapper endpoint)
- {
- return endpoint.IsAtomicOperationsEndpoint ? OpenApiMediaTypes.RelaxedAtomicOperationsWithRelaxedOpenApi : OpenApiMediaTypes.RelaxedOpenApi;
- }
-
- private static HttpStatusCode[] GetSuccessStatusCodesForEndpoint(JsonApiEndpointWrapper endpoint)
- {
- if (endpoint.IsAtomicOperationsEndpoint)
- {
- return
- [
- HttpStatusCode.OK,
- HttpStatusCode.NoContent
- ];
- }
-
- HttpStatusCode[]? statusCodes = null;
-
- if (endpoint.Value is JsonApiEndpoints.GetCollection or JsonApiEndpoints.GetSingle or JsonApiEndpoints.GetSecondary or JsonApiEndpoints.GetRelationship)
- {
- statusCodes =
- [
- HttpStatusCode.OK,
- HttpStatusCode.NotModified
- ];
- }
- else if (endpoint.Value == JsonApiEndpoints.Post)
- {
- statusCodes =
- [
- HttpStatusCode.Created,
- HttpStatusCode.NoContent
- ];
- }
- else if (endpoint.Value == JsonApiEndpoints.Patch)
- {
- statusCodes =
- [
- HttpStatusCode.OK,
- HttpStatusCode.NoContent
- ];
- }
- else if (endpoint.Value is JsonApiEndpoints.Delete or JsonApiEndpoints.PostRelationship or JsonApiEndpoints.PatchRelationship or
- JsonApiEndpoints.DeleteRelationship)
- {
- statusCodes = [HttpStatusCode.NoContent];
- }
-
- ConsistencyGuard.ThrowIf(statusCodes == null);
- return statusCodes;
- }
-
- private HttpStatusCode[] GetErrorStatusCodesForEndpoint(JsonApiEndpointWrapper endpoint, ResourceType? resourceType)
- {
- if (endpoint.IsAtomicOperationsEndpoint)
- {
- return
- [
- HttpStatusCode.BadRequest,
- HttpStatusCode.Forbidden,
- HttpStatusCode.NotFound,
- HttpStatusCode.Conflict,
- HttpStatusCode.UnprocessableEntity
- ];
- }
-
- // Condition doesn't apply to atomic operations, because Forbidden is also used when an operation is not accessible.
- ClientIdGenerationMode clientIdGeneration = resourceType?.ClientIdGeneration ?? _options.ClientIdGeneration;
-
- HttpStatusCode[]? statusCodes = null;
-
- if (endpoint.Value == JsonApiEndpoints.GetCollection)
- {
- statusCodes = [HttpStatusCode.BadRequest];
- }
- else if (endpoint.Value is JsonApiEndpoints.GetSingle or JsonApiEndpoints.GetSecondary or JsonApiEndpoints.GetRelationship)
- {
- statusCodes =
- [
- HttpStatusCode.BadRequest,
- HttpStatusCode.NotFound
- ];
- }
- else if (endpoint.Value == JsonApiEndpoints.Post && clientIdGeneration == ClientIdGenerationMode.Forbidden)
- {
- statusCodes =
- [
- HttpStatusCode.BadRequest,
- HttpStatusCode.Forbidden,
- HttpStatusCode.NotFound,
- HttpStatusCode.Conflict,
- HttpStatusCode.UnprocessableEntity
- ];
- }
- else if (endpoint.Value is JsonApiEndpoints.Post or JsonApiEndpoints.Patch)
- {
- statusCodes =
- [
- HttpStatusCode.BadRequest,
- HttpStatusCode.NotFound,
- HttpStatusCode.Conflict,
- HttpStatusCode.UnprocessableEntity
- ];
- }
- else if (endpoint.Value == JsonApiEndpoints.Delete)
- {
- statusCodes = [HttpStatusCode.NotFound];
- }
- else if (endpoint.Value is JsonApiEndpoints.PostRelationship or JsonApiEndpoints.PatchRelationship or JsonApiEndpoints.DeleteRelationship)
- {
- statusCodes =
- [
- HttpStatusCode.BadRequest,
- HttpStatusCode.NotFound,
- HttpStatusCode.Conflict,
- HttpStatusCode.UnprocessableEntity
- ];
- }
-
- ConsistencyGuard.ThrowIf(statusCodes == null);
- return statusCodes;
- }
-
- private void SetRequestMetadata(ActionModel action, JsonApiEndpointWrapper endpoint)
- {
- if (RequiresRequestBody(endpoint))
- {
- JsonApiMediaType mediaType = GetMediaTypeForEndpoint(endpoint);
- action.Filters.Add(new ConsumesAttribute(mediaType.ToString()));
- }
- }
-
- private static bool RequiresRequestBody(JsonApiEndpointWrapper endpoint)
- {
- return endpoint.IsAtomicOperationsEndpoint || endpoint.Value is JsonApiEndpoints.Post or JsonApiEndpoints.Patch or JsonApiEndpoints.PostRelationship or
- JsonApiEndpoints.PatchRelationship or JsonApiEndpoints.DeleteRelationship;
- }
-
- private sealed class JsonApiEndpointWrapper
- {
- private static readonly JsonApiEndpointWrapper AtomicOperations = new(true, JsonApiEndpoints.None);
-
- public bool IsAtomicOperationsEndpoint { get; }
- public JsonApiEndpoints Value { get; }
- public bool IsUnknown => !IsAtomicOperationsEndpoint && Value == JsonApiEndpoints.None;
-
- private JsonApiEndpointWrapper(bool isAtomicOperationsEndpoint, JsonApiEndpoints value)
- {
- IsAtomicOperationsEndpoint = isAtomicOperationsEndpoint;
- Value = value;
- }
-
- public static JsonApiEndpointWrapper FromActionModel(ActionModel actionModel)
- {
- if (EndpointResolver.Instance.IsAtomicOperationsController(actionModel.ActionMethod))
- {
- return AtomicOperations;
- }
-
- JsonApiEndpoints endpoint = EndpointResolver.Instance.GetEndpoint(actionModel.ActionMethod);
- return new JsonApiEndpointWrapper(false, endpoint);
- }
-
- public override string ToString()
- {
- return IsAtomicOperationsEndpoint ? "PostOperations" : Value.ToString();
- }
- }
-}
diff --git a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/OpenApiOperationIdSelector.cs b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/OpenApiOperationIdSelector.cs
index ed11481e27..c59470f371 100644
--- a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/OpenApiOperationIdSelector.cs
+++ b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/OpenApiOperationIdSelector.cs
@@ -1,13 +1,14 @@
-using System.Reflection;
using System.Text.Json;
using Humanizer;
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Middleware;
+using JsonApiDotNetCore.OpenApi.Swashbuckle.JsonApiMetadata.ActionMethods;
using JsonApiDotNetCore.OpenApi.Swashbuckle.JsonApiObjects.Documents;
using JsonApiDotNetCore.OpenApi.Swashbuckle.JsonApiObjects.Relationships;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.Controllers;
+using Swashbuckle.AspNetCore.SwaggerGen;
namespace JsonApiDotNetCore.OpenApi.Swashbuckle;
@@ -37,6 +38,8 @@ internal sealed class OpenApiOperationIdSelector
[typeof(OperationsRequestDocument)] = AtomicOperationsIdTemplate
};
+ private static readonly Func DefaultOperationIdSelector = new SwaggerGeneratorOptions().OperationIdSelector;
+
private readonly IControllerResourceMapping _controllerResourceMapping;
private readonly IJsonApiOptions _options;
@@ -53,34 +56,47 @@ public string GetOpenApiOperationId(ApiDescription endpoint)
{
ArgumentNullException.ThrowIfNull(endpoint);
- MethodInfo actionMethod = endpoint.ActionDescriptor.GetActionMethod();
- ResourceType? primaryResourceType = _controllerResourceMapping.GetResourceTypeForController(actionMethod.ReflectedType);
+ var actionMethod = OpenApiActionMethod.Create(endpoint.ActionDescriptor);
- string template = GetTemplate(endpoint);
- return ApplyTemplate(template, primaryResourceType, endpoint);
+ switch (actionMethod)
+ {
+ case BuiltinJsonApiActionMethod builtinJsonApiActionMethod:
+ {
+ ResourceType? primaryResourceType = _controllerResourceMapping.GetResourceTypeForController(builtinJsonApiActionMethod.ControllerType);
+
+ string template = GetTemplate(endpoint);
+ return ApplyTemplate(template, primaryResourceType, endpoint);
+ }
+ default:
+ {
+ return DefaultOperationIdSelector(endpoint);
+ }
+ }
}
private static string GetTemplate(ApiDescription endpoint)
{
- Type bodyType = GetBodyType(endpoint);
- ConsistencyGuard.ThrowIf(!SchemaOpenTypeToOpenApiOperationIdTemplateMap.TryGetValue(bodyType, out string? template));
+ Type documentType = GetDocumentType(endpoint);
+ ConsistencyGuard.ThrowIf(!SchemaOpenTypeToOpenApiOperationIdTemplateMap.TryGetValue(documentType, out string? template));
return template;
}
- private static Type GetBodyType(ApiDescription endpoint)
+ private static Type GetDocumentType(ApiDescription endpoint)
{
- var producesResponseTypeAttribute = endpoint.ActionDescriptor.GetFilterMetadata();
+ ProducesResponseTypeAttribute? producesResponseTypeAttribute = endpoint.ActionDescriptor.FilterDescriptors
+ .Select(filterDescriptor => filterDescriptor.Filter).OfType().FirstOrDefault();
+
ConsistencyGuard.ThrowIf(producesResponseTypeAttribute == null);
ControllerParameterDescriptor? requestBodyDescriptor = endpoint.ActionDescriptor.GetBodyParameterDescriptor();
- Type bodyType = (requestBodyDescriptor?.ParameterType ?? producesResponseTypeAttribute.Type).ConstructedToOpenType();
+ Type documentOpenType = (requestBodyDescriptor?.ParameterType ?? producesResponseTypeAttribute.Type).ConstructedToOpenType();
- if (bodyType == typeof(CollectionResponseDocument<>) && endpoint.ParameterDescriptions.Count > 0)
+ if (documentOpenType == typeof(CollectionResponseDocument<>) && endpoint.ParameterDescriptions.Count > 0)
{
- bodyType = typeof(SecondaryResponseDocument<>);
+ documentOpenType = typeof(SecondaryResponseDocument<>);
}
- return bodyType;
+ return documentOpenType;
}
private string ApplyTemplate(string openApiOperationIdTemplate, ResourceType? resourceType, ApiDescription endpoint)
diff --git a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/SchemaGenerators/Documents/ResourceOrRelationshipDocumentSchemaGenerator.cs b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/SchemaGenerators/Documents/ResourceOrRelationshipDocumentSchemaGenerator.cs
index 767f0d0143..0762e5b8c1 100644
--- a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/SchemaGenerators/Documents/ResourceOrRelationshipDocumentSchemaGenerator.cs
+++ b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/SchemaGenerators/Documents/ResourceOrRelationshipDocumentSchemaGenerator.cs
@@ -1,6 +1,4 @@
using JsonApiDotNetCore.Configuration;
-using JsonApiDotNetCore.OpenApi.Swashbuckle.JsonApiObjects.Documents;
-using JsonApiDotNetCore.OpenApi.Swashbuckle.JsonApiObjects.Relationships;
using JsonApiDotNetCore.OpenApi.Swashbuckle.SchemaGenerators.Components;
using JsonApiDotNetCore.OpenApi.Swashbuckle.SwaggerComponents;
using Microsoft.OpenApi.Models;
@@ -13,26 +11,6 @@ namespace JsonApiDotNetCore.OpenApi.Swashbuckle.SchemaGenerators.Documents;
///
internal sealed class ResourceOrRelationshipDocumentSchemaGenerator : DocumentSchemaGenerator
{
- private static readonly Type[] RequestDocumentSchemaTypes =
- [
- typeof(CreateRequestDocument<>),
- typeof(UpdateRequestDocument<>),
- typeof(ToOneInRequest<>),
- typeof(NullableToOneInRequest<>),
- typeof(ToManyInRequest<>)
- ];
-
- private static readonly Type[] ResponseDocumentSchemaTypes =
- [
- typeof(CollectionResponseDocument<>),
- typeof(PrimaryResponseDocument<>),
- typeof(SecondaryResponseDocument<>),
- typeof(NullableSecondaryResponseDocument<>),
- typeof(IdentifierResponseDocument<>),
- typeof(NullableIdentifierResponseDocument<>),
- typeof(IdentifierCollectionResponseDocument<>)
- ];
-
private readonly SchemaGenerator _defaultSchemaGenerator;
private readonly DataContainerSchemaGenerator _dataContainerSchemaGenerator;
private readonly IResourceGraph _resourceGraph;
@@ -53,8 +31,7 @@ public ResourceOrRelationshipDocumentSchemaGenerator(SchemaGenerationTracer sche
public override bool CanGenerate(Type schemaType)
{
- Type schemaOpenType = schemaType.ConstructedToOpenType();
- return RequestDocumentSchemaTypes.Contains(schemaOpenType) || ResponseDocumentSchemaTypes.Contains(schemaOpenType);
+ return JsonApiSchemaFacts.IsRequestDocumentSchemaType(schemaType) || JsonApiSchemaFacts.IsResponseDocumentSchemaType(schemaType);
}
protected override OpenApiSchema GenerateDocumentSchema(Type schemaType, SchemaRepository schemaRepository)
@@ -63,7 +40,7 @@ protected override OpenApiSchema GenerateDocumentSchema(Type schemaType, SchemaR
ArgumentNullException.ThrowIfNull(schemaRepository);
var resourceSchemaType = ResourceSchemaType.Create(schemaType, _resourceGraph);
- bool isRequestSchema = RequestDocumentSchemaTypes.Contains(resourceSchemaType.SchemaOpenType);
+ bool isRequestSchema = JsonApiSchemaFacts.IsRequestDocumentSchemaType(resourceSchemaType.SchemaOpenType);
_ = _dataContainerSchemaGenerator.GenerateSchema(schemaType, resourceSchemaType.ResourceType, isRequestSchema, !isRequestSchema, schemaRepository);
diff --git a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/SchemaGenerators/GenerationCacheSchemaGenerator.cs b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/SchemaGenerators/GenerationCacheSchemaGenerator.cs
index beba632ebf..beb10f94bf 100644
--- a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/SchemaGenerators/GenerationCacheSchemaGenerator.cs
+++ b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/SchemaGenerators/GenerationCacheSchemaGenerator.cs
@@ -1,5 +1,4 @@
-using System.Reflection;
-using JsonApiDotNetCore.OpenApi.Swashbuckle.JsonApiMetadata;
+using JsonApiDotNetCore.OpenApi.Swashbuckle.JsonApiMetadata.ActionMethods;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.OpenApi.Any;
@@ -18,18 +17,14 @@ internal sealed class GenerationCacheSchemaGenerator
private readonly SchemaGenerationTracer _schemaGenerationTracer;
private readonly IActionDescriptorCollectionProvider _defaultProvider;
- private readonly JsonApiEndpointMetadataProvider _jsonApiEndpointMetadataProvider;
- public GenerationCacheSchemaGenerator(SchemaGenerationTracer schemaGenerationTracer, IActionDescriptorCollectionProvider defaultProvider,
- JsonApiEndpointMetadataProvider jsonApiEndpointMetadataProvider)
+ public GenerationCacheSchemaGenerator(SchemaGenerationTracer schemaGenerationTracer, IActionDescriptorCollectionProvider defaultProvider)
{
ArgumentNullException.ThrowIfNull(schemaGenerationTracer);
ArgumentNullException.ThrowIfNull(defaultProvider);
- ArgumentNullException.ThrowIfNull(jsonApiEndpointMetadataProvider);
_schemaGenerationTracer = schemaGenerationTracer;
_defaultProvider = defaultProvider;
- _jsonApiEndpointMetadataProvider = jsonApiEndpointMetadataProvider;
}
public bool HasAtomicOperationsEndpoint(SchemaRepository schemaRepository)
@@ -74,15 +69,13 @@ private OpenApiSchema GenerateFullSchema(SchemaRepository schemaRepository)
private bool EvaluateHasAtomicOperationsEndpoint()
{
- IEnumerable actionDescriptors =
- _defaultProvider.ActionDescriptors.Items.Where(JsonApiActionDescriptorCollectionProvider.IsVisibleJsonApiEndpoint);
+ IEnumerable descriptors = _defaultProvider.ActionDescriptors.Items.Where(JsonApiActionDescriptorCollectionProvider.IsVisibleEndpoint);
- foreach (ActionDescriptor actionDescriptor in actionDescriptors)
+ foreach (ActionDescriptor descriptor in descriptors)
{
- MethodInfo actionMethod = actionDescriptor.GetActionMethod();
- JsonApiEndpointMetadataContainer endpointMetadataContainer = _jsonApiEndpointMetadataProvider.Get(actionMethod);
+ var actionMethod = OpenApiActionMethod.Create(descriptor);
- if (endpointMetadataContainer.RequestMetadata is AtomicOperationsRequestMetadata)
+ if (actionMethod is AtomicOperationsActionMethod)
{
return true;
}
diff --git a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/SchemaGenerators/JsonApiSchemaGenerator.cs b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/SchemaGenerators/JsonApiSchemaGenerator.cs
index 19d94eb48e..c7d50572c3 100644
--- a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/SchemaGenerators/JsonApiSchemaGenerator.cs
+++ b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/SchemaGenerators/JsonApiSchemaGenerator.cs
@@ -10,14 +10,18 @@ namespace JsonApiDotNetCore.OpenApi.Swashbuckle.SchemaGenerators;
internal sealed class JsonApiSchemaGenerator : ISchemaGenerator
{
+ private readonly SchemaGenerator _defaultSchemaGenerator;
private readonly ResourceIdSchemaGenerator _resourceIdSchemaGenerator;
private readonly DocumentSchemaGenerator[] _documentSchemaGenerators;
- public JsonApiSchemaGenerator(ResourceIdSchemaGenerator resourceIdSchemaGenerator, IEnumerable documentSchemaGenerators)
+ public JsonApiSchemaGenerator(SchemaGenerator defaultSchemaGenerator, ResourceIdSchemaGenerator resourceIdSchemaGenerator,
+ IEnumerable documentSchemaGenerators)
{
+ ArgumentNullException.ThrowIfNull(defaultSchemaGenerator);
ArgumentNullException.ThrowIfNull(resourceIdSchemaGenerator);
ArgumentNullException.ThrowIfNull(documentSchemaGenerators);
+ _defaultSchemaGenerator = defaultSchemaGenerator;
_resourceIdSchemaGenerator = resourceIdSchemaGenerator;
_documentSchemaGenerators = documentSchemaGenerators as DocumentSchemaGenerator[] ?? documentSchemaGenerators.ToArray();
}
@@ -33,17 +37,23 @@ public OpenApiSchema GenerateSchema(Type schemaType, SchemaRepository schemaRepo
return _resourceIdSchemaGenerator.GenerateSchema(schemaType, schemaRepository);
}
- DocumentSchemaGenerator schemaGenerator = GetDocumentSchemaGenerator(schemaType);
- OpenApiSchema referenceSchema = schemaGenerator.GenerateSchema(schemaType, schemaRepository);
+ DocumentSchemaGenerator? schemaGenerator = GetDocumentSchemaGenerator(schemaType);
- if (memberInfo != null || parameterInfo != null)
+ if (schemaGenerator != null)
{
- // For unknown reasons, Swashbuckle chooses to wrap request bodies in allOf, but not response bodies.
- // We just replicate that behavior here. See https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/861#issuecomment-1373631712.
- referenceSchema = referenceSchema.WrapInExtendedSchema();
+ OpenApiSchema referenceSchema = schemaGenerator.GenerateSchema(schemaType, schemaRepository);
+
+ if (memberInfo != null || parameterInfo != null)
+ {
+ // For unknown reasons, Swashbuckle chooses to wrap request bodies in allOf, but not response bodies.
+ // We just replicate that behavior here. See https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/861#issuecomment-1373631712.
+ referenceSchema = referenceSchema.WrapInExtendedSchema();
+ }
+
+ return referenceSchema;
}
- return referenceSchema;
+ return _defaultSchemaGenerator.GenerateSchema(schemaType, schemaRepository, memberInfo, parameterInfo, routeInfo);
}
private static bool IsJsonApiParameter(ParameterInfo parameter)
@@ -51,20 +61,16 @@ private static bool IsJsonApiParameter(ParameterInfo parameter)
return parameter.Member.DeclaringType != null && parameter.Member.DeclaringType.IsAssignableTo(typeof(CoreJsonApiController));
}
- private DocumentSchemaGenerator GetDocumentSchemaGenerator(Type schemaType)
+ private DocumentSchemaGenerator? GetDocumentSchemaGenerator(Type schemaType)
{
- DocumentSchemaGenerator? generator = null;
-
foreach (DocumentSchemaGenerator documentSchemaGenerator in _documentSchemaGenerators)
{
if (documentSchemaGenerator.CanGenerate(schemaType))
{
- generator = documentSchemaGenerator;
- break;
+ return documentSchemaGenerator;
}
}
- ConsistencyGuard.ThrowIf(generator == null);
- return generator;
+ return null;
}
}
diff --git a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/ServiceCollectionExtensions.cs b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/ServiceCollectionExtensions.cs
index 6872115289..10d4ec50cb 100644
--- a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/ServiceCollectionExtensions.cs
+++ b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/ServiceCollectionExtensions.cs
@@ -28,14 +28,9 @@ public static void AddOpenApiForJsonApi(this IServiceCollection services, Action
AddCustomApiExplorer(services);
AddCustomSwaggerComponents(services);
- AddSwaggerGenerator(services);
+ AddSwaggerGenerator(services, configureSwaggerGenOptions);
- if (configureSwaggerGenOptions != null)
- {
- services.Configure(configureSwaggerGenOptions);
- }
-
- services.AddSingleton();
+ services.Replace(ServiceDescriptor.Singleton());
services.TryAddSingleton();
services.Replace(ServiceDescriptor.Singleton());
}
@@ -50,7 +45,6 @@ private static void AssertHasJsonApi(IServiceCollection services)
private static void AddCustomApiExplorer(IServiceCollection services)
{
- services.TryAddSingleton();
services.TryAddSingleton();
services.TryAddSingleton();
services.TryAddSingleton();
@@ -73,10 +67,14 @@ private static void AddCustomApiExplorer(IServiceCollection services)
private static void AddApiExplorer(IServiceCollection services)
{
- // The code below was copied from the implementation of MvcApiExplorerMvcCoreBuilderExtensions.AddApiExplorer(),
+ // This call was copied from the implementation of MvcApiExplorerMvcCoreBuilderExtensions.AddApiExplorer(),
// so we don't need to take IMvcCoreBuilder as an input parameter.
-
services.TryAddEnumerable(ServiceDescriptor.Transient());
+
+ // This call ensures that Minimal API endpoints appear in Swashbuckle.
+ // Don't be fooled to believe this call is redundant: When running from Visual Studio, a startup filter is injected
+ // that also calls this. But that doesn't happen when running from the command line or from an integration test.
+ services.AddEndpointsApiExplorer();
}
private static void AddCustomSwaggerComponents(IServiceCollection services)
@@ -87,14 +85,14 @@ private static void AddCustomSwaggerComponents(IServiceCollection services)
services.TryAddSingleton();
}
- private static void AddSwaggerGenerator(IServiceCollection services)
+ private static void AddSwaggerGenerator(IServiceCollection services, Action? configureSwaggerGenOptions)
{
AddSchemaGenerators(services);
services.TryAddSingleton();
- services.AddSingleton();
+ services.Replace(ServiceDescriptor.Singleton());
- services.AddSwaggerGen();
+ services.AddSwaggerGen(configureSwaggerGenOptions);
services.AddSingleton, ConfigureSwaggerGenOptions>();
}
diff --git a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/SwaggerComponents/DocumentationOpenApiOperationFilter.cs b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/SwaggerComponents/DocumentationOpenApiOperationFilter.cs
index 1b5c0d5f4c..0d6f89b1b1 100644
--- a/src/JsonApiDotNetCore.OpenApi.Swashbuckle/SwaggerComponents/DocumentationOpenApiOperationFilter.cs
+++ b/src/JsonApiDotNetCore.OpenApi.Swashbuckle/SwaggerComponents/DocumentationOpenApiOperationFilter.cs
@@ -78,7 +78,10 @@ public void Apply(OpenApiOperation operation, OperationFilterContext context)
if (hasHeadVerb)
{
- operation.Responses.Clear();
+ foreach (OpenApiResponse response in operation.Responses.Values)
+ {
+ response.Content.Clear();
+ }
}
MethodInfo actionMethod = context.ApiDescription.ActionDescriptor.GetActionMethod();
diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/AtomicLoggingTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/AtomicLoggingTests.cs
index 1662f9387f..8f6b405d3b 100644
--- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/AtomicLoggingTests.cs
+++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/AtomicLoggingTests.cs
@@ -19,13 +19,12 @@ public AtomicLoggingTests(IntegrationTestContext();
- testContext.ConfigureLogging(options =>
+ testContext.ConfigureLogging(builder =>
{
var loggerProvider = new CapturingLoggerProvider(LogLevel.Information);
- options.AddProvider(loggerProvider);
- options.SetMinimumLevel(LogLevel.Information);
+ builder.AddProvider(loggerProvider);
- options.Services.AddSingleton(loggerProvider);
+ builder.Services.AddSingleton(loggerProvider);
});
testContext.ConfigureServices(services => services.AddSingleton());
diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/AtomicTraceLoggingTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/AtomicTraceLoggingTests.cs
index 0ea01e7d33..def482f732 100644
--- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/AtomicTraceLoggingTests.cs
+++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/AtomicTraceLoggingTests.cs
@@ -19,15 +19,15 @@ public AtomicTraceLoggingTests(IntegrationTestContext();
- testContext.ConfigureLogging(options =>
+ testContext.ConfigureLogging(builder =>
{
var loggerProvider = new CapturingLoggerProvider((category, level) =>
level == LogLevel.Trace && category.StartsWith("JsonApiDotNetCore.", StringComparison.Ordinal));
- options.AddProvider(loggerProvider);
- options.SetMinimumLevel(LogLevel.Trace);
+ builder.AddProvider(loggerProvider);
+ builder.SetMinimumLevel(LogLevel.Trace);
- options.Services.AddSingleton(loggerProvider);
+ builder.Services.AddSingleton(loggerProvider);
});
}
diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Authorization/Scopes/ScopesStartup.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Authorization/Scopes/ScopesStartup.cs
index 0f6eaf4391..8eaa59a1f6 100644
--- a/test/JsonApiDotNetCoreTests/IntegrationTests/Authorization/Scopes/ScopesStartup.cs
+++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Authorization/Scopes/ScopesStartup.cs
@@ -13,6 +13,6 @@ public override void ConfigureServices(IServiceCollection services)
{
IMvcCoreBuilder mvcBuilder = services.AddMvcCore(options => options.Filters.Add(int.MaxValue));
- services.AddJsonApi(SetJsonApiOptions, mvcBuilder: mvcBuilder);
+ services.AddJsonApi(ConfigureJsonApiOptions, mvcBuilder: mvcBuilder);
}
}
diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/CustomRoutes/ApiControllerAttributeLogTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/CustomRoutes/ApiControllerAttributeLogTests.cs
index 132fa446b1..160bc3bc92 100644
--- a/test/JsonApiDotNetCoreTests/IntegrationTests/CustomRoutes/ApiControllerAttributeLogTests.cs
+++ b/test/JsonApiDotNetCoreTests/IntegrationTests/CustomRoutes/ApiControllerAttributeLogTests.cs
@@ -16,11 +16,11 @@ public ApiControllerAttributeLogTests()
_loggerProvider = new CapturingLoggerProvider(LogLevel.Warning);
- ConfigureLogging(options =>
+ ConfigureLogging(builder =>
{
- options.AddProvider(_loggerProvider);
+ builder.AddProvider(_loggerProvider);
- options.Services.AddSingleton(_loggerProvider);
+ builder.Services.AddSingleton(_loggerProvider);
});
}
diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ExceptionHandling/ExceptionHandlerTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ExceptionHandling/ExceptionHandlerTests.cs
index 35a5364938..80155d1150 100644
--- a/test/JsonApiDotNetCoreTests/IntegrationTests/ExceptionHandling/ExceptionHandlerTests.cs
+++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ExceptionHandling/ExceptionHandlerTests.cs
@@ -22,12 +22,12 @@ public ExceptionHandlerTests(IntegrationTestContext();
testContext.UseController();
- testContext.ConfigureLogging(options =>
+ testContext.ConfigureLogging(builder =>
{
var loggerProvider = new CapturingLoggerProvider(LogLevel.Warning);
- options.AddProvider(loggerProvider);
+ builder.AddProvider(loggerProvider);
- options.Services.AddSingleton(loggerProvider);
+ builder.Services.AddSingleton(loggerProvider);
});
testContext.ConfigureServices(services =>
diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/HostingInIIS/HostingStartup.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/HostingInIIS/HostingStartup.cs
index 1653cd5e96..68ec695d04 100644
--- a/test/JsonApiDotNetCoreTests/IntegrationTests/HostingInIIS/HostingStartup.cs
+++ b/test/JsonApiDotNetCoreTests/IntegrationTests/HostingInIIS/HostingStartup.cs
@@ -9,9 +9,9 @@ namespace JsonApiDotNetCoreTests.IntegrationTests.HostingInIIS;
public sealed class HostingStartup : TestableStartup
where TDbContext : TestableDbContext
{
- protected override void SetJsonApiOptions(JsonApiOptions options)
+ protected override void ConfigureJsonApiOptions(JsonApiOptions options)
{
- base.SetJsonApiOptions(options);
+ base.ConfigureJsonApiOptions(options);
options.Namespace = "public-api";
options.IncludeTotalResourceCount = true;
diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Logging/LoggingTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Logging/LoggingTests.cs
index 4d35403f9e..2283c67656 100644
--- a/test/JsonApiDotNetCoreTests/IntegrationTests/Logging/LoggingTests.cs
+++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Logging/LoggingTests.cs
@@ -20,15 +20,15 @@ public LoggingTests(IntegrationTestContext, Lo
testContext.UseController();
testContext.UseController();
- testContext.ConfigureLogging(options =>
+ testContext.ConfigureLogging(builder =>
{
var loggerProvider = new CapturingLoggerProvider((category, level) =>
level >= LogLevel.Trace && category.StartsWith("JsonApiDotNetCore.", StringComparison.Ordinal));
- options.AddProvider(loggerProvider);
- options.SetMinimumLevel(LogLevel.Trace);
+ builder.AddProvider(loggerProvider);
+ builder.SetMinimumLevel(LogLevel.Trace);
- options.Services.AddSingleton(loggerProvider);
+ builder.Services.AddSingleton(loggerProvider);
});
}
diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/NamingConventions/KebabCasingConventionStartup.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/NamingConventions/KebabCasingConventionStartup.cs
index 6321943718..d581bea56c 100644
--- a/test/JsonApiDotNetCoreTests/IntegrationTests/NamingConventions/KebabCasingConventionStartup.cs
+++ b/test/JsonApiDotNetCoreTests/IntegrationTests/NamingConventions/KebabCasingConventionStartup.cs
@@ -9,9 +9,9 @@ namespace JsonApiDotNetCoreTests.IntegrationTests.NamingConventions;
public sealed class KebabCasingConventionStartup : TestableStartup
where TDbContext : TestableDbContext
{
- protected override void SetJsonApiOptions(JsonApiOptions options)
+ protected override void ConfigureJsonApiOptions(JsonApiOptions options)
{
- base.SetJsonApiOptions(options);
+ base.ConfigureJsonApiOptions(options);
options.Namespace = "public-api";
options.UseRelativeLinks = true;
diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/NamingConventions/PascalCasingConventionStartup.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/NamingConventions/PascalCasingConventionStartup.cs
index dad29067cf..32996706c0 100644
--- a/test/JsonApiDotNetCoreTests/IntegrationTests/NamingConventions/PascalCasingConventionStartup.cs
+++ b/test/JsonApiDotNetCoreTests/IntegrationTests/NamingConventions/PascalCasingConventionStartup.cs
@@ -8,9 +8,9 @@ namespace JsonApiDotNetCoreTests.IntegrationTests.NamingConventions;
public sealed class PascalCasingConventionStartup : TestableStartup
where TDbContext : TestableDbContext
{
- protected override void SetJsonApiOptions(JsonApiOptions options)
+ protected override void ConfigureJsonApiOptions(JsonApiOptions options)
{
- base.SetJsonApiOptions(options);
+ base.ConfigureJsonApiOptions(options);
options.Namespace = "PublicApi";
options.UseRelativeLinks = true;
diff --git a/test/JsonApiDotNetCoreTests/Startups/AbsoluteLinksInApiNamespaceStartup.cs b/test/JsonApiDotNetCoreTests/Startups/AbsoluteLinksInApiNamespaceStartup.cs
index 5e165653b3..9ee6281d9a 100644
--- a/test/JsonApiDotNetCoreTests/Startups/AbsoluteLinksInApiNamespaceStartup.cs
+++ b/test/JsonApiDotNetCoreTests/Startups/AbsoluteLinksInApiNamespaceStartup.cs
@@ -8,9 +8,9 @@ namespace JsonApiDotNetCoreTests.Startups;
public sealed class AbsoluteLinksInApiNamespaceStartup : TestableStartup
where TDbContext : TestableDbContext
{
- protected override void SetJsonApiOptions(JsonApiOptions options)
+ protected override void ConfigureJsonApiOptions(JsonApiOptions options)
{
- base.SetJsonApiOptions(options);
+ base.ConfigureJsonApiOptions(options);
options.Namespace = "api";
options.UseRelativeLinks = false;
diff --git a/test/JsonApiDotNetCoreTests/Startups/AbsoluteLinksNoNamespaceStartup.cs b/test/JsonApiDotNetCoreTests/Startups/AbsoluteLinksNoNamespaceStartup.cs
index c8234ed695..41882df043 100644
--- a/test/JsonApiDotNetCoreTests/Startups/AbsoluteLinksNoNamespaceStartup.cs
+++ b/test/JsonApiDotNetCoreTests/Startups/AbsoluteLinksNoNamespaceStartup.cs
@@ -8,9 +8,9 @@ namespace JsonApiDotNetCoreTests.Startups;
public sealed class AbsoluteLinksNoNamespaceStartup : TestableStartup
where TDbContext : TestableDbContext
{
- protected override void SetJsonApiOptions(JsonApiOptions options)
+ protected override void ConfigureJsonApiOptions(JsonApiOptions options)
{
- base.SetJsonApiOptions(options);
+ base.ConfigureJsonApiOptions(options);
options.Namespace = null;
options.UseRelativeLinks = false;
diff --git a/test/JsonApiDotNetCoreTests/Startups/NoModelStateValidationStartup.cs b/test/JsonApiDotNetCoreTests/Startups/NoModelStateValidationStartup.cs
index ac6f9c82f1..75c7b46ab2 100644
--- a/test/JsonApiDotNetCoreTests/Startups/NoModelStateValidationStartup.cs
+++ b/test/JsonApiDotNetCoreTests/Startups/NoModelStateValidationStartup.cs
@@ -8,9 +8,9 @@ namespace JsonApiDotNetCoreTests.Startups;
public sealed class NoModelStateValidationStartup : TestableStartup
where TDbContext : TestableDbContext
{
- protected override void SetJsonApiOptions(JsonApiOptions options)
+ protected override void ConfigureJsonApiOptions(JsonApiOptions options)
{
- base.SetJsonApiOptions(options);
+ base.ConfigureJsonApiOptions(options);
options.ValidateModelState = false;
}
diff --git a/test/JsonApiDotNetCoreTests/Startups/RelativeLinksInApiNamespaceStartup.cs b/test/JsonApiDotNetCoreTests/Startups/RelativeLinksInApiNamespaceStartup.cs
index 5fdfd20048..091bdff810 100644
--- a/test/JsonApiDotNetCoreTests/Startups/RelativeLinksInApiNamespaceStartup.cs
+++ b/test/JsonApiDotNetCoreTests/Startups/RelativeLinksInApiNamespaceStartup.cs
@@ -8,9 +8,9 @@ namespace JsonApiDotNetCoreTests.Startups;
public sealed class RelativeLinksInApiNamespaceStartup : TestableStartup
where TDbContext : TestableDbContext
{
- protected override void SetJsonApiOptions(JsonApiOptions options)
+ protected override void ConfigureJsonApiOptions(JsonApiOptions options)
{
- base.SetJsonApiOptions(options);
+ base.ConfigureJsonApiOptions(options);
options.Namespace = "api";
options.UseRelativeLinks = true;
diff --git a/test/JsonApiDotNetCoreTests/Startups/RelativeLinksNoNamespaceStartup.cs b/test/JsonApiDotNetCoreTests/Startups/RelativeLinksNoNamespaceStartup.cs
index 99ae80d207..7871d3ab1a 100644
--- a/test/JsonApiDotNetCoreTests/Startups/RelativeLinksNoNamespaceStartup.cs
+++ b/test/JsonApiDotNetCoreTests/Startups/RelativeLinksNoNamespaceStartup.cs
@@ -8,9 +8,9 @@ namespace JsonApiDotNetCoreTests.Startups;
public sealed class RelativeLinksNoNamespaceStartup : TestableStartup
where TDbContext : TestableDbContext
{
- protected override void SetJsonApiOptions(JsonApiOptions options)
+ protected override void ConfigureJsonApiOptions(JsonApiOptions options)
{
- base.SetJsonApiOptions(options);
+ base.ConfigureJsonApiOptions(options);
options.Namespace = null;
options.UseRelativeLinks = true;
diff --git a/test/JsonApiDotNetCoreTests/UnitTests/Serialization/Extensions/ResourceObjectConverterTests.cs b/test/JsonApiDotNetCoreTests/UnitTests/Serialization/Extensions/ResourceObjectConverterTests.cs
index 2b0557bcdf..6b5e4e9497 100644
--- a/test/JsonApiDotNetCoreTests/UnitTests/Serialization/Extensions/ResourceObjectConverterTests.cs
+++ b/test/JsonApiDotNetCoreTests/UnitTests/Serialization/Extensions/ResourceObjectConverterTests.cs
@@ -188,7 +188,7 @@ public void Throws_for_request_body_with_extension_in_attributes_when_extension_
};
// Assert
- JsonApiException? exception = action.Should().ThrowExactly().WithInnerExceptionExactly().Which;
+ JsonApiException exception = action.Should().ThrowExactly().WithInnerExceptionExactly().Which;
exception.StackTrace.Should().Contain(nameof(ExtensionAwareResourceObjectConverter));
exception.Errors.Should().HaveCount(1);
@@ -229,7 +229,7 @@ public void Throws_for_request_body_with_extension_in_relationships_when_extensi
};
// Assert
- JsonApiException? exception = action.Should().ThrowExactly().WithInnerExceptionExactly().Which;
+ JsonApiException exception = action.Should().ThrowExactly().WithInnerExceptionExactly().Which;
exception.StackTrace.Should().Contain(nameof(ExtensionAwareResourceObjectConverter));
exception.Errors.Should().HaveCount(1);
diff --git a/test/JsonApiDotNetCoreTests/UnitTests/Serialization/Extensions/SourcePointerInExceptionTests.cs b/test/JsonApiDotNetCoreTests/UnitTests/Serialization/Extensions/SourcePointerInExceptionTests.cs
index 1638c0b0d2..089a01d21e 100644
--- a/test/JsonApiDotNetCoreTests/UnitTests/Serialization/Extensions/SourcePointerInExceptionTests.cs
+++ b/test/JsonApiDotNetCoreTests/UnitTests/Serialization/Extensions/SourcePointerInExceptionTests.cs
@@ -43,7 +43,7 @@ public async Task Adds_source_pointer_to_JsonApiException_thrown_from_JsonConver
Func action = async () => await reader.ReadAsync(httpContext.Request);
// Assert
- JsonApiException? exception = (await action.Should().ThrowExactlyAsync()).Which;
+ JsonApiException exception = (await action.Should().ThrowExactlyAsync()).Which;
exception.StackTrace.Should().Contain(nameof(ThrowingResourceObjectConverter));
exception.Errors.Should().HaveCount(1);
@@ -71,7 +71,7 @@ public async Task Makes_source_pointer_absolute_in_JsonApiException_thrown_from_
Func action = async () => await reader.ReadAsync(httpContext.Request);
// Assert
- JsonApiException? exception = (await action.Should().ThrowExactlyAsync()).Which;
+ JsonApiException exception = (await action.Should().ThrowExactlyAsync()).Which;
exception.StackTrace.Should().Contain(nameof(ThrowingResourceObjectConverter));
exception.Errors.Should().HaveCount(1);
diff --git a/test/OpenApiKiotaEndToEndTests/AdditionalPropertiesTests.cs b/test/OpenApiKiotaEndToEndTests/AdditionalPropertiesTests.cs
index d3f8d75d43..400232d94a 100644
--- a/test/OpenApiKiotaEndToEndTests/AdditionalPropertiesTests.cs
+++ b/test/OpenApiKiotaEndToEndTests/AdditionalPropertiesTests.cs
@@ -8,6 +8,12 @@ public sealed class AdditionalPropertiesTests
{
private static readonly string GeneratedCodeDirectory = $"{Path.DirectorySeparatorChar}GeneratedCode{Path.DirectorySeparatorChar}";
+ private static readonly HashSet Whitelist = new([
+ "Meta.cs",
+ "HttpValidationProblemDetails.cs",
+ "HttpValidationProblemDetails_errors.cs"
+ ], StringComparer.OrdinalIgnoreCase);
+
[Fact]
public async Task Additional_properties_are_only_allowed_in_meta()
{
@@ -19,13 +25,17 @@ public async Task Additional_properties_are_only_allowed_in_meta()
RecurseSubdirectories = true
}))
{
- if (path.Contains(GeneratedCodeDirectory, StringComparison.OrdinalIgnoreCase) &&
- !string.Equals(Path.GetFileName(path), "Meta.cs", StringComparison.OrdinalIgnoreCase))
+ if (path.Contains(GeneratedCodeDirectory, StringComparison.OrdinalIgnoreCase))
{
- string content = await File.ReadAllTextAsync(path);
- bool containsAdditionalData = content.Contains("public IDictionary AdditionalData");
+ string fileName = Path.GetFileName(path);
+
+ if (!Whitelist.Contains(fileName))
+ {
+ string content = await File.ReadAllTextAsync(path);
+ bool containsAdditionalData = content.Contains("public IDictionary AdditionalData");
- containsAdditionalData.Should().BeFalse($"file '{path}' should not contain AdditionalData");
+ containsAdditionalData.Should().BeFalse($"file '{path}' should not contain AdditionalData");
+ }
}
}
}
diff --git a/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/CupOfCoffees/CupOfCoffeesRequestBuilder.cs b/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/CupOfCoffees/CupOfCoffeesRequestBuilder.cs
new file mode 100644
index 0000000000..2f2b0d0fb2
--- /dev/null
+++ b/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/CupOfCoffees/CupOfCoffeesRequestBuilder.cs
@@ -0,0 +1,140 @@
+//
+#nullable enable
+#pragma warning disable CS8625
+#pragma warning disable CS0618
+using Microsoft.Kiota.Abstractions.Extensions;
+using Microsoft.Kiota.Abstractions.Serialization;
+using Microsoft.Kiota.Abstractions;
+using OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.CupOfCoffees.Item;
+using OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Models;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading.Tasks;
+using System.Threading;
+using System;
+namespace OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.CupOfCoffees
+{
+ ///
+ /// Builds and executes requests for operations under \cupOfCoffees
+ ///
+ [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
+ public partial class CupOfCoffeesRequestBuilder : BaseRequestBuilder
+ {
+ /// Gets an item from the OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.cupOfCoffees.item collection
+ /// The identifier of the cupOfCoffee to delete.
+ /// A
+ public global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.CupOfCoffees.Item.CupOfCoffeesItemRequestBuilder this[string position]
+ {
+ get
+ {
+ var urlTplParams = new Dictionary(PathParameters);
+ urlTplParams.Add("id", position);
+ return new global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.CupOfCoffees.Item.CupOfCoffeesItemRequestBuilder(urlTplParams, RequestAdapter);
+ }
+ }
+
+ ///
+ /// Instantiates a new and sets the default values.
+ ///
+ /// Path parameters for the request
+ /// The request adapter to use to execute the requests.
+ public CupOfCoffeesRequestBuilder(Dictionary pathParameters, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/cupOfCoffees{?query*}", pathParameters)
+ {
+ }
+
+ ///
+ /// Instantiates a new and sets the default values.
+ ///
+ /// The raw URL to use for the request builder.
+ /// The request adapter to use to execute the requests.
+ public CupOfCoffeesRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/cupOfCoffees{?query*}", rawUrl)
+ {
+ }
+
+ ///
+ /// Retrieves a collection of cupOfCoffees.
+ ///
+ /// A
+ /// Cancellation token to use when cancelling requests
+ /// Configuration for the request such as headers, query parameters, and middleware options.
+ /// When receiving a 400 status code
+ public async Task GetAsync(Action>? requestConfiguration = default, CancellationToken cancellationToken = default)
+ {
+ var requestInfo = ToGetRequestInformation(requestConfiguration);
+ var errorMapping = new Dictionary>
+ {
+ { "400", global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Models.ErrorResponseDocument.CreateFromDiscriminatorValue },
+ };
+ return await RequestAdapter.SendAsync(requestInfo, global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Models.CupOfCoffeeCollectionResponseDocument.CreateFromDiscriminatorValue, errorMapping, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ /// Compare the returned ETag HTTP header with an earlier one to determine if the response has changed since it was fetched.
+ ///
+ /// Cancellation token to use when cancelling requests
+ /// Configuration for the request such as headers, query parameters, and middleware options.
+ public async Task HeadAsync(Action>? requestConfiguration = default, CancellationToken cancellationToken = default)
+ {
+ var requestInfo = ToHeadRequestInformation(requestConfiguration);
+ await RequestAdapter.SendNoContentAsync(requestInfo, default, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ /// Retrieves a collection of cupOfCoffees.
+ ///
+ /// A
+ /// Configuration for the request such as headers, query parameters, and middleware options.
+ public RequestInformation ToGetRequestInformation(Action>? requestConfiguration = default)
+ {
+ var requestInfo = new RequestInformation(Method.GET, UrlTemplate, PathParameters);
+ requestInfo.Configure(requestConfiguration);
+ requestInfo.Headers.TryAdd("Accept", "application/vnd.api+json;ext=openapi");
+ return requestInfo;
+ }
+
+ ///
+ /// Compare the returned ETag HTTP header with an earlier one to determine if the response has changed since it was fetched.
+ ///
+ /// A
+ /// Configuration for the request such as headers, query parameters, and middleware options.
+ public RequestInformation ToHeadRequestInformation(Action>? requestConfiguration = default)
+ {
+ var requestInfo = new RequestInformation(Method.HEAD, UrlTemplate, PathParameters);
+ requestInfo.Configure(requestConfiguration);
+ return requestInfo;
+ }
+
+ ///
+ /// Returns a request builder with the provided arbitrary URL. Using this method means any other path or query parameters are ignored.
+ ///
+ /// A
+ /// The raw URL to use for the request builder.
+ public global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.CupOfCoffees.CupOfCoffeesRequestBuilder WithUrl(string rawUrl)
+ {
+ return new global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.CupOfCoffees.CupOfCoffeesRequestBuilder(rawUrl, RequestAdapter);
+ }
+
+ ///
+ /// Retrieves a collection of cupOfCoffees.
+ ///
+ [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
+ public partial class CupOfCoffeesRequestBuilderGetQueryParameters
+ {
+ /// For syntax, see the documentation for the [`include`](https://www.jsonapi.net/usage/reading/including-relationships.html)/[`filter`](https://www.jsonapi.net/usage/reading/filtering.html)/[`sort`](https://www.jsonapi.net/usage/reading/sorting.html)/[`page`](https://www.jsonapi.net/usage/reading/pagination.html)/[`fields`](https://www.jsonapi.net/usage/reading/sparse-fieldset-selection.html) query string parameters.
+ [QueryParameter("query")]
+ public string? Query { get; set; }
+ }
+
+ ///
+ /// Compare the returned ETag HTTP header with an earlier one to determine if the response has changed since it was fetched.
+ ///
+ [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
+ public partial class CupOfCoffeesRequestBuilderHeadQueryParameters
+ {
+ /// For syntax, see the documentation for the [`include`](https://www.jsonapi.net/usage/reading/including-relationships.html)/[`filter`](https://www.jsonapi.net/usage/reading/filtering.html)/[`sort`](https://www.jsonapi.net/usage/reading/sorting.html)/[`page`](https://www.jsonapi.net/usage/reading/pagination.html)/[`fields`](https://www.jsonapi.net/usage/reading/sparse-fieldset-selection.html) query string parameters.
+ [QueryParameter("query")]
+ public string? Query { get; set; }
+ }
+ }
+}
+#pragma warning restore CS0618
diff --git a/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/CupOfCoffees/Item/CupOfCoffeesItemRequestBuilder.cs b/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/CupOfCoffees/Item/CupOfCoffeesItemRequestBuilder.cs
new file mode 100644
index 0000000000..7b43a0ac42
--- /dev/null
+++ b/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/CupOfCoffees/Item/CupOfCoffeesItemRequestBuilder.cs
@@ -0,0 +1,80 @@
+//
+#nullable enable
+#pragma warning disable CS8625
+#pragma warning disable CS0618
+using Microsoft.Kiota.Abstractions.Extensions;
+using Microsoft.Kiota.Abstractions.Serialization;
+using Microsoft.Kiota.Abstractions;
+using OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Models;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading.Tasks;
+using System.Threading;
+using System;
+namespace OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.CupOfCoffees.Item
+{
+ ///
+ /// Builds and executes requests for operations under \cupOfCoffees\{id}
+ ///
+ [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
+ public partial class CupOfCoffeesItemRequestBuilder : BaseRequestBuilder
+ {
+ ///
+ /// Instantiates a new and sets the default values.
+ ///
+ /// Path parameters for the request
+ /// The request adapter to use to execute the requests.
+ public CupOfCoffeesItemRequestBuilder(Dictionary pathParameters, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/cupOfCoffees/{id}", pathParameters)
+ {
+ }
+
+ ///
+ /// Instantiates a new and sets the default values.
+ ///
+ /// The raw URL to use for the request builder.
+ /// The request adapter to use to execute the requests.
+ public CupOfCoffeesItemRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/cupOfCoffees/{id}", rawUrl)
+ {
+ }
+
+ ///
+ /// Deletes an existing cupOfCoffee by its identifier.
+ ///
+ /// Cancellation token to use when cancelling requests
+ /// Configuration for the request such as headers, query parameters, and middleware options.
+ /// When receiving a 404 status code
+ public async Task DeleteAsync(Action>? requestConfiguration = default, CancellationToken cancellationToken = default)
+ {
+ var requestInfo = ToDeleteRequestInformation(requestConfiguration);
+ var errorMapping = new Dictionary>
+ {
+ { "404", global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Models.ErrorResponseDocument.CreateFromDiscriminatorValue },
+ };
+ await RequestAdapter.SendNoContentAsync(requestInfo, errorMapping, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ /// Deletes an existing cupOfCoffee by its identifier.
+ ///
+ /// A
+ /// Configuration for the request such as headers, query parameters, and middleware options.
+ public RequestInformation ToDeleteRequestInformation(Action>? requestConfiguration = default)
+ {
+ var requestInfo = new RequestInformation(Method.DELETE, UrlTemplate, PathParameters);
+ requestInfo.Configure(requestConfiguration);
+ requestInfo.Headers.TryAdd("Accept", "application/vnd.api+json;ext=openapi");
+ return requestInfo;
+ }
+
+ ///
+ /// Returns a request builder with the provided arbitrary URL. Using this method means any other path or query parameters are ignored.
+ ///
+ /// A
+ /// The raw URL to use for the request builder.
+ public global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.CupOfCoffees.Item.CupOfCoffeesItemRequestBuilder WithUrl(string rawUrl)
+ {
+ return new global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.CupOfCoffees.Item.CupOfCoffeesItemRequestBuilder(rawUrl, RequestAdapter);
+ }
+ }
+}
+#pragma warning restore CS0618
diff --git a/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/Emails/EmailsRequestBuilder.cs b/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/Emails/EmailsRequestBuilder.cs
new file mode 100644
index 0000000000..55f9b9449f
--- /dev/null
+++ b/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/Emails/EmailsRequestBuilder.cs
@@ -0,0 +1,52 @@
+//
+#nullable enable
+#pragma warning disable CS8625
+#pragma warning disable CS0618
+using Microsoft.Kiota.Abstractions.Extensions;
+using Microsoft.Kiota.Abstractions;
+using OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Emails.Send;
+using OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Emails.SentSince;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading.Tasks;
+using System;
+namespace OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Emails
+{
+ ///
+ /// Builds and executes requests for operations under \emails
+ ///
+ [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
+ public partial class EmailsRequestBuilder : BaseRequestBuilder
+ {
+ /// The send property
+ public global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Emails.Send.SendRequestBuilder Send
+ {
+ get => new global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Emails.Send.SendRequestBuilder(PathParameters, RequestAdapter);
+ }
+
+ /// The sentSince property
+ public global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Emails.SentSince.SentSinceRequestBuilder SentSince
+ {
+ get => new global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Emails.SentSince.SentSinceRequestBuilder(PathParameters, RequestAdapter);
+ }
+
+ ///
+ /// Instantiates a new and sets the default values.
+ ///
+ /// Path parameters for the request
+ /// The request adapter to use to execute the requests.
+ public EmailsRequestBuilder(Dictionary pathParameters, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/emails", pathParameters)
+ {
+ }
+
+ ///
+ /// Instantiates a new and sets the default values.
+ ///
+ /// The raw URL to use for the request builder.
+ /// The request adapter to use to execute the requests.
+ public EmailsRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/emails", rawUrl)
+ {
+ }
+ }
+}
+#pragma warning restore CS0618
diff --git a/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/Emails/Send/SendRequestBuilder.cs b/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/Emails/Send/SendRequestBuilder.cs
new file mode 100644
index 0000000000..488e238f02
--- /dev/null
+++ b/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/Emails/Send/SendRequestBuilder.cs
@@ -0,0 +1,86 @@
+//
+#nullable enable
+#pragma warning disable CS8625
+#pragma warning disable CS0618
+using Microsoft.Kiota.Abstractions.Extensions;
+using Microsoft.Kiota.Abstractions.Serialization;
+using Microsoft.Kiota.Abstractions;
+using OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Models;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading.Tasks;
+using System.Threading;
+using System;
+namespace OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Emails.Send
+{
+ ///
+ /// Builds and executes requests for operations under \emails\send
+ ///
+ [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
+ public partial class SendRequestBuilder : BaseRequestBuilder
+ {
+ ///
+ /// Instantiates a new and sets the default values.
+ ///
+ /// Path parameters for the request
+ /// The request adapter to use to execute the requests.
+ public SendRequestBuilder(Dictionary pathParameters, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/emails/send", pathParameters)
+ {
+ }
+
+ ///
+ /// Instantiates a new and sets the default values.
+ ///
+ /// The raw URL to use for the request builder.
+ /// The request adapter to use to execute the requests.
+ public SendRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/emails/send", rawUrl)
+ {
+ }
+
+ ///
+ /// Sends an email to the specified recipient.
+ ///
+ /// A
+ /// The email to send.
+ /// Cancellation token to use when cancelling requests
+ /// Configuration for the request such as headers, query parameters, and middleware options.
+ /// When receiving a 400 status code
+ public async Task PostAsync(global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Models.Email body, Action>? requestConfiguration = default, CancellationToken cancellationToken = default)
+ {
+ _ = body ?? throw new ArgumentNullException(nameof(body));
+ var requestInfo = ToPostRequestInformation(body, requestConfiguration);
+ var errorMapping = new Dictionary>
+ {
+ { "400", global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Models.HttpValidationProblemDetails.CreateFromDiscriminatorValue },
+ };
+ return await RequestAdapter.SendPrimitiveAsync(requestInfo, errorMapping, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ /// Sends an email to the specified recipient.
+ ///
+ /// A
+ /// The email to send.
+ /// Configuration for the request such as headers, query parameters, and middleware options.
+ public RequestInformation ToPostRequestInformation(global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Models.Email body, Action>? requestConfiguration = default)
+ {
+ _ = body ?? throw new ArgumentNullException(nameof(body));
+ var requestInfo = new RequestInformation(Method.POST, UrlTemplate, PathParameters);
+ requestInfo.Configure(requestConfiguration);
+ requestInfo.Headers.TryAdd("Accept", "application/problem+json");
+ requestInfo.SetContentFromParsable(RequestAdapter, "application/json", body);
+ return requestInfo;
+ }
+
+ ///
+ /// Returns a request builder with the provided arbitrary URL. Using this method means any other path or query parameters are ignored.
+ ///
+ /// A
+ /// The raw URL to use for the request builder.
+ public global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Emails.Send.SendRequestBuilder WithUrl(string rawUrl)
+ {
+ return new global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Emails.Send.SendRequestBuilder(rawUrl, RequestAdapter);
+ }
+ }
+}
+#pragma warning restore CS0618
diff --git a/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/Emails/SentSince/SentSinceRequestBuilder.cs b/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/Emails/SentSince/SentSinceRequestBuilder.cs
new file mode 100644
index 0000000000..17d0f7dfaf
--- /dev/null
+++ b/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/Emails/SentSince/SentSinceRequestBuilder.cs
@@ -0,0 +1,128 @@
+//
+#nullable enable
+#pragma warning disable CS8625
+#pragma warning disable CS0618
+using Microsoft.Kiota.Abstractions.Extensions;
+using Microsoft.Kiota.Abstractions.Serialization;
+using Microsoft.Kiota.Abstractions;
+using OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Models;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading.Tasks;
+using System.Threading;
+using System;
+namespace OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Emails.SentSince
+{
+ ///
+ /// Builds and executes requests for operations under \emails\sent-since
+ ///
+ [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
+ public partial class SentSinceRequestBuilder : BaseRequestBuilder
+ {
+ ///
+ /// Instantiates a new and sets the default values.
+ ///
+ /// Path parameters for the request
+ /// The request adapter to use to execute the requests.
+ public SentSinceRequestBuilder(Dictionary pathParameters, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/emails/sent-since?sinceUtc={sinceUtc}", pathParameters)
+ {
+ }
+
+ ///
+ /// Instantiates a new and sets the default values.
+ ///
+ /// The raw URL to use for the request builder.
+ /// The request adapter to use to execute the requests.
+ public SentSinceRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/emails/sent-since?sinceUtc={sinceUtc}", rawUrl)
+ {
+ }
+
+ ///
+ /// Gets all emails sent since the specified date/time.
+ ///
+ /// A List<global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Models.Email>
+ /// Cancellation token to use when cancelling requests
+ /// Configuration for the request such as headers, query parameters, and middleware options.
+ /// When receiving a 400 status code
+ public async Task?> GetAsync(Action>? requestConfiguration = default, CancellationToken cancellationToken = default)
+ {
+ var requestInfo = ToGetRequestInformation(requestConfiguration);
+ var errorMapping = new Dictionary>
+ {
+ { "400", global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Models.HttpValidationProblemDetails.CreateFromDiscriminatorValue },
+ };
+ var collectionResult = await RequestAdapter.SendCollectionAsync(requestInfo, global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Models.Email.CreateFromDiscriminatorValue, errorMapping, cancellationToken).ConfigureAwait(false);
+ return collectionResult?.AsList();
+ }
+
+ ///
+ /// Gets all emails sent since the specified date/time.
+ ///
+ /// A
+ /// Cancellation token to use when cancelling requests
+ /// Configuration for the request such as headers, query parameters, and middleware options.
+ public async Task HeadAsync(Action>? requestConfiguration = default, CancellationToken cancellationToken = default)
+ {
+ var requestInfo = ToHeadRequestInformation(requestConfiguration);
+ return await RequestAdapter.SendPrimitiveAsync(requestInfo, default, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ /// Gets all emails sent since the specified date/time.
+ ///
+ /// A
+ /// Configuration for the request such as headers, query parameters, and middleware options.
+ public RequestInformation ToGetRequestInformation(Action>? requestConfiguration = default)
+ {
+ var requestInfo = new RequestInformation(Method.GET, UrlTemplate, PathParameters);
+ requestInfo.Configure(requestConfiguration);
+ requestInfo.Headers.TryAdd("Accept", "application/json");
+ return requestInfo;
+ }
+
+ ///
+ /// Gets all emails sent since the specified date/time.
+ ///
+ /// A
+ /// Configuration for the request such as headers, query parameters, and middleware options.
+ public RequestInformation ToHeadRequestInformation(Action>? requestConfiguration = default)
+ {
+ var requestInfo = new RequestInformation(Method.HEAD, UrlTemplate, PathParameters);
+ requestInfo.Configure(requestConfiguration);
+ return requestInfo;
+ }
+
+ ///
+ /// Returns a request builder with the provided arbitrary URL. Using this method means any other path or query parameters are ignored.
+ ///
+ /// A
+ /// The raw URL to use for the request builder.
+ public global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Emails.SentSince.SentSinceRequestBuilder WithUrl(string rawUrl)
+ {
+ return new global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Emails.SentSince.SentSinceRequestBuilder(rawUrl, RequestAdapter);
+ }
+
+ ///
+ /// Gets all emails sent since the specified date/time.
+ ///
+ [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
+ public partial class SentSinceRequestBuilderGetQueryParameters
+ {
+ /// The date/time (in UTC) since which the email was sent.
+ [QueryParameter("sinceUtc")]
+ public DateTimeOffset? SinceUtc { get; set; }
+ }
+
+ ///
+ /// Gets all emails sent since the specified date/time.
+ ///
+ [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
+ public partial class SentSinceRequestBuilderHeadQueryParameters
+ {
+ /// The date/time (in UTC) since which the email was sent.
+ [QueryParameter("sinceUtc")]
+ public DateTimeOffset? SinceUtc { get; set; }
+ }
+ }
+}
+#pragma warning restore CS0618
diff --git a/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/FileTransfers/FileTransfersRequestBuilder.cs b/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/FileTransfers/FileTransfersRequestBuilder.cs
new file mode 100644
index 0000000000..1241d1694e
--- /dev/null
+++ b/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/FileTransfers/FileTransfersRequestBuilder.cs
@@ -0,0 +1,156 @@
+//
+#nullable enable
+#pragma warning disable CS8625
+#pragma warning disable CS0618
+using Microsoft.Kiota.Abstractions.Extensions;
+using Microsoft.Kiota.Abstractions.Serialization;
+using Microsoft.Kiota.Abstractions;
+using OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.FileTransfers.Find;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading.Tasks;
+using System.Threading;
+using System;
+namespace OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.FileTransfers
+{
+ ///
+ /// Builds and executes requests for operations under \fileTransfers
+ ///
+ [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
+ public partial class FileTransfersRequestBuilder : BaseRequestBuilder
+ {
+ /// The find property
+ public global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.FileTransfers.Find.FindRequestBuilder Find
+ {
+ get => new global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.FileTransfers.Find.FindRequestBuilder(PathParameters, RequestAdapter);
+ }
+
+ ///
+ /// Instantiates a new and sets the default values.
+ ///
+ /// Path parameters for the request
+ /// The request adapter to use to execute the requests.
+ public FileTransfersRequestBuilder(Dictionary pathParameters, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/fileTransfers{?fileName*}", pathParameters)
+ {
+ }
+
+ ///
+ /// Instantiates a new and sets the default values.
+ ///
+ /// The raw URL to use for the request builder.
+ /// The request adapter to use to execute the requests.
+ public FileTransfersRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/fileTransfers{?fileName*}", rawUrl)
+ {
+ }
+
+ ///
+ /// Downloads the file with the specified name. Returns HTTP 404 if not found.
+ ///
+ /// A
+ /// Cancellation token to use when cancelling requests
+ /// Configuration for the request such as headers, query parameters, and middleware options.
+ public async Task GetAsync(Action>? requestConfiguration = default, CancellationToken cancellationToken = default)
+ {
+ var requestInfo = ToGetRequestInformation(requestConfiguration);
+ return await RequestAdapter.SendPrimitiveAsync(requestInfo, default, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ /// Downloads the file with the specified name. Returns HTTP 404 if not found.
+ ///
+ /// A
+ /// Cancellation token to use when cancelling requests
+ /// Configuration for the request such as headers, query parameters, and middleware options.
+ public async Task HeadAsync(Action>? requestConfiguration = default, CancellationToken cancellationToken = default)
+ {
+ var requestInfo = ToHeadRequestInformation(requestConfiguration);
+ return await RequestAdapter.SendPrimitiveAsync(requestInfo, default, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ /// Uploads a file. Returns HTTP 400 if the file is empty.
+ ///
+ /// A
+ /// The request body
+ /// Cancellation token to use when cancelling requests
+ /// Configuration for the request such as headers, query parameters, and middleware options.
+ public async Task PostAsync(MultipartBody body, Action>? requestConfiguration = default, CancellationToken cancellationToken = default)
+ {
+ _ = body ?? throw new ArgumentNullException(nameof(body));
+ var requestInfo = ToPostRequestInformation(body, requestConfiguration);
+ return await RequestAdapter.SendPrimitiveAsync(requestInfo, default, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ /// Downloads the file with the specified name. Returns HTTP 404 if not found.
+ ///
+ /// A
+ /// Configuration for the request such as headers, query parameters, and middleware options.
+ public RequestInformation ToGetRequestInformation(Action>? requestConfiguration = default)
+ {
+ var requestInfo = new RequestInformation(Method.GET, UrlTemplate, PathParameters);
+ requestInfo.Configure(requestConfiguration);
+ requestInfo.Headers.TryAdd("Accept", "application/octet-stream");
+ return requestInfo;
+ }
+
+ ///
+ /// Downloads the file with the specified name. Returns HTTP 404 if not found.
+ ///
+ /// A
+ /// Configuration for the request such as headers, query parameters, and middleware options.
+ public RequestInformation ToHeadRequestInformation(Action>? requestConfiguration = default)
+ {
+ var requestInfo = new RequestInformation(Method.HEAD, UrlTemplate, PathParameters);
+ requestInfo.Configure(requestConfiguration);
+ return requestInfo;
+ }
+
+ ///
+ /// Uploads a file. Returns HTTP 400 if the file is empty.
+ ///
+ /// A
+ /// The request body
+ /// Configuration for the request such as headers, query parameters, and middleware options.
+ public RequestInformation ToPostRequestInformation(MultipartBody body, Action>? requestConfiguration = default)
+ {
+ _ = body ?? throw new ArgumentNullException(nameof(body));
+ var requestInfo = new RequestInformation(Method.POST, UrlTemplate, PathParameters);
+ requestInfo.Configure(requestConfiguration);
+ requestInfo.Headers.TryAdd("Accept", "text/plain;q=0.9");
+ requestInfo.SetContentFromParsable(RequestAdapter, "multipart/form-data", body);
+ return requestInfo;
+ }
+
+ ///
+ /// Returns a request builder with the provided arbitrary URL. Using this method means any other path or query parameters are ignored.
+ ///
+ /// A
+ /// The raw URL to use for the request builder.
+ public global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.FileTransfers.FileTransfersRequestBuilder WithUrl(string rawUrl)
+ {
+ return new global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.FileTransfers.FileTransfersRequestBuilder(rawUrl, RequestAdapter);
+ }
+
+ ///
+ /// Downloads the file with the specified name. Returns HTTP 404 if not found.
+ ///
+ [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
+ public partial class FileTransfersRequestBuilderGetQueryParameters
+ {
+ [QueryParameter("fileName")]
+ public string? FileName { get; set; }
+ }
+
+ ///
+ /// Downloads the file with the specified name. Returns HTTP 404 if not found.
+ ///
+ [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
+ public partial class FileTransfersRequestBuilderHeadQueryParameters
+ {
+ [QueryParameter("fileName")]
+ public string? FileName { get; set; }
+ }
+ }
+}
+#pragma warning restore CS0618
diff --git a/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/FileTransfers/Find/FindRequestBuilder.cs b/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/FileTransfers/Find/FindRequestBuilder.cs
new file mode 100644
index 0000000000..421bfe7ed1
--- /dev/null
+++ b/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/FileTransfers/Find/FindRequestBuilder.cs
@@ -0,0 +1,118 @@
+//
+#nullable enable
+#pragma warning disable CS8625
+#pragma warning disable CS0618
+using Microsoft.Kiota.Abstractions.Extensions;
+using Microsoft.Kiota.Abstractions.Serialization;
+using Microsoft.Kiota.Abstractions;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading.Tasks;
+using System.Threading;
+using System;
+namespace OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.FileTransfers.Find
+{
+ ///
+ /// Builds and executes requests for operations under \fileTransfers\find
+ ///
+ [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
+ public partial class FindRequestBuilder : BaseRequestBuilder
+ {
+ ///
+ /// Instantiates a new and sets the default values.
+ ///
+ /// Path parameters for the request
+ /// The request adapter to use to execute the requests.
+ public FindRequestBuilder(Dictionary pathParameters, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/fileTransfers/find{?fileName*}", pathParameters)
+ {
+ }
+
+ ///
+ /// Instantiates a new and sets the default values.
+ ///
+ /// The raw URL to use for the request builder.
+ /// The request adapter to use to execute the requests.
+ public FindRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/fileTransfers/find{?fileName*}", rawUrl)
+ {
+ }
+
+ ///
+ /// Returns whether the specified file is available for download.
+ ///
+ /// A
+ /// Cancellation token to use when cancelling requests
+ /// Configuration for the request such as headers, query parameters, and middleware options.
+ public async Task GetAsync(Action>? requestConfiguration = default, CancellationToken cancellationToken = default)
+ {
+ var requestInfo = ToGetRequestInformation(requestConfiguration);
+ return await RequestAdapter.SendPrimitiveAsync(requestInfo, default, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ /// Returns whether the specified file is available for download.
+ ///
+ /// A
+ /// Cancellation token to use when cancelling requests
+ /// Configuration for the request such as headers, query parameters, and middleware options.
+ public async Task HeadAsync(Action>? requestConfiguration = default, CancellationToken cancellationToken = default)
+ {
+ var requestInfo = ToHeadRequestInformation(requestConfiguration);
+ return await RequestAdapter.SendPrimitiveAsync(requestInfo, default, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ /// Returns whether the specified file is available for download.
+ ///
+ /// A
+ /// Configuration for the request such as headers, query parameters, and middleware options.
+ public RequestInformation ToGetRequestInformation(Action>? requestConfiguration = default)
+ {
+ var requestInfo = new RequestInformation(Method.GET, UrlTemplate, PathParameters);
+ requestInfo.Configure(requestConfiguration);
+ return requestInfo;
+ }
+
+ ///
+ /// Returns whether the specified file is available for download.
+ ///
+ /// A
+ /// Configuration for the request such as headers, query parameters, and middleware options.
+ public RequestInformation ToHeadRequestInformation(Action>? requestConfiguration = default)
+ {
+ var requestInfo = new RequestInformation(Method.HEAD, UrlTemplate, PathParameters);
+ requestInfo.Configure(requestConfiguration);
+ return requestInfo;
+ }
+
+ ///
+ /// Returns a request builder with the provided arbitrary URL. Using this method means any other path or query parameters are ignored.
+ ///
+ /// A
+ /// The raw URL to use for the request builder.
+ public global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.FileTransfers.Find.FindRequestBuilder WithUrl(string rawUrl)
+ {
+ return new global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.FileTransfers.Find.FindRequestBuilder(rawUrl, RequestAdapter);
+ }
+
+ ///
+ /// Returns whether the specified file is available for download.
+ ///
+ [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
+ public partial class FindRequestBuilderGetQueryParameters
+ {
+ [QueryParameter("fileName")]
+ public string? FileName { get; set; }
+ }
+
+ ///
+ /// Returns whether the specified file is available for download.
+ ///
+ [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
+ public partial class FindRequestBuilderHeadQueryParameters
+ {
+ [QueryParameter("fileName")]
+ public string? FileName { get; set; }
+ }
+ }
+}
+#pragma warning restore CS0618
diff --git a/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/MixedControllersClient.cs b/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/MixedControllersClient.cs
new file mode 100644
index 0000000000..1074664525
--- /dev/null
+++ b/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/MixedControllersClient.cs
@@ -0,0 +1,68 @@
+//
+#nullable enable
+#pragma warning disable CS8625
+#pragma warning disable CS0618
+using Microsoft.Kiota.Abstractions.Extensions;
+using Microsoft.Kiota.Abstractions.Store;
+using Microsoft.Kiota.Abstractions;
+using Microsoft.Kiota.Serialization.Form;
+using Microsoft.Kiota.Serialization.Json;
+using Microsoft.Kiota.Serialization.Multipart;
+using Microsoft.Kiota.Serialization.Text;
+using OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.CupOfCoffees;
+using OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Emails;
+using OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.FileTransfers;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading.Tasks;
+using System;
+namespace OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode
+{
+ ///
+ /// The main entry point of the SDK, exposes the configuration and the fluent API.
+ ///
+ [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
+ public partial class MixedControllersClient : BaseRequestBuilder
+ {
+ /// The cupOfCoffees property
+ public global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.CupOfCoffees.CupOfCoffeesRequestBuilder CupOfCoffees
+ {
+ get => new global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.CupOfCoffees.CupOfCoffeesRequestBuilder(PathParameters, RequestAdapter);
+ }
+
+ /// The emails property
+ public global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Emails.EmailsRequestBuilder Emails
+ {
+ get => new global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Emails.EmailsRequestBuilder(PathParameters, RequestAdapter);
+ }
+
+ /// The fileTransfers property
+ public global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.FileTransfers.FileTransfersRequestBuilder FileTransfers
+ {
+ get => new global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.FileTransfers.FileTransfersRequestBuilder(PathParameters, RequestAdapter);
+ }
+
+ ///
+ /// Instantiates a new and sets the default values.
+ ///
+ /// The backing store to use for the models.
+ /// The request adapter to use to execute the requests.
+ public MixedControllersClient(IRequestAdapter requestAdapter, IBackingStoreFactory backingStore = default) : base(requestAdapter, "{+baseurl}", new Dictionary())
+ {
+ ApiClientBuilder.RegisterDefaultSerializer();
+ ApiClientBuilder.RegisterDefaultSerializer();
+ ApiClientBuilder.RegisterDefaultSerializer();
+ ApiClientBuilder.RegisterDefaultSerializer();
+ ApiClientBuilder.RegisterDefaultDeserializer();
+ ApiClientBuilder.RegisterDefaultDeserializer();
+ ApiClientBuilder.RegisterDefaultDeserializer();
+ if (string.IsNullOrEmpty(RequestAdapter.BaseUrl))
+ {
+ RequestAdapter.BaseUrl = "http://localhost";
+ }
+ PathParameters.TryAdd("baseurl", RequestAdapter.BaseUrl);
+ RequestAdapter.EnableBackingStore(backingStore);
+ }
+ }
+}
+#pragma warning restore CS0618
diff --git a/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/Models/AttributesInCupOfCoffeeResponse.cs b/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/Models/AttributesInCupOfCoffeeResponse.cs
new file mode 100644
index 0000000000..74cab86ec0
--- /dev/null
+++ b/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/Models/AttributesInCupOfCoffeeResponse.cs
@@ -0,0 +1,68 @@
+//
+#nullable enable
+#pragma warning disable CS8625
+#pragma warning disable CS0618
+using Microsoft.Kiota.Abstractions.Extensions;
+using Microsoft.Kiota.Abstractions.Serialization;
+using System.Collections.Generic;
+using System.IO;
+using System;
+namespace OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Models
+{
+ [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
+ #pragma warning disable CS1591
+ public partial class AttributesInCupOfCoffeeResponse : global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Models.AttributesInResponse, IParsable
+ #pragma warning restore CS1591
+ {
+ /// The hasMilk property
+ public bool? HasMilk
+ {
+ get { return BackingStore?.Get("hasMilk"); }
+ set { BackingStore?.Set("hasMilk", value); }
+ }
+
+ /// The hasSugar property
+ public bool? HasSugar
+ {
+ get { return BackingStore?.Get("hasSugar"); }
+ set { BackingStore?.Set("hasSugar", value); }
+ }
+
+ ///
+ /// Creates a new instance of the appropriate class based on discriminator value
+ ///
+ /// A
+ /// The parse node to use to read the discriminator value and create the object
+ public static new global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Models.AttributesInCupOfCoffeeResponse CreateFromDiscriminatorValue(IParseNode parseNode)
+ {
+ _ = parseNode ?? throw new ArgumentNullException(nameof(parseNode));
+ return new global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Models.AttributesInCupOfCoffeeResponse();
+ }
+
+ ///
+ /// The deserialization information for the current model
+ ///
+ /// A IDictionary<string, Action<IParseNode>>
+ public override IDictionary> GetFieldDeserializers()
+ {
+ return new Dictionary>(base.GetFieldDeserializers())
+ {
+ { "hasMilk", n => { HasMilk = n.GetBoolValue(); } },
+ { "hasSugar", n => { HasSugar = n.GetBoolValue(); } },
+ };
+ }
+
+ ///
+ /// Serializes information the current object
+ ///
+ /// Serialization writer to use to serialize this model
+ public override void Serialize(ISerializationWriter writer)
+ {
+ _ = writer ?? throw new ArgumentNullException(nameof(writer));
+ base.Serialize(writer);
+ writer.WriteBoolValue("hasMilk", HasMilk);
+ writer.WriteBoolValue("hasSugar", HasSugar);
+ }
+ }
+}
+#pragma warning restore CS0618
diff --git a/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/Models/AttributesInResponse.cs b/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/Models/AttributesInResponse.cs
new file mode 100644
index 0000000000..9532132b93
--- /dev/null
+++ b/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/Models/AttributesInResponse.cs
@@ -0,0 +1,75 @@
+//
+#nullable enable
+#pragma warning disable CS8625
+#pragma warning disable CS0618
+using Microsoft.Kiota.Abstractions.Extensions;
+using Microsoft.Kiota.Abstractions.Serialization;
+using Microsoft.Kiota.Abstractions.Store;
+using System.Collections.Generic;
+using System.IO;
+using System;
+namespace OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Models
+{
+ [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
+ #pragma warning disable CS1591
+ public partial class AttributesInResponse : IBackedModel, IParsable
+ #pragma warning restore CS1591
+ {
+ /// Stores model information.
+ public IBackingStore BackingStore { get; private set; }
+
+ /// The openapiDiscriminator property
+ public global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Models.ResourceType? OpenapiDiscriminator
+ {
+ get { return BackingStore?.Get("openapi:discriminator"); }
+ set { BackingStore?.Set("openapi:discriminator", value); }
+ }
+
+ ///
+ /// Instantiates a new and sets the default values.
+ ///
+ public AttributesInResponse()
+ {
+ BackingStore = BackingStoreFactorySingleton.Instance.CreateBackingStore();
+ }
+
+ ///
+ /// Creates a new instance of the appropriate class based on discriminator value
+ ///
+ /// A
+ /// The parse node to use to read the discriminator value and create the object
+ public static global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Models.AttributesInResponse CreateFromDiscriminatorValue(IParseNode parseNode)
+ {
+ _ = parseNode ?? throw new ArgumentNullException(nameof(parseNode));
+ var mappingValue = parseNode.GetChildNode("openapi:discriminator")?.GetStringValue();
+ return mappingValue switch
+ {
+ "cupOfCoffees" => new global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Models.AttributesInCupOfCoffeeResponse(),
+ _ => new global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Models.AttributesInResponse(),
+ };
+ }
+
+ ///
+ /// The deserialization information for the current model
+ ///
+ /// A IDictionary<string, Action<IParseNode>>
+ public virtual IDictionary> GetFieldDeserializers()
+ {
+ return new Dictionary>
+ {
+ { "openapi:discriminator", n => { OpenapiDiscriminator = n.GetEnumValue(); } },
+ };
+ }
+
+ ///
+ /// Serializes information the current object
+ ///
+ /// Serialization writer to use to serialize this model
+ public virtual void Serialize(ISerializationWriter writer)
+ {
+ _ = writer ?? throw new ArgumentNullException(nameof(writer));
+ writer.WriteEnumValue("openapi:discriminator", OpenapiDiscriminator);
+ }
+ }
+}
+#pragma warning restore CS0618
diff --git a/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/Models/CupOfCoffeeCollectionResponseDocument.cs b/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/Models/CupOfCoffeeCollectionResponseDocument.cs
new file mode 100644
index 0000000000..6327429d7f
--- /dev/null
+++ b/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/Models/CupOfCoffeeCollectionResponseDocument.cs
@@ -0,0 +1,97 @@
+//
+#nullable enable
+#pragma warning disable CS8625
+#pragma warning disable CS0618
+using Microsoft.Kiota.Abstractions.Extensions;
+using Microsoft.Kiota.Abstractions.Serialization;
+using Microsoft.Kiota.Abstractions.Store;
+using System.Collections.Generic;
+using System.IO;
+using System;
+namespace OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Models
+{
+ [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
+ #pragma warning disable CS1591
+ public partial class CupOfCoffeeCollectionResponseDocument : IBackedModel, IParsable
+ #pragma warning restore CS1591
+ {
+ /// Stores model information.
+ public IBackingStore BackingStore { get; private set; }
+
+ /// The data property
+ public List? Data
+ {
+ get { return BackingStore?.Get?>("data"); }
+ set { BackingStore?.Set("data", value); }
+ }
+
+ /// The included property
+ public List? Included
+ {
+ get { return BackingStore?.Get?>("included"); }
+ set { BackingStore?.Set("included", value); }
+ }
+
+ /// The links property
+ public global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Models.ResourceCollectionTopLevelLinks? Links
+ {
+ get { return BackingStore?.Get("links"); }
+ set { BackingStore?.Set("links", value); }
+ }
+
+ /// The meta property
+ public global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Models.Meta? Meta
+ {
+ get { return BackingStore?.Get("meta"); }
+ set { BackingStore?.Set("meta", value); }
+ }
+
+ ///
+ /// Instantiates a new and sets the default values.
+ ///
+ public CupOfCoffeeCollectionResponseDocument()
+ {
+ BackingStore = BackingStoreFactorySingleton.Instance.CreateBackingStore();
+ }
+
+ ///
+ /// Creates a new instance of the appropriate class based on discriminator value
+ ///
+ /// A
+ /// The parse node to use to read the discriminator value and create the object
+ public static global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Models.CupOfCoffeeCollectionResponseDocument CreateFromDiscriminatorValue(IParseNode parseNode)
+ {
+ _ = parseNode ?? throw new ArgumentNullException(nameof(parseNode));
+ return new global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Models.CupOfCoffeeCollectionResponseDocument();
+ }
+
+ ///
+ /// The deserialization information for the current model
+ ///
+ /// A IDictionary<string, Action<IParseNode>>
+ public virtual IDictionary> GetFieldDeserializers()
+ {
+ return new Dictionary>
+ {
+ { "data", n => { Data = n.GetCollectionOfObjectValues(global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Models.DataInCupOfCoffeeResponse.CreateFromDiscriminatorValue)?.AsList(); } },
+ { "included", n => { Included = n.GetCollectionOfObjectValues(global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Models.ResourceInResponse.CreateFromDiscriminatorValue)?.AsList(); } },
+ { "links", n => { Links = n.GetObjectValue(global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Models.ResourceCollectionTopLevelLinks.CreateFromDiscriminatorValue); } },
+ { "meta", n => { Meta = n.GetObjectValue(global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Models.Meta.CreateFromDiscriminatorValue); } },
+ };
+ }
+
+ ///
+ /// Serializes information the current object
+ ///
+ /// Serialization writer to use to serialize this model
+ public virtual void Serialize(ISerializationWriter writer)
+ {
+ _ = writer ?? throw new ArgumentNullException(nameof(writer));
+ writer.WriteCollectionOfObjectValues("data", Data);
+ writer.WriteCollectionOfObjectValues("included", Included);
+ writer.WriteObjectValue("links", Links);
+ writer.WriteObjectValue