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("meta", Meta); + } + } +} +#pragma warning restore CS0618 diff --git a/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/Models/DataInCupOfCoffeeResponse.cs b/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/Models/DataInCupOfCoffeeResponse.cs new file mode 100644 index 0000000000..f67b1c2346 --- /dev/null +++ b/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/Models/DataInCupOfCoffeeResponse.cs @@ -0,0 +1,77 @@ +// +#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 DataInCupOfCoffeeResponse : global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Models.ResourceInResponse, IParsable + #pragma warning restore CS1591 + { + /// The attributes property + public global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Models.AttributesInCupOfCoffeeResponse? Attributes + { + get { return BackingStore?.Get("attributes"); } + set { BackingStore?.Set("attributes", value); } + } + + /// The id property + public string? Id + { + get { return BackingStore?.Get("id"); } + set { BackingStore?.Set("id", value); } + } + + /// The links property + public global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Models.ResourceLinks? Links + { + get { return BackingStore?.Get("links"); } + set { BackingStore?.Set("links", 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.DataInCupOfCoffeeResponse CreateFromDiscriminatorValue(IParseNode parseNode) + { + _ = parseNode ?? throw new ArgumentNullException(nameof(parseNode)); + return new global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Models.DataInCupOfCoffeeResponse(); + } + + /// + /// The deserialization information for the current model + /// + /// A IDictionary<string, Action<IParseNode>> + public override IDictionary> GetFieldDeserializers() + { + return new Dictionary>(base.GetFieldDeserializers()) + { + { "attributes", n => { Attributes = n.GetObjectValue(global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Models.AttributesInCupOfCoffeeResponse.CreateFromDiscriminatorValue); } }, + { "id", n => { Id = n.GetStringValue(); } }, + { "links", n => { Links = n.GetObjectValue(global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Models.ResourceLinks.CreateFromDiscriminatorValue); } }, + }; + } + + /// + /// 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.WriteObjectValue("attributes", Attributes); + writer.WriteStringValue("id", Id); + writer.WriteObjectValue("links", Links); + } + } +} +#pragma warning restore CS0618 diff --git a/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/Models/Email.cs b/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/Models/Email.cs new file mode 100644 index 0000000000..7ee9a1eb93 --- /dev/null +++ b/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/Models/Email.cs @@ -0,0 +1,105 @@ +// +#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 Email : IBackedModel, IParsable + #pragma warning restore CS1591 + { + /// Stores model information. + public IBackingStore BackingStore { get; private set; } + + /// The email body. + public string? Body + { + get { return BackingStore?.Get("body"); } + set { BackingStore?.Set("body", value); } + } + + /// The email address of the sender. + public string? From + { + get { return BackingStore?.Get("from"); } + set { BackingStore?.Set("from", value); } + } + + /// The date/time (in UTC) at which this email was sent. + public DateTimeOffset? SentAtUtc + { + get { return BackingStore?.Get("sentAtUtc"); } + set { BackingStore?.Set("sentAtUtc", value); } + } + + /// The email subject. + public string? Subject + { + get { return BackingStore?.Get("subject"); } + set { BackingStore?.Set("subject", value); } + } + + /// The email address of the recipient. + public string? To + { + get { return BackingStore?.Get("to"); } + set { BackingStore?.Set("to", value); } + } + + /// + /// Instantiates a new and sets the default values. + /// + public Email() + { + 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.Email CreateFromDiscriminatorValue(IParseNode parseNode) + { + _ = parseNode ?? throw new ArgumentNullException(nameof(parseNode)); + return new global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Models.Email(); + } + + /// + /// The deserialization information for the current model + /// + /// A IDictionary<string, Action<IParseNode>> + public virtual IDictionary> GetFieldDeserializers() + { + return new Dictionary> + { + { "body", n => { Body = n.GetStringValue(); } }, + { "from", n => { From = n.GetStringValue(); } }, + { "sentAtUtc", n => { SentAtUtc = n.GetDateTimeOffsetValue(); } }, + { "subject", n => { Subject = n.GetStringValue(); } }, + { "to", n => { To = n.GetStringValue(); } }, + }; + } + + /// + /// 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.WriteStringValue("body", Body); + writer.WriteStringValue("from", From); + writer.WriteStringValue("subject", Subject); + writer.WriteStringValue("to", To); + } + } +} +#pragma warning restore CS0618 diff --git a/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/Models/ErrorLinks.cs b/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/Models/ErrorLinks.cs new file mode 100644 index 0000000000..95dc5f1bd1 --- /dev/null +++ b/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/Models/ErrorLinks.cs @@ -0,0 +1,79 @@ +// +#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 ErrorLinks : IBackedModel, IParsable + #pragma warning restore CS1591 + { + /// The about property + public string? About + { + get { return BackingStore?.Get("about"); } + set { BackingStore?.Set("about", value); } + } + + /// Stores model information. + public IBackingStore BackingStore { get; private set; } + + /// The type property + public string? Type + { + get { return BackingStore?.Get("type"); } + set { BackingStore?.Set("type", value); } + } + + /// + /// Instantiates a new and sets the default values. + /// + public ErrorLinks() + { + 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.ErrorLinks CreateFromDiscriminatorValue(IParseNode parseNode) + { + _ = parseNode ?? throw new ArgumentNullException(nameof(parseNode)); + return new global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Models.ErrorLinks(); + } + + /// + /// The deserialization information for the current model + /// + /// A IDictionary<string, Action<IParseNode>> + public virtual IDictionary> GetFieldDeserializers() + { + return new Dictionary> + { + { "about", n => { About = n.GetStringValue(); } }, + { "type", n => { Type = n.GetStringValue(); } }, + }; + } + + /// + /// 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.WriteStringValue("about", About); + writer.WriteStringValue("type", Type); + } + } +} +#pragma warning restore CS0618 diff --git a/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/Models/ErrorObject.cs b/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/Models/ErrorObject.cs new file mode 100644 index 0000000000..e0d4ddbaa0 --- /dev/null +++ b/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/Models/ErrorObject.cs @@ -0,0 +1,133 @@ +// +#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 ErrorObject : IBackedModel, IParsable + #pragma warning restore CS1591 + { + /// Stores model information. + public IBackingStore BackingStore { get; private set; } + + /// The code property + public string? Code + { + get { return BackingStore?.Get("code"); } + set { BackingStore?.Set("code", value); } + } + + /// The detail property + public string? Detail + { + get { return BackingStore?.Get("detail"); } + set { BackingStore?.Set("detail", value); } + } + + /// The id property + public string? Id + { + get { return BackingStore?.Get("id"); } + set { BackingStore?.Set("id", value); } + } + + /// The links property + public global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Models.ErrorLinks? 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); } + } + + /// The source property + public global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Models.ErrorSource? Source + { + get { return BackingStore?.Get("source"); } + set { BackingStore?.Set("source", value); } + } + + /// The status property + public string? Status + { + get { return BackingStore?.Get("status"); } + set { BackingStore?.Set("status", value); } + } + + /// The title property + public string? Title + { + get { return BackingStore?.Get("title"); } + set { BackingStore?.Set("title", value); } + } + + /// + /// Instantiates a new and sets the default values. + /// + public ErrorObject() + { + 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.ErrorObject CreateFromDiscriminatorValue(IParseNode parseNode) + { + _ = parseNode ?? throw new ArgumentNullException(nameof(parseNode)); + return new global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Models.ErrorObject(); + } + + /// + /// The deserialization information for the current model + /// + /// A IDictionary<string, Action<IParseNode>> + public virtual IDictionary> GetFieldDeserializers() + { + return new Dictionary> + { + { "code", n => { Code = n.GetStringValue(); } }, + { "detail", n => { Detail = n.GetStringValue(); } }, + { "id", n => { Id = n.GetStringValue(); } }, + { "links", n => { Links = n.GetObjectValue(global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Models.ErrorLinks.CreateFromDiscriminatorValue); } }, + { "meta", n => { Meta = n.GetObjectValue(global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Models.Meta.CreateFromDiscriminatorValue); } }, + { "source", n => { Source = n.GetObjectValue(global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Models.ErrorSource.CreateFromDiscriminatorValue); } }, + { "status", n => { Status = n.GetStringValue(); } }, + { "title", n => { Title = n.GetStringValue(); } }, + }; + } + + /// + /// 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.WriteStringValue("code", Code); + writer.WriteStringValue("detail", Detail); + writer.WriteStringValue("id", Id); + writer.WriteObjectValue("links", Links); + writer.WriteObjectValue("meta", Meta); + writer.WriteObjectValue("source", Source); + writer.WriteStringValue("status", Status); + writer.WriteStringValue("title", Title); + } + } +} +#pragma warning restore CS0618 diff --git a/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/Models/ErrorResponseDocument.cs b/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/Models/ErrorResponseDocument.cs new file mode 100644 index 0000000000..87d5b69c0f --- /dev/null +++ b/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/Models/ErrorResponseDocument.cs @@ -0,0 +1,92 @@ +// +#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 Microsoft.Kiota.Abstractions; +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 ErrorResponseDocument : ApiException, IBackedModel, IParsable + #pragma warning restore CS1591 + { + /// Stores model information. + public IBackingStore BackingStore { get; private set; } + + /// The errors property + public List? Errors + { + get { return BackingStore?.Get?>("errors"); } + set { BackingStore?.Set("errors", value); } + } + + /// The links property + public global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Models.ErrorTopLevelLinks? Links + { + get { return BackingStore?.Get("links"); } + set { BackingStore?.Set("links", value); } + } + + /// The primary error message. + public override string Message { get => base.Message; } + + /// 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 ErrorResponseDocument() + { + 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.ErrorResponseDocument CreateFromDiscriminatorValue(IParseNode parseNode) + { + _ = parseNode ?? throw new ArgumentNullException(nameof(parseNode)); + return new global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Models.ErrorResponseDocument(); + } + + /// + /// The deserialization information for the current model + /// + /// A IDictionary<string, Action<IParseNode>> + public virtual IDictionary> GetFieldDeserializers() + { + return new Dictionary> + { + { "errors", n => { Errors = n.GetCollectionOfObjectValues(global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Models.ErrorObject.CreateFromDiscriminatorValue)?.AsList(); } }, + { "links", n => { Links = n.GetObjectValue(global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Models.ErrorTopLevelLinks.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("errors", Errors); + writer.WriteObjectValue("links", Links); + writer.WriteObjectValue("meta", Meta); + } + } +} +#pragma warning restore CS0618 diff --git a/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/Models/ErrorSource.cs b/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/Models/ErrorSource.cs new file mode 100644 index 0000000000..fedf6dd1d9 --- /dev/null +++ b/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/Models/ErrorSource.cs @@ -0,0 +1,88 @@ +// +#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 ErrorSource : IBackedModel, IParsable + #pragma warning restore CS1591 + { + /// Stores model information. + public IBackingStore BackingStore { get; private set; } + + /// The header property + public string? Header + { + get { return BackingStore?.Get("header"); } + set { BackingStore?.Set("header", value); } + } + + /// The parameter property + public string? Parameter + { + get { return BackingStore?.Get("parameter"); } + set { BackingStore?.Set("parameter", value); } + } + + /// The pointer property + public string? Pointer + { + get { return BackingStore?.Get("pointer"); } + set { BackingStore?.Set("pointer", value); } + } + + /// + /// Instantiates a new and sets the default values. + /// + public ErrorSource() + { + 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.ErrorSource CreateFromDiscriminatorValue(IParseNode parseNode) + { + _ = parseNode ?? throw new ArgumentNullException(nameof(parseNode)); + return new global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Models.ErrorSource(); + } + + /// + /// The deserialization information for the current model + /// + /// A IDictionary<string, Action<IParseNode>> + public virtual IDictionary> GetFieldDeserializers() + { + return new Dictionary> + { + { "header", n => { Header = n.GetStringValue(); } }, + { "parameter", n => { Parameter = n.GetStringValue(); } }, + { "pointer", n => { Pointer = n.GetStringValue(); } }, + }; + } + + /// + /// 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.WriteStringValue("header", Header); + writer.WriteStringValue("parameter", Parameter); + writer.WriteStringValue("pointer", Pointer); + } + } +} +#pragma warning restore CS0618 diff --git a/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/Models/ErrorTopLevelLinks.cs b/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/Models/ErrorTopLevelLinks.cs new file mode 100644 index 0000000000..9a14379229 --- /dev/null +++ b/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/Models/ErrorTopLevelLinks.cs @@ -0,0 +1,79 @@ +// +#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 ErrorTopLevelLinks : IBackedModel, IParsable + #pragma warning restore CS1591 + { + /// Stores model information. + public IBackingStore BackingStore { get; private set; } + + /// The describedby property + public string? Describedby + { + get { return BackingStore?.Get("describedby"); } + set { BackingStore?.Set("describedby", value); } + } + + /// The self property + public string? Self + { + get { return BackingStore?.Get("self"); } + set { BackingStore?.Set("self", value); } + } + + /// + /// Instantiates a new and sets the default values. + /// + public ErrorTopLevelLinks() + { + 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.ErrorTopLevelLinks CreateFromDiscriminatorValue(IParseNode parseNode) + { + _ = parseNode ?? throw new ArgumentNullException(nameof(parseNode)); + return new global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Models.ErrorTopLevelLinks(); + } + + /// + /// The deserialization information for the current model + /// + /// A IDictionary<string, Action<IParseNode>> + public virtual IDictionary> GetFieldDeserializers() + { + return new Dictionary> + { + { "describedby", n => { Describedby = n.GetStringValue(); } }, + { "self", n => { Self = n.GetStringValue(); } }, + }; + } + + /// + /// 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.WriteStringValue("describedby", Describedby); + writer.WriteStringValue("self", Self); + } + } +} +#pragma warning restore CS0618 diff --git a/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/Models/HttpValidationProblemDetails.cs b/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/Models/HttpValidationProblemDetails.cs new file mode 100644 index 0000000000..186f5be13a --- /dev/null +++ b/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/Models/HttpValidationProblemDetails.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.Store; +using Microsoft.Kiota.Abstractions; +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 HttpValidationProblemDetails : ApiException, IAdditionalDataHolder, IBackedModel, IParsable + #pragma warning restore CS1591 + { + /// Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well. + public IDictionary AdditionalData + { + get { return BackingStore.Get>("AdditionalData") ?? new Dictionary(); } + set { BackingStore.Set("AdditionalData", value); } + } + + /// Stores model information. + public IBackingStore BackingStore { get; private set; } + + /// The detail property + public string? Detail + { + get { return BackingStore?.Get("detail"); } + set { BackingStore?.Set("detail", value); } + } + + /// The errors property + public global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Models.HttpValidationProblemDetails_errors? Errors + { + get { return BackingStore?.Get("errors"); } + set { BackingStore?.Set("errors", value); } + } + + /// The instance property + public string? Instance + { + get { return BackingStore?.Get("instance"); } + set { BackingStore?.Set("instance", value); } + } + + /// The primary error message. + public override string Message { get => base.Message; } + + /// The status property + public int? Status + { + get { return BackingStore?.Get("status"); } + set { BackingStore?.Set("status", value); } + } + + /// The title property + public string? Title + { + get { return BackingStore?.Get("title"); } + set { BackingStore?.Set("title", value); } + } + + /// The type property + public string? Type + { + get { return BackingStore?.Get("type"); } + set { BackingStore?.Set("type", value); } + } + + /// + /// Instantiates a new and sets the default values. + /// + public HttpValidationProblemDetails() + { + BackingStore = BackingStoreFactorySingleton.Instance.CreateBackingStore(); + AdditionalData = new Dictionary(); + } + + /// + /// 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.HttpValidationProblemDetails CreateFromDiscriminatorValue(IParseNode parseNode) + { + _ = parseNode ?? throw new ArgumentNullException(nameof(parseNode)); + return new global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Models.HttpValidationProblemDetails(); + } + + /// + /// The deserialization information for the current model + /// + /// A IDictionary<string, Action<IParseNode>> + public virtual IDictionary> GetFieldDeserializers() + { + return new Dictionary> + { + { "detail", n => { Detail = n.GetStringValue(); } }, + { "errors", n => { Errors = n.GetObjectValue(global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Models.HttpValidationProblemDetails_errors.CreateFromDiscriminatorValue); } }, + { "instance", n => { Instance = n.GetStringValue(); } }, + { "status", n => { Status = n.GetIntValue(); } }, + { "title", n => { Title = n.GetStringValue(); } }, + { "type", n => { Type = n.GetStringValue(); } }, + }; + } + + /// + /// 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.WriteStringValue("detail", Detail); + writer.WriteObjectValue("errors", Errors); + writer.WriteStringValue("instance", Instance); + writer.WriteIntValue("status", Status); + writer.WriteStringValue("title", Title); + writer.WriteStringValue("type", Type); + writer.WriteAdditionalData(AdditionalData); + } + } +} +#pragma warning restore CS0618 diff --git a/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/Models/HttpValidationProblemDetails_errors.cs b/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/Models/HttpValidationProblemDetails_errors.cs new file mode 100644 index 0000000000..93e17e8c2d --- /dev/null +++ b/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/Models/HttpValidationProblemDetails_errors.cs @@ -0,0 +1,70 @@ +// +#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 HttpValidationProblemDetails_errors : IAdditionalDataHolder, IBackedModel, IParsable + #pragma warning restore CS1591 + { + /// Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well. + public IDictionary AdditionalData + { + get { return BackingStore.Get>("AdditionalData") ?? new Dictionary(); } + set { BackingStore.Set("AdditionalData", value); } + } + + /// Stores model information. + public IBackingStore BackingStore { get; private set; } + + /// + /// Instantiates a new and sets the default values. + /// + public HttpValidationProblemDetails_errors() + { + BackingStore = BackingStoreFactorySingleton.Instance.CreateBackingStore(); + AdditionalData = new Dictionary(); + } + + /// + /// 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.HttpValidationProblemDetails_errors CreateFromDiscriminatorValue(IParseNode parseNode) + { + _ = parseNode ?? throw new ArgumentNullException(nameof(parseNode)); + return new global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Models.HttpValidationProblemDetails_errors(); + } + + /// + /// The deserialization information for the current model + /// + /// A IDictionary<string, Action<IParseNode>> + public virtual IDictionary> GetFieldDeserializers() + { + return new Dictionary> + { + }; + } + + /// + /// 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.WriteAdditionalData(AdditionalData); + } + } +} +#pragma warning restore CS0618 diff --git a/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/Models/Meta.cs b/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/Models/Meta.cs new file mode 100644 index 0000000000..3ba5bc356b --- /dev/null +++ b/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/Models/Meta.cs @@ -0,0 +1,70 @@ +// +#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 Meta : IAdditionalDataHolder, IBackedModel, IParsable + #pragma warning restore CS1591 + { + /// Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well. + public IDictionary AdditionalData + { + get { return BackingStore.Get>("AdditionalData") ?? new Dictionary(); } + set { BackingStore.Set("AdditionalData", value); } + } + + /// Stores model information. + public IBackingStore BackingStore { get; private set; } + + /// + /// Instantiates a new and sets the default values. + /// + public Meta() + { + BackingStore = BackingStoreFactorySingleton.Instance.CreateBackingStore(); + AdditionalData = new Dictionary(); + } + + /// + /// 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.Meta CreateFromDiscriminatorValue(IParseNode parseNode) + { + _ = parseNode ?? throw new ArgumentNullException(nameof(parseNode)); + return new global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Models.Meta(); + } + + /// + /// The deserialization information for the current model + /// + /// A IDictionary<string, Action<IParseNode>> + public virtual IDictionary> GetFieldDeserializers() + { + return new Dictionary> + { + }; + } + + /// + /// 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.WriteAdditionalData(AdditionalData); + } + } +} +#pragma warning restore CS0618 diff --git a/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/Models/ResourceCollectionTopLevelLinks.cs b/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/Models/ResourceCollectionTopLevelLinks.cs new file mode 100644 index 0000000000..17e0cc38c4 --- /dev/null +++ b/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/Models/ResourceCollectionTopLevelLinks.cs @@ -0,0 +1,115 @@ +// +#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 ResourceCollectionTopLevelLinks : IBackedModel, IParsable + #pragma warning restore CS1591 + { + /// Stores model information. + public IBackingStore BackingStore { get; private set; } + + /// The describedby property + public string? Describedby + { + get { return BackingStore?.Get("describedby"); } + set { BackingStore?.Set("describedby", value); } + } + + /// The first property + public string? First + { + get { return BackingStore?.Get("first"); } + set { BackingStore?.Set("first", value); } + } + + /// The last property + public string? Last + { + get { return BackingStore?.Get("last"); } + set { BackingStore?.Set("last", value); } + } + + /// The next property + public string? Next + { + get { return BackingStore?.Get("next"); } + set { BackingStore?.Set("next", value); } + } + + /// The prev property + public string? Prev + { + get { return BackingStore?.Get("prev"); } + set { BackingStore?.Set("prev", value); } + } + + /// The self property + public string? Self + { + get { return BackingStore?.Get("self"); } + set { BackingStore?.Set("self", value); } + } + + /// + /// Instantiates a new and sets the default values. + /// + public ResourceCollectionTopLevelLinks() + { + 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.ResourceCollectionTopLevelLinks CreateFromDiscriminatorValue(IParseNode parseNode) + { + _ = parseNode ?? throw new ArgumentNullException(nameof(parseNode)); + return new global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Models.ResourceCollectionTopLevelLinks(); + } + + /// + /// The deserialization information for the current model + /// + /// A IDictionary<string, Action<IParseNode>> + public virtual IDictionary> GetFieldDeserializers() + { + return new Dictionary> + { + { "describedby", n => { Describedby = n.GetStringValue(); } }, + { "first", n => { First = n.GetStringValue(); } }, + { "last", n => { Last = n.GetStringValue(); } }, + { "next", n => { Next = n.GetStringValue(); } }, + { "prev", n => { Prev = n.GetStringValue(); } }, + { "self", n => { Self = n.GetStringValue(); } }, + }; + } + + /// + /// 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.WriteStringValue("describedby", Describedby); + writer.WriteStringValue("first", First); + writer.WriteStringValue("last", Last); + writer.WriteStringValue("next", Next); + writer.WriteStringValue("prev", Prev); + writer.WriteStringValue("self", Self); + } + } +} +#pragma warning restore CS0618 diff --git a/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/Models/ResourceInResponse.cs b/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/Models/ResourceInResponse.cs new file mode 100644 index 0000000000..6482b3f020 --- /dev/null +++ b/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/Models/ResourceInResponse.cs @@ -0,0 +1,84 @@ +// +#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 ResourceInResponse : IBackedModel, IParsable + #pragma warning restore CS1591 + { + /// Stores model information. + public IBackingStore BackingStore { get; private set; } + + /// The meta property + public global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Models.Meta? Meta + { + get { return BackingStore?.Get("meta"); } + set { BackingStore?.Set("meta", value); } + } + + /// The type property + public global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Models.ResourceType? Type + { + get { return BackingStore?.Get("type"); } + set { BackingStore?.Set("type", value); } + } + + /// + /// Instantiates a new and sets the default values. + /// + public ResourceInResponse() + { + 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.ResourceInResponse CreateFromDiscriminatorValue(IParseNode parseNode) + { + _ = parseNode ?? throw new ArgumentNullException(nameof(parseNode)); + var mappingValue = parseNode.GetChildNode("type")?.GetStringValue(); + return mappingValue switch + { + "cupOfCoffees" => new global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Models.DataInCupOfCoffeeResponse(), + _ => new global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Models.ResourceInResponse(), + }; + } + + /// + /// The deserialization information for the current model + /// + /// A IDictionary<string, Action<IParseNode>> + public virtual IDictionary> GetFieldDeserializers() + { + return new Dictionary> + { + { "meta", n => { Meta = n.GetObjectValue(global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Models.Meta.CreateFromDiscriminatorValue); } }, + { "type", n => { Type = 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.WriteObjectValue("meta", Meta); + writer.WriteEnumValue("type", Type); + } + } +} +#pragma warning restore CS0618 diff --git a/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/Models/ResourceLinks.cs b/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/Models/ResourceLinks.cs new file mode 100644 index 0000000000..901e72ad9b --- /dev/null +++ b/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/Models/ResourceLinks.cs @@ -0,0 +1,70 @@ +// +#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 ResourceLinks : IBackedModel, IParsable + #pragma warning restore CS1591 + { + /// Stores model information. + public IBackingStore BackingStore { get; private set; } + + /// The self property + public string? Self + { + get { return BackingStore?.Get("self"); } + set { BackingStore?.Set("self", value); } + } + + /// + /// Instantiates a new and sets the default values. + /// + public ResourceLinks() + { + 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.ResourceLinks CreateFromDiscriminatorValue(IParseNode parseNode) + { + _ = parseNode ?? throw new ArgumentNullException(nameof(parseNode)); + return new global::OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Models.ResourceLinks(); + } + + /// + /// The deserialization information for the current model + /// + /// A IDictionary<string, Action<IParseNode>> + public virtual IDictionary> GetFieldDeserializers() + { + return new Dictionary> + { + { "self", n => { Self = n.GetStringValue(); } }, + }; + } + + /// + /// 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.WriteStringValue("self", Self); + } + } +} +#pragma warning restore CS0618 diff --git a/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/Models/ResourceType.cs b/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/Models/ResourceType.cs new file mode 100644 index 0000000000..2db9c19590 --- /dev/null +++ b/test/OpenApiKiotaEndToEndTests/MixedControllers/GeneratedCode/Models/ResourceType.cs @@ -0,0 +1,18 @@ +// +#nullable enable +#pragma warning disable CS8625 +using System.Runtime.Serialization; +using System; +namespace OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Models +{ + [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")] + #pragma warning disable CS1591 + public enum ResourceType + #pragma warning restore CS1591 + { + [EnumMember(Value = "cupOfCoffees")] + #pragma warning disable CS1591 + CupOfCoffees, + #pragma warning restore CS1591 + } +} diff --git a/test/OpenApiKiotaEndToEndTests/MixedControllers/MixedControllerTests.cs b/test/OpenApiKiotaEndToEndTests/MixedControllers/MixedControllerTests.cs new file mode 100644 index 0000000000..6fac5b747b --- /dev/null +++ b/test/OpenApiKiotaEndToEndTests/MixedControllers/MixedControllerTests.cs @@ -0,0 +1,314 @@ +using System.Net; +using FluentAssertions; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Kiota.Abstractions; +using Microsoft.Kiota.Abstractions.Serialization; +using Microsoft.Kiota.Http.HttpClientLibrary; +using OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode; +using OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Models; +using OpenApiTests.MixedControllers; +using TestBuildingBlocks; +using Xunit; +using Xunit.Abstractions; +using ClientEmail = OpenApiKiotaEndToEndTests.MixedControllers.GeneratedCode.Models.Email; +using ServerEmail = OpenApiTests.MixedControllers.Email; + +namespace OpenApiKiotaEndToEndTests.MixedControllers; + +public sealed class MixedControllerTests : IClassFixture>, IDisposable +{ + private readonly IntegrationTestContext _testContext; + private readonly TestableHttpClientRequestAdapterFactory _requestAdapterFactory; + private readonly MixedControllerFakers _fakers = new(); + + public MixedControllerTests(IntegrationTestContext testContext, ITestOutputHelper testOutputHelper) + { + _testContext = testContext; + _requestAdapterFactory = new TestableHttpClientRequestAdapterFactory(testOutputHelper); + + testContext.UseController(); + testContext.UseController(); + + testContext.ConfigureServices(services => + { + services.AddSingleton(); + services.AddSingleton(); + services.TryAddEnumerable(ServiceDescriptor.Transient()); + }); + + var fileStorage = _testContext.Factory.Services.GetRequiredService(); + fileStorage.Files.Clear(); + + var emailsProvider = _testContext.Factory.Services.GetRequiredService(); + emailsProvider.SentEmails.Clear(); + } + + [Fact] + public async Task Can_upload_file() + { + // Arrange + using HttpClientRequestAdapter requestAdapter = _requestAdapterFactory.CreateAdapter(_testContext.Factory); + var apiClient = new MixedControllersClient(requestAdapter); + + byte[] fileContents = "Hello upload"u8.ToArray(); + using var stream = new MemoryStream(); + stream.Write(fileContents); + stream.Seek(0, SeekOrigin.Begin); + + var requestBody = new MultipartBody(); + requestBody.AddOrReplacePart("file", "text/plain", stream, "demo-upload.txt"); + + // Act + string? response = await apiClient.FileTransfers.PostAsync(requestBody); + + // Assert + response.Should().Be($"Received file with a size of {fileContents.Length} bytes."); + } + + [Fact] + public async Task Cannot_upload_empty_file() + { + // Arrange + using HttpClientRequestAdapter requestAdapter = _requestAdapterFactory.CreateAdapter(_testContext.Factory); + var apiClient = new MixedControllersClient(requestAdapter); + + using var stream = new MemoryStream(); + var requestBody = new MultipartBody(); + requestBody.AddOrReplacePart("file", "text/plain", stream, "demo-empty.txt"); + + // Act + Func action = async () => await apiClient.FileTransfers.PostAsync(requestBody); + + // Assert + ApiException exception = (await action.Should().ThrowExactlyAsync()).Which; + exception.ResponseStatusCode.Should().Be((int)HttpStatusCode.BadRequest); + } + + [Fact] + public async Task Finds_existing_file() + { + // Arrange + byte[] fileContents = "Hello find"u8.ToArray(); + + var storage = _testContext.Factory.Services.GetRequiredService(); + storage.Files.TryAdd("demo-existing-file.txt", fileContents); + + using HttpClientRequestAdapter requestAdapter = _requestAdapterFactory.CreateAdapter(_testContext.Factory); + var apiClient = new MixedControllersClient(requestAdapter); + + // Act + Func action = async () => await apiClient.FileTransfers.Find.GetAsync(request => request.QueryParameters.FileName = "demo-existing-file.txt"); + + // Assert + await action.Should().NotThrowAsync(); + } + + [Fact] + public async Task Does_not_find_missing_file() + { + // Arrange + using HttpClientRequestAdapter requestAdapter = _requestAdapterFactory.CreateAdapter(_testContext.Factory); + var apiClient = new MixedControllersClient(requestAdapter); + + // Act + Func action = async () => await apiClient.FileTransfers.Find.GetAsync(request => request.QueryParameters.FileName = "demo-missing-file.txt"); + + // Assert + ApiException exception = (await action.Should().ThrowExactlyAsync()).Which; + exception.ResponseStatusCode.Should().Be((int)HttpStatusCode.NotFound); + } + + [Fact] + public async Task Can_download_file() + { + // Arrange + byte[] fileContents = "Hello download"u8.ToArray(); + + var storage = _testContext.Factory.Services.GetRequiredService(); + storage.Files.TryAdd("demo-download.txt", fileContents); + + using HttpClientRequestAdapter requestAdapter = _requestAdapterFactory.CreateAdapter(_testContext.Factory); + var apiClient = new MixedControllersClient(requestAdapter); + + // Act + await using Stream? response = await apiClient.FileTransfers.GetAsync(request => request.QueryParameters.FileName = "demo-download.txt"); + + // Assert + response.Should().NotBeNull(); + + using var streamReader = new StreamReader(response); + string downloadedContents = await streamReader.ReadToEndAsync(); + + downloadedContents.Should().Be("Hello download"); + } + + [Fact] + public async Task Cannot_download_missing_file() + { + // Arrange + using HttpClientRequestAdapter requestAdapter = _requestAdapterFactory.CreateAdapter(_testContext.Factory); + var apiClient = new MixedControllersClient(requestAdapter); + + // Act + Func action = async () => await apiClient.FileTransfers.GetAsync(request => request.QueryParameters.FileName = "demo-missing-file.txt"); + + // Assert + ApiException exception = (await action.Should().ThrowExactlyAsync()).Which; + exception.ResponseStatusCode.Should().Be((int)HttpStatusCode.NotFound); + } + + [Fact] + public async Task Can_send_email() + { + // Arrange + var emailsProvider = _testContext.Factory.Services.GetRequiredService(); + + ServerEmail newEmail = _fakers.Email.GenerateOne(); + + using HttpClientRequestAdapter requestAdapter = _requestAdapterFactory.CreateAdapter(_testContext.Factory); + var apiClient = new MixedControllersClient(requestAdapter); + + var requestBody = new ClientEmail + { + Subject = newEmail.Subject, + Body = newEmail.Body, + From = newEmail.From, + To = newEmail.To + }; + + // Act + await apiClient.Emails.Send.PostAsync(requestBody); + + // Assert + emailsProvider.SentEmails.Should().HaveCount(1); + } + + [Fact] + public async Task Cannot_send_email_with_invalid_addresses() + { + // Arrange + ServerEmail newEmail = _fakers.Email.GenerateOne(); + newEmail.From = "invalid-sender-address"; + newEmail.To = "invalid-recipient-address"; + + using HttpClientRequestAdapter requestAdapter = _requestAdapterFactory.CreateAdapter(_testContext.Factory); + var apiClient = new MixedControllersClient(requestAdapter); + + var requestBody = new ClientEmail + { + Subject = newEmail.Subject, + Body = newEmail.Body, + From = newEmail.From, + To = newEmail.To + }; + + // Act + Func action = async () => await apiClient.Emails.Send.PostAsync(requestBody); + + // Assert + HttpValidationProblemDetails exception = (await action.Should().ThrowExactlyAsync()).Which; + + exception.Status.Should().Be((int)HttpStatusCode.BadRequest); + exception.Type.Should().Be("https://tools.ietf.org/html/rfc9110#section-15.5.1"); + exception.Title.Should().Be("One or more validation errors occurred."); + exception.Detail.Should().BeNull(); + exception.Instance.Should().BeNull(); + exception.Errors.Should().NotBeNull(); + + IDictionary errors = exception.Errors.AdditionalData.Should().HaveCount(2).And.Subject; + + errors.Should().ContainKey("From").WhoseValue.Should().BeOfType().Subject.GetValue().ToArray().Should().ContainSingle().Which.Should() + .BeOfType().Subject.GetValue().Should().Be("The From field is not a valid e-mail address."); + + errors.Should().ContainKey("To").WhoseValue.Should().BeOfType().Subject.GetValue().ToArray().Should().ContainSingle().Which.Should() + .BeOfType().Subject.GetValue().Should().Be("The To field is not a valid e-mail address."); + } + + [Fact] + public async Task Can_get_sent_emails() + { + // Arrange + var timeProvider = _testContext.Factory.Services.GetRequiredService(); + var emailsProvider = _testContext.Factory.Services.GetRequiredService(); + + DateTimeOffset utcNow = timeProvider.GetUtcNow(); + + ServerEmail existingEmail = _fakers.Email.GenerateOne(); + existingEmail.SetSentAt(utcNow.AddHours(-1)); + emailsProvider.SentEmails.TryAdd(utcNow, existingEmail); + + using HttpClientRequestAdapter requestAdapter = _requestAdapterFactory.CreateAdapter(_testContext.Factory); + var apiClient = new MixedControllersClient(requestAdapter); + + DateTimeOffset sinceUtc = utcNow.AddHours(-2); + + // Act + List? response = await apiClient.Emails.SentSince.GetAsync(request => request.QueryParameters.SinceUtc = sinceUtc); + + // Assert + response.Should().HaveCount(1); + response.ElementAt(0).Subject.Should().Be(existingEmail.Subject); + response.ElementAt(0).Body.Should().Be(existingEmail.Body); + response.ElementAt(0).From.Should().Be(existingEmail.From); + response.ElementAt(0).To.Should().Be(existingEmail.To); + response.ElementAt(0).SentAtUtc.Should().Be(existingEmail.SentAtUtc); + } + + [Fact] + public async Task Cannot_get_sent_emails_in_future() + { + // Arrange + var timeProvider = _testContext.Factory.Services.GetRequiredService(); + + using HttpClientRequestAdapter requestAdapter = _requestAdapterFactory.CreateAdapter(_testContext.Factory); + var apiClient = new MixedControllersClient(requestAdapter); + + DateTimeOffset sinceUtc = timeProvider.GetUtcNow().AddHours(1); + + // Act + Func action = async () => await apiClient.Emails.SentSince.GetAsync(request => request.QueryParameters.SinceUtc = sinceUtc); + + // Assert + HttpValidationProblemDetails exception = (await action.Should().ThrowExactlyAsync()).Which; + + exception.Status.Should().Be((int)HttpStatusCode.BadRequest); + exception.Type.Should().Be("https://tools.ietf.org/html/rfc9110#section-15.5.1"); + exception.Title.Should().Be("One or more validation errors occurred."); + exception.Detail.Should().BeNull(); + exception.Instance.Should().BeNull(); + exception.Errors.Should().NotBeNull(); + + IDictionary? errors = exception.Errors.AdditionalData.Should().HaveCount(1).And.Subject; + + errors.Should().ContainKey("sinceUtc").WhoseValue.Should().BeOfType().Subject.GetValue().ToArray().Should().ContainSingle().Which.Should() + .BeOfType().Subject.GetValue().Should().Be("The sinceUtc parameter must be in the past."); + } + + [Fact] + public async Task Can_try_get_sent_emails_in_future() + { + // Arrange + var timeProvider = _testContext.Factory.Services.GetRequiredService(); + + using HttpClientRequestAdapter requestAdapter = _requestAdapterFactory.CreateAdapter(_testContext.Factory); + var apiClient = new MixedControllersClient(requestAdapter); + + DateTimeOffset sinceUtc = timeProvider.GetUtcNow().AddHours(1); + + // Act + Func action = async () => await apiClient.Emails.SentSince.HeadAsync(request => request.QueryParameters.SinceUtc = sinceUtc); + + // Assert + ApiException exception = (await action.Should().ThrowExactlyAsync()).Which; + + exception.ResponseStatusCode.Should().Be((int)HttpStatusCode.BadRequest); + exception.ResponseHeaders.Should().BeEmpty(); + } + + public void Dispose() + { + _requestAdapterFactory.Dispose(); + } +} diff --git a/test/OpenApiKiotaEndToEndTests/OpenApiKiotaEndToEndTests.csproj b/test/OpenApiKiotaEndToEndTests/OpenApiKiotaEndToEndTests.csproj index ca0db62217..04e8e209d5 100644 --- a/test/OpenApiKiotaEndToEndTests/OpenApiKiotaEndToEndTests.csproj +++ b/test/OpenApiKiotaEndToEndTests/OpenApiKiotaEndToEndTests.csproj @@ -51,6 +51,13 @@ ./%(Name)/GeneratedCode $(JsonApiExtraArguments) + + MixedControllers + $(MSBuildProjectName).%(Name).GeneratedCode + %(Name)Client + ./%(Name)/GeneratedCode + $(JsonApiExtraArguments) + ModelStateValidation $(MSBuildProjectName).%(Name).GeneratedCode diff --git a/test/OpenApiNSwagEndToEndTests/AtomicOperations/MediaTypeTests.cs b/test/OpenApiNSwagEndToEndTests/AtomicOperations/MediaTypeTests.cs new file mode 100644 index 0000000000..208f81b0de --- /dev/null +++ b/test/OpenApiNSwagEndToEndTests/AtomicOperations/MediaTypeTests.cs @@ -0,0 +1,83 @@ +using System.Net; +using FluentAssertions; +using JsonApiDotNetCore.Middleware; +using JsonApiDotNetCore.Serialization.Objects; +using OpenApiTests; +using OpenApiTests.AtomicOperations; +using TestBuildingBlocks; +using Xunit; + +namespace OpenApiNSwagEndToEndTests.AtomicOperations; + +public sealed class MediaTypeTests : IClassFixture, OperationsDbContext>> +{ + private readonly IntegrationTestContext, OperationsDbContext> _testContext; + private readonly OperationsFakers _fakers; + + public MediaTypeTests(IntegrationTestContext, OperationsDbContext> testContext) + { + _testContext = testContext; + + testContext.UseController(); + + _fakers = new OperationsFakers(testContext.Factory.Services); + } + + [Fact] + public async Task Can_create_resource_with_default_media_type() + { + // Arrange + Teacher newTeacher = _fakers.Teacher.GenerateOne(); + + var requestBody = new + { + atomic__operations = new[] + { + new + { + op = "add", + data = new + { + type = "teachers", + attributes = new + { + name = newTeacher.Name, + emailAddress = newTeacher.EmailAddress + } + } + } + } + }; + + const string route = "/operations"; + + // Act + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + + // Assert + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); + + httpResponse.Content.Headers.ContentType.Should().NotBeNull(); + httpResponse.Content.Headers.ContentType.ToString().Should().Be(JsonApiMediaType.AtomicOperations.ToString()); + + responseDocument.Results.Should().HaveCount(1); + + responseDocument.Results[0].Data.SingleValue.RefShould().NotBeNull().And.Subject.With(resource => + { + resource.Type.Should().Be("teachers"); + resource.Attributes.Should().ContainKey("name").WhoseValue.Should().Be(newTeacher.Name); + resource.Attributes.Should().ContainKey("emailAddress").WhoseValue.Should().Be(newTeacher.EmailAddress); + resource.Relationships.Should().BeNull(); + }); + + long newTeacherId = long.Parse(responseDocument.Results[0].Data.SingleValue!.Id.Should().NotBeNull().And.Subject); + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + Teacher teacherInDatabase = await dbContext.Teachers.FirstWithIdAsync(newTeacherId); + + teacherInDatabase.Name.Should().Be(newTeacher.Name); + teacherInDatabase.EmailAddress.Should().Be(newTeacher.EmailAddress); + }); + } +} diff --git a/test/OpenApiNSwagEndToEndTests/MixedControllers/MixedControllerTests.cs b/test/OpenApiNSwagEndToEndTests/MixedControllers/MixedControllerTests.cs new file mode 100644 index 0000000000..ee98280ae6 --- /dev/null +++ b/test/OpenApiNSwagEndToEndTests/MixedControllers/MixedControllerTests.cs @@ -0,0 +1,316 @@ +using System.Net; +using FluentAssertions; +using JsonApiDotNetCore.OpenApi.Client.NSwag; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using OpenApiNSwagEndToEndTests.MixedControllers.GeneratedCode; +using OpenApiTests.MixedControllers; +using TestBuildingBlocks; +using Xunit; +using Xunit.Abstractions; +using ClientEmail = OpenApiNSwagEndToEndTests.MixedControllers.GeneratedCode.Email; +using ServerEmail = OpenApiTests.MixedControllers.Email; + +namespace OpenApiNSwagEndToEndTests.MixedControllers; + +public sealed class MixedControllerTests : IClassFixture>, IDisposable +{ + private readonly IntegrationTestContext _testContext; + private readonly XUnitLogHttpMessageHandler _logHttpMessageHandler; + private readonly MixedControllerFakers _fakers = new(); + + public MixedControllerTests(IntegrationTestContext testContext, ITestOutputHelper testOutputHelper) + { + _testContext = testContext; + _logHttpMessageHandler = new XUnitLogHttpMessageHandler(testOutputHelper); + + testContext.UseController(); + testContext.UseController(); + + testContext.ConfigureServices(services => + { + services.AddSingleton(); + services.AddSingleton(); + services.TryAddEnumerable(ServiceDescriptor.Transient()); + }); + + var fileStorage = _testContext.Factory.Services.GetRequiredService(); + fileStorage.Files.Clear(); + + var emailsProvider = _testContext.Factory.Services.GetRequiredService(); + emailsProvider.SentEmails.Clear(); + } + + [Fact] + public async Task Can_upload_file() + { + // Arrange + using HttpClient httpClient = _testContext.Factory.CreateDefaultClient(_logHttpMessageHandler); + var apiClient = new MixedControllersClient(httpClient); + + byte[] fileContents = "Hello upload"u8.ToArray(); + using var stream = new MemoryStream(); + stream.Write(fileContents); + stream.Seek(0, SeekOrigin.Begin); + var fileParameter = new FileParameter(stream, "demo-upload.txt", "text/plain"); + + // Act + string response = await apiClient.UploadAsync(fileParameter); + + // Assert + response.Should().Be($"Received file with a size of {fileContents.Length} bytes."); + } + + [Fact] + public async Task Cannot_upload_empty_file() + { + // Arrange + using HttpClient httpClient = _testContext.Factory.CreateDefaultClient(_logHttpMessageHandler); + var apiClient = new MixedControllersClient(httpClient); + + using var stream = new MemoryStream(); + var fileParameter = new FileParameter(stream, "demo-empty.txt", "text/plain"); + + // Act + Func action = async () => await apiClient.UploadAsync(fileParameter); + + // Assert + ApiException exception = (await action.Should().ThrowExactlyAsync()).Which; + + exception.StatusCode.Should().Be((int)HttpStatusCode.BadRequest); + exception.Message.Should().Be("HTTP 400: Bad Request"); + exception.Response.Should().Be("Empty files cannot be uploaded."); + } + + [Fact] + public async Task Finds_existing_file() + { + // Arrange + byte[] fileContents = "Hello find"u8.ToArray(); + + var storage = _testContext.Factory.Services.GetRequiredService(); + storage.Files.TryAdd("demo-existing-file.txt", fileContents); + + using HttpClient httpClient = _testContext.Factory.CreateDefaultClient(_logHttpMessageHandler); + var apiClient = new MixedControllersClient(httpClient); + + // Act + Func action = async () => await apiClient.ExistsAsync("demo-existing-file.txt"); + + // Assert + await action.Should().NotThrowAsync(); + } + + [Fact] + public async Task Does_not_find_missing_file() + { + // Arrange + using HttpClient httpClient = _testContext.Factory.CreateDefaultClient(_logHttpMessageHandler); + var apiClient = new MixedControllersClient(httpClient); + + // Act + Func action = async () => await apiClient.ExistsAsync("demo-missing-file.txt"); + + // Assert + ApiException exception = (await action.Should().ThrowExactlyAsync()).Which; + + exception.StatusCode.Should().Be((int)HttpStatusCode.NotFound); + exception.Message.Should().Be("HTTP 404: Not Found"); + exception.Response.Should().BeNull(); + } + + [Fact] + public async Task Can_download_file() + { + // Arrange + byte[] fileContents = "Hello download"u8.ToArray(); + + var storage = _testContext.Factory.Services.GetRequiredService(); + storage.Files.TryAdd("demo-download.txt", fileContents); + + using HttpClient httpClient = _testContext.Factory.CreateDefaultClient(_logHttpMessageHandler); + var apiClient = new MixedControllersClient(httpClient); + + // Act + using FileResponse response = await apiClient.DownloadAsync("demo-download.txt"); + + // Assert + response.StatusCode.Should().Be((int)HttpStatusCode.OK); + response.Headers.Should().ContainKey("Content-Type").WhoseValue.Should().ContainSingle().Which.Should().Be("application/octet-stream"); + response.Headers.Should().ContainKey("Content-Length").WhoseValue.Should().ContainSingle().Which.Should().Be(fileContents.Length.ToString()); + + using var streamReader = new StreamReader(response.Stream); + string downloadedContents = await streamReader.ReadToEndAsync(); + + downloadedContents.Should().Be("Hello download"); + } + + [Fact] + public async Task Cannot_download_missing_file() + { + // Arrange + using HttpClient httpClient = _testContext.Factory.CreateDefaultClient(_logHttpMessageHandler); + var apiClient = new MixedControllersClient(httpClient); + + // Act + Func action = async () => await apiClient.DownloadAsync("demo-missing-file.txt"); + + // Assert + ApiException exception = (await action.Should().ThrowExactlyAsync()).Which; + + exception.StatusCode.Should().Be((int)HttpStatusCode.NotFound); + exception.Message.Should().Be("HTTP 404: Not Found"); + exception.Response.Should().Be("The file 'demo-missing-file.txt' does not exist."); + } + + [Fact] + public async Task Can_send_email() + { + // Arrange + var emailsProvider = _testContext.Factory.Services.GetRequiredService(); + + ServerEmail newEmail = _fakers.Email.GenerateOne(); + + using HttpClient httpClient = _testContext.Factory.CreateDefaultClient(_logHttpMessageHandler); + var apiClient = new MixedControllersClient(httpClient); + + var requestBody = new ClientEmail + { + Subject = newEmail.Subject, + Body = newEmail.Body, + From = newEmail.From, + To = newEmail.To + }; + + // Act + await apiClient.SendEmailAsync(requestBody); + + // Assert + emailsProvider.SentEmails.Should().HaveCount(1); + } + + [Fact] + public async Task Cannot_send_email_with_invalid_addresses() + { + // Arrange + ServerEmail newEmail = _fakers.Email.GenerateOne(); + newEmail.From = "invalid-sender-address"; + newEmail.To = "invalid-recipient-address"; + + using HttpClient httpClient = _testContext.Factory.CreateDefaultClient(_logHttpMessageHandler); + var apiClient = new MixedControllersClient(httpClient); + + var requestBody = new ClientEmail + { + Subject = newEmail.Subject, + Body = newEmail.Body, + From = newEmail.From, + To = newEmail.To + }; + + // Act + Func action = async () => await apiClient.SendEmailAsync(requestBody); + + // Assert + ApiException exception = (await action.Should().ThrowExactlyAsync>()).Which; + + exception.StatusCode.Should().Be((int)HttpStatusCode.BadRequest); + exception.Message.Should().Be("HTTP 400: Bad Request"); + exception.Result.Status.Should().Be((int)HttpStatusCode.BadRequest); + exception.Result.Type.Should().Be("https://tools.ietf.org/html/rfc9110#section-15.5.1"); + exception.Result.Title.Should().Be("One or more validation errors occurred."); + exception.Result.Detail.Should().BeNull(); + exception.Result.Instance.Should().BeNull(); + + IDictionary> errors = exception.Result.Errors.Should().HaveCount(2).And.Subject; + errors.Should().ContainKey("From").WhoseValue.Should().ContainSingle().Which.Should().Be("The From field is not a valid e-mail address."); + errors.Should().ContainKey("To").WhoseValue.Should().ContainSingle().Which.Should().Be("The To field is not a valid e-mail address."); + } + + [Fact] + public async Task Can_get_sent_emails() + { + // Arrange + var timeProvider = _testContext.Factory.Services.GetRequiredService(); + var emailsProvider = _testContext.Factory.Services.GetRequiredService(); + + DateTimeOffset utcNow = timeProvider.GetUtcNow(); + + ServerEmail existingEmail = _fakers.Email.GenerateOne(); + existingEmail.SetSentAt(utcNow.AddHours(-1)); + emailsProvider.SentEmails.TryAdd(utcNow, existingEmail); + + using HttpClient httpClient = _testContext.Factory.CreateDefaultClient(_logHttpMessageHandler); + var apiClient = new MixedControllersClient(httpClient); + + DateTimeOffset sinceUtc = utcNow.AddHours(-2); + + // Act + ICollection response = await apiClient.GetSentSinceAsync(sinceUtc); + + // Assert + response.Should().HaveCount(1); + response.ElementAt(0).Subject.Should().Be(existingEmail.Subject); + response.ElementAt(0).Body.Should().Be(existingEmail.Body); + response.ElementAt(0).From.Should().Be(existingEmail.From); + response.ElementAt(0).To.Should().Be(existingEmail.To); + response.ElementAt(0).SentAtUtc.Should().Be(existingEmail.SentAtUtc); + } + + [Fact] + public async Task Cannot_get_sent_emails_in_future() + { + // Arrange + var timeProvider = _testContext.Factory.Services.GetRequiredService(); + + using HttpClient httpClient = _testContext.Factory.CreateDefaultClient(_logHttpMessageHandler); + var apiClient = new MixedControllersClient(httpClient); + + DateTimeOffset sinceUtc = timeProvider.GetUtcNow().AddHours(1); + + // Act + Func action = async () => await apiClient.GetSentSinceAsync(sinceUtc); + + // Assert + ApiException exception = (await action.Should().ThrowExactlyAsync>()).Which; + + exception.StatusCode.Should().Be((int)HttpStatusCode.BadRequest); + exception.Message.Should().Be("HTTP 400: Bad Request"); + exception.Result.Status.Should().Be((int)HttpStatusCode.BadRequest); + exception.Result.Type.Should().Be("https://tools.ietf.org/html/rfc9110#section-15.5.1"); + exception.Result.Title.Should().Be("One or more validation errors occurred."); + exception.Result.Detail.Should().BeNull(); + exception.Result.Instance.Should().BeNull(); + + IDictionary> errors = exception.Result.Errors.Should().HaveCount(1).And.Subject; + errors.Should().ContainKey("sinceUtc").WhoseValue.Should().ContainSingle().Which.Should().Be("The sinceUtc parameter must be in the past."); + } + + [Fact] + public async Task Can_try_get_sent_emails_in_future() + { + // Arrange + var timeProvider = _testContext.Factory.Services.GetRequiredService(); + + using HttpClient httpClient = _testContext.Factory.CreateDefaultClient(_logHttpMessageHandler); + var apiClient = new MixedControllersClient(httpClient); + + DateTimeOffset sinceUtc = timeProvider.GetUtcNow().AddHours(1); + + // Act + Func action = async () => await apiClient.TryGetSentSinceAsync(sinceUtc); + + // Assert + ApiException exception = (await action.Should().ThrowExactlyAsync()).Which; + + exception.StatusCode.Should().Be((int)HttpStatusCode.BadRequest); + exception.Message.Should().Be("HTTP 400: Bad Request"); + exception.Response.Should().BeNull(); + } + + public void Dispose() + { + _logHttpMessageHandler.Dispose(); + } +} diff --git a/test/OpenApiNSwagEndToEndTests/OpenApiNSwagEndToEndTests.csproj b/test/OpenApiNSwagEndToEndTests/OpenApiNSwagEndToEndTests.csproj index 2fc34289fa..74bde91b8e 100644 --- a/test/OpenApiNSwagEndToEndTests/OpenApiNSwagEndToEndTests.csproj +++ b/test/OpenApiNSwagEndToEndTests/OpenApiNSwagEndToEndTests.csproj @@ -65,6 +65,12 @@ %(Name)Client %(ClassName).cs + + MixedControllers + $(MSBuildProjectName).%(Name).GeneratedCode + %(Name)Client + %(ClassName).cs + AtomicOperations $(MSBuildProjectName).%(Name).GeneratedCode diff --git a/test/OpenApiNSwagEndToEndTests/RestrictedControllers/MediaTypeTests.cs b/test/OpenApiNSwagEndToEndTests/RestrictedControllers/MediaTypeTests.cs new file mode 100644 index 0000000000..211c498466 --- /dev/null +++ b/test/OpenApiNSwagEndToEndTests/RestrictedControllers/MediaTypeTests.cs @@ -0,0 +1,115 @@ +using System.Net; +using FluentAssertions; +using JsonApiDotNetCore.Middleware; +using JsonApiDotNetCore.Serialization.Objects; +using Microsoft.EntityFrameworkCore; +using OpenApiTests; +using OpenApiTests.RestrictedControllers; +using TestBuildingBlocks; +using Xunit; + +namespace OpenApiNSwagEndToEndTests.RestrictedControllers; + +public sealed class MediaTypeTests : IClassFixture, RestrictionDbContext>> +{ + private readonly IntegrationTestContext, RestrictionDbContext> _testContext; + private readonly RestrictionFakers _fakers = new(); + + public MediaTypeTests(IntegrationTestContext, RestrictionDbContext> testContext) + { + _testContext = testContext; + + testContext.UseController(); + } + + [Fact] + public async Task Can_create_resource_with_default_media_type() + { + // Arrange + DataStream existingVideoStream = _fakers.DataStream.GenerateOne(); + DataStream existingAudioStream = _fakers.DataStream.GenerateOne(); + WriteOnlyChannel newChannel = _fakers.WriteOnlyChannel.GenerateOne(); + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + dbContext.DataStreams.AddRange(existingVideoStream, existingAudioStream); + await dbContext.SaveChangesAsync(); + }); + + var requestBody = new + { + data = new + { + type = "writeOnlyChannels", + attributes = new + { + name = newChannel.Name, + isAdultOnly = newChannel.IsAdultOnly + }, + relationships = new + { + videoStream = new + { + data = new + { + type = "dataStreams", + id = existingVideoStream.StringId + } + }, + audioStreams = new + { + data = new[] + { + new + { + type = "dataStreams", + id = existingAudioStream.StringId + } + } + } + } + } + }; + + const string route = "/writeOnlyChannels"; + + // Act + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + + // Assert + httpResponse.ShouldHaveStatusCode(HttpStatusCode.Created); + + httpResponse.Content.Headers.ContentType.Should().NotBeNull(); + httpResponse.Content.Headers.ContentType.ToString().Should().Be(JsonApiMediaType.Default.ToString()); + + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Attributes.Should().NotBeEmpty(); + responseDocument.Data.SingleValue.Relationships.Should().NotBeEmpty(); + + long newChannelId = long.Parse(responseDocument.Data.SingleValue.Id.Should().NotBeNull().And.Subject); + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + // @formatter:wrap_chained_method_calls chop_always + // @formatter:keep_existing_linebreaks true + + WriteOnlyChannel channelInDatabase = await dbContext.WriteOnlyChannels + .Include(channel => channel.VideoStream) + .Include(channel => channel.AudioStreams) + .FirstWithIdAsync(newChannelId); + + // @formatter:keep_existing_linebreaks restore + // @formatter:wrap_chained_method_calls restore + + channelInDatabase.Name.Should().Be(newChannel.Name); + channelInDatabase.IsCommercial.Should().BeNull(); + channelInDatabase.IsAdultOnly.Should().Be(newChannel.IsAdultOnly); + + channelInDatabase.VideoStream.Should().NotBeNull(); + channelInDatabase.VideoStream.Id.Should().Be(existingVideoStream.Id); + + channelInDatabase.AudioStreams.Should().HaveCount(1); + channelInDatabase.AudioStreams.ElementAt(0).Id.Should().Be(existingAudioStream.Id); + }); + } +} diff --git a/test/OpenApiTests/Documentation/DocumentationStartup.cs b/test/OpenApiTests/Documentation/DocumentationStartup.cs index 318343f24e..2e6aaa78cd 100644 --- a/test/OpenApiTests/Documentation/DocumentationStartup.cs +++ b/test/OpenApiTests/Documentation/DocumentationStartup.cs @@ -11,15 +11,17 @@ namespace OpenApiTests.Documentation; public sealed class DocumentationStartup : OpenApiStartup where TDbContext : TestableDbContext { - protected override void SetJsonApiOptions(JsonApiOptions options) + protected override void ConfigureJsonApiOptions(JsonApiOptions options) { - base.SetJsonApiOptions(options); + base.ConfigureJsonApiOptions(options); options.ClientIdGeneration = ClientIdGenerationMode.Allowed; } - protected override void SetupSwaggerGenAction(SwaggerGenOptions options) + protected override void ConfigureSwaggerGenOptions(SwaggerGenOptions options) { + base.ConfigureSwaggerGenOptions(options); + options.SwaggerDoc("v1", new OpenApiInfo { Version = "v1", @@ -36,7 +38,5 @@ protected override void SetupSwaggerGenAction(SwaggerGenOptions options) Url = new Uri("https://licenses.nuget.org/MIT") } }); - - base.SetupSwaggerGenAction(options); } } diff --git a/test/OpenApiTests/LegacyOpenApi/LegacyStartup.cs b/test/OpenApiTests/LegacyOpenApi/LegacyStartup.cs index 0888be0e1c..4498a579cc 100644 --- a/test/OpenApiTests/LegacyOpenApi/LegacyStartup.cs +++ b/test/OpenApiTests/LegacyOpenApi/LegacyStartup.cs @@ -11,9 +11,9 @@ namespace OpenApiTests.LegacyOpenApi; public sealed class LegacyStartup : OpenApiStartup 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.DefaultAttrCapabilities = AttrCapabilities.AllowView; diff --git a/test/OpenApiTests/MixedControllers/CoffeeDbContext.cs b/test/OpenApiTests/MixedControllers/CoffeeDbContext.cs new file mode 100644 index 0000000000..e4ee84cc8f --- /dev/null +++ b/test/OpenApiTests/MixedControllers/CoffeeDbContext.cs @@ -0,0 +1,12 @@ +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore; +using TestBuildingBlocks; + +namespace OpenApiTests.MixedControllers; + +[UsedImplicitly(ImplicitUseTargetFlags.Members)] +public sealed class CoffeeDbContext(DbContextOptions options) + : TestableDbContext(options) +{ + public DbSet CupsOfCoffee => Set(); +} diff --git a/test/OpenApiTests/MixedControllers/CoffeeSummary.cs b/test/OpenApiTests/MixedControllers/CoffeeSummary.cs new file mode 100644 index 0000000000..92e56afdf7 --- /dev/null +++ b/test/OpenApiTests/MixedControllers/CoffeeSummary.cs @@ -0,0 +1,26 @@ +using JetBrains.Annotations; +using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace OpenApiTests.MixedControllers; + +[UsedImplicitly(ImplicitUseTargetFlags.Members)] +[Resource(ControllerNamespace = "OpenApiTests.MixedControllers", GenerateControllerEndpoints = JsonApiEndpoints.None)] +public sealed class CoffeeSummary : Identifiable +{ + [Attr] + public int TotalCount { get; set; } + + [Attr] + public int BlackCount { get; set; } + + [Attr] + public int OnlySugarCount { get; set; } + + [Attr] + public int OnlyMilkCount { get; set; } + + [Attr] + public int SugarWithMilkCount { get; set; } +} diff --git a/test/OpenApiTests/MixedControllers/CoffeeSummaryController.cs b/test/OpenApiTests/MixedControllers/CoffeeSummaryController.cs new file mode 100644 index 0000000000..84f552800e --- /dev/null +++ b/test/OpenApiTests/MixedControllers/CoffeeSummaryController.cs @@ -0,0 +1,85 @@ +using System.Net; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Errors; +using JsonApiDotNetCore.Serialization.Objects; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +namespace OpenApiTests.MixedControllers; + +public sealed class CoffeeSummaryController : BaseJsonApiController +{ + private readonly CoffeeDbContext _dbContext; + + public CoffeeSummaryController(CoffeeDbContext dbContext, IJsonApiOptions options, IResourceGraph resourceGraph, ILoggerFactory loggerFactory) + : base(options, resourceGraph, loggerFactory, null, null) + { + ArgumentNullException.ThrowIfNull(dbContext); + + _dbContext = dbContext; + } + + [HttpGet("summary", Name = "get-coffee-summary")] + [HttpHead("summary", Name = "head-coffee-summary")] + [EndpointDescription("Summarizes all cups of coffee, indicating their ingredients.")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task GetSummaryAsync(CancellationToken cancellationToken) + { + var summary = new CoffeeSummary + { + Id = 1 + }; + + foreach (CupOfCoffee cupOfCoffee in await _dbContext.CupsOfCoffee.ToArrayAsync(cancellationToken)) + { + bool hasSugar = cupOfCoffee.HasSugar.GetValueOrDefault(); + bool hasMilk = cupOfCoffee.HasMilk.GetValueOrDefault(); + + switch (hasSugar, hasMilk) + { + case (false, false): + { + summary.BlackCount++; + break; + } + case (false, true): + { + summary.OnlyMilkCount++; + break; + } + case (true, false): + { + summary.OnlySugarCount++; + break; + } + case (true, true): + { + summary.SugarWithMilkCount++; + break; + } + } + + summary.TotalCount++; + } + + return summary; + } + + [HttpDelete("only-milk", Name = "delete-only-milk")] + [EndpointDescription("Deletes all cups of coffee with milk, but no sugar.")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task DeleteOnlyMilkAsync(CancellationToken cancellationToken) + { + int numDeleted = await _dbContext.CupsOfCoffee.Where(cupOfCoffee => cupOfCoffee.HasMilk == true && cupOfCoffee.HasSugar != true) + .ExecuteDeleteAsync(cancellationToken); + + if (numDeleted == 0) + { + throw new JsonApiException(new ErrorObject(HttpStatusCode.NotFound)); + } + } +} diff --git a/test/OpenApiTests/MixedControllers/CupOfCoffee.cs b/test/OpenApiTests/MixedControllers/CupOfCoffee.cs new file mode 100644 index 0000000000..ec16388f67 --- /dev/null +++ b/test/OpenApiTests/MixedControllers/CupOfCoffee.cs @@ -0,0 +1,20 @@ +using System.ComponentModel.DataAnnotations; +using JetBrains.Annotations; +using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace OpenApiTests.MixedControllers; + +[UsedImplicitly(ImplicitUseTargetFlags.Members)] +[Resource(ControllerNamespace = "OpenApiTests.MixedControllers", GenerateControllerEndpoints = JsonApiEndpoints.GetCollection | JsonApiEndpoints.Delete)] +public sealed class CupOfCoffee : Identifiable +{ + [Attr] + [Required] + public bool? HasSugar { get; set; } + + [Attr] + [Required] + public bool? HasMilk { get; set; } +} diff --git a/test/OpenApiTests/MixedControllers/Email.cs b/test/OpenApiTests/MixedControllers/Email.cs new file mode 100644 index 0000000000..5de104114a --- /dev/null +++ b/test/OpenApiTests/MixedControllers/Email.cs @@ -0,0 +1,41 @@ +using System.ComponentModel.DataAnnotations; +using JetBrains.Annotations; + +namespace OpenApiTests.MixedControllers; + +[UsedImplicitly(ImplicitUseTargetFlags.Members)] +public sealed record Email +{ + /// + /// The email subject. + /// + [MaxLength(255)] + public required string Subject { get; set; } + + /// + /// The email body. + /// + public required string Body { get; set; } + + /// + /// The email address of the sender. + /// + [EmailAddress] + public required string From { get; set; } + + /// + /// The email address of the recipient. + /// + [EmailAddress] + public required string To { get; set; } + + /// + /// The date/time (in UTC) at which this email was sent. + /// + public DateTimeOffset SentAtUtc { get; private set; } + + public void SetSentAt(DateTimeOffset utcValue) + { + SentAtUtc = utcValue; + } +} diff --git a/test/OpenApiTests/MixedControllers/FileTransferController.cs b/test/OpenApiTests/MixedControllers/FileTransferController.cs new file mode 100644 index 0000000000..42d974836d --- /dev/null +++ b/test/OpenApiTests/MixedControllers/FileTransferController.cs @@ -0,0 +1,68 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace OpenApiTests.MixedControllers; + +[Route("fileTransfers")] +[Tags("fileTransfers")] +public sealed class FileTransferController : ControllerBase +{ + private const string BinaryContentType = "application/octet-stream"; + + private readonly InMemoryFileStorage _inMemoryFileStorage; + + public FileTransferController(InMemoryFileStorage inMemoryFileStorage) + { + ArgumentNullException.ThrowIfNull(inMemoryFileStorage); + + _inMemoryFileStorage = inMemoryFileStorage; + } + + [HttpPost(Name = "upload")] + [EndpointDescription("Uploads a file. Returns HTTP 400 if the file is empty.")] + [ProducesResponseType(StatusCodes.Status200OK, "text/plain")] + [ProducesResponseType(typeof(void), StatusCodes.Status400BadRequest)] + public async Task UploadAsync(IFormFile? file) + { + if (file?.Length > 0) + { + byte[] fileContents; + + using (var stream = new MemoryStream()) + { + await file.CopyToAsync(stream); + fileContents = stream.ToArray(); + } + + _inMemoryFileStorage.Files.AddOrUpdate(file.FileName, _ => fileContents, (_, _) => fileContents); + return Ok($"Received file with a size of {file.Length} bytes."); + } + + return BadRequest("Empty files cannot be uploaded."); + } + + [HttpGet("find", Name = "exists")] + [HttpHead("find", Name = "tryExists")] + [EndpointDescription("Returns whether the specified file is available for download.")] + [ProducesResponseType(typeof(void), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(void), StatusCodes.Status404NotFound)] + public IActionResult Exists(string fileName) + { + return _inMemoryFileStorage.Files.ContainsKey(fileName) ? Ok() : NotFound(); + } + + [HttpGet(Name = "download")] + [HttpHead(Name = "tryDownload")] + [EndpointDescription("Downloads the file with the specified name. Returns HTTP 404 if not found.")] + [ProducesResponseType(StatusCodes.Status200OK, BinaryContentType)] + [ProducesResponseType(typeof(void), StatusCodes.Status404NotFound)] + public IActionResult Download(string fileName) + { + if (_inMemoryFileStorage.Files.TryGetValue(fileName, out byte[]? fileContents)) + { + return File(fileContents, BinaryContentType); + } + + return NotFound($"The file '{fileName}' does not exist."); + } +} diff --git a/test/OpenApiTests/MixedControllers/GeneratedSwagger/swagger.g.json b/test/OpenApiTests/MixedControllers/GeneratedSwagger/swagger.g.json new file mode 100644 index 0000000000..7b8fc14830 --- /dev/null +++ b/test/OpenApiTests/MixedControllers/GeneratedSwagger/swagger.g.json @@ -0,0 +1,847 @@ +{ + "openapi": "3.0.4", + "info": { + "title": "OpenApiTests", + "version": "1.0" + }, + "servers": [ + { + "url": "http://localhost" + } + ], + "paths": { + "/cupOfCoffees": { + "get": { + "tags": [ + "cupOfCoffees" + ], + "summary": "Retrieves a collection of cupOfCoffees.", + "operationId": "getCupOfCoffeeCollection", + "parameters": [ + { + "name": "query", + "in": "query", + "description": "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.", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string", + "nullable": true + }, + "example": "" + } + }, + { + "name": "If-None-Match", + "in": "header", + "description": "A list of ETags, resulting in HTTP status 304 without a body, if one of them matches the current fingerprint.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Successfully returns the found cupOfCoffees, or an empty array if none were found.", + "headers": { + "ETag": { + "description": "A fingerprint of the HTTP response, which can be used in an If-None-Match header to only fetch changes.", + "required": true, + "schema": { + "type": "string" + } + } + }, + "content": { + "application/vnd.api+json; ext=openapi": { + "schema": { + "$ref": "#/components/schemas/cupOfCoffeeCollectionResponseDocument" + } + } + } + }, + "304": { + "description": "The fingerprint of the HTTP response matches one of the ETags from the incoming If-None-Match header.", + "headers": { + "ETag": { + "description": "A fingerprint of the HTTP response, which can be used in an If-None-Match header to only fetch changes.", + "required": true, + "schema": { + "type": "string" + } + } + } + }, + "400": { + "description": "The query string is invalid.", + "content": { + "application/vnd.api+json; ext=openapi": { + "schema": { + "$ref": "#/components/schemas/errorResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "cupOfCoffees" + ], + "summary": "Retrieves a collection of cupOfCoffees without returning them.", + "description": "Compare the returned ETag HTTP header with an earlier one to determine if the response has changed since it was fetched.", + "operationId": "headCupOfCoffeeCollection", + "parameters": [ + { + "name": "query", + "in": "query", + "description": "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.", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string", + "nullable": true + }, + "example": "" + } + }, + { + "name": "If-None-Match", + "in": "header", + "description": "A list of ETags, resulting in HTTP status 304 without a body, if one of them matches the current fingerprint.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "The operation completed successfully.", + "headers": { + "ETag": { + "description": "A fingerprint of the HTTP response, which can be used in an If-None-Match header to only fetch changes.", + "required": true, + "schema": { + "type": "string" + } + }, + "Content-Length": { + "description": "Size of the HTTP response body, in bytes.", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + } + }, + "304": { + "description": "The fingerprint of the HTTP response matches one of the ETags from the incoming If-None-Match header.", + "headers": { + "ETag": { + "description": "A fingerprint of the HTTP response, which can be used in an If-None-Match header to only fetch changes.", + "required": true, + "schema": { + "type": "string" + } + } + } + }, + "400": { + "description": "The query string is invalid." + } + } + } + }, + "/cupOfCoffees/{id}": { + "delete": { + "tags": [ + "cupOfCoffees" + ], + "summary": "Deletes an existing cupOfCoffee by its identifier.", + "operationId": "deleteCupOfCoffee", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "The identifier of the cupOfCoffee to delete.", + "required": true, + "schema": { + "minLength": 1, + "type": "string", + "format": "int64" + } + } + ], + "responses": { + "204": { + "description": "The cupOfCoffee was successfully deleted." + }, + "404": { + "description": "The cupOfCoffee does not exist.", + "content": { + "application/vnd.api+json; ext=openapi": { + "schema": { + "$ref": "#/components/schemas/errorResponseDocument" + } + } + } + } + } + } + }, + "/emails/send": { + "post": { + "tags": [ + "emails" + ], + "description": "Sends an email to the specified recipient.", + "operationId": "sendEmail", + "requestBody": { + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/email" + } + ], + "description": "The email to send." + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK" + }, + "400": { + "description": "Bad Request", + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/httpValidationProblemDetails" + } + } + } + } + } + } + }, + "/emails/sent-since": { + "get": { + "tags": [ + "emails" + ], + "description": "Gets all emails sent since the specified date/time.", + "operationId": "getSentSince", + "parameters": [ + { + "name": "sinceUtc", + "in": "query", + "description": "The date/time (in UTC) since which the email was sent.", + "required": true, + "schema": { + "type": "string", + "description": "The date/time (in UTC) since which the email was sent.", + "format": "date-time" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/email" + } + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/httpValidationProblemDetails" + } + } + } + } + } + }, + "head": { + "tags": [ + "emails" + ], + "description": "Gets all emails sent since the specified date/time.", + "operationId": "tryGetSentSince", + "parameters": [ + { + "name": "sinceUtc", + "in": "query", + "description": "The date/time (in UTC) since which the email was sent.", + "required": true, + "schema": { + "type": "string", + "description": "The date/time (in UTC) since which the email was sent.", + "format": "date-time" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "400": { + "description": "Bad Request" + } + } + } + }, + "/fileTransfers": { + "post": { + "tags": [ + "fileTransfers" + ], + "description": "Uploads a file. Returns HTTP 400 if the file is empty.", + "operationId": "upload", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "file": { + "type": "string", + "format": "binary" + } + } + }, + "encoding": { + "file": { + "style": "form" + } + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + }, + "400": { + "description": "Bad Request" + } + } + }, + "get": { + "tags": [ + "fileTransfers" + ], + "description": "Downloads the file with the specified name. Returns HTTP 404 if not found.", + "operationId": "download", + "parameters": [ + { + "name": "fileName", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/octet-stream": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "404": { + "description": "Not Found" + } + } + }, + "head": { + "tags": [ + "fileTransfers" + ], + "description": "Downloads the file with the specified name. Returns HTTP 404 if not found.", + "operationId": "tryDownload", + "parameters": [ + { + "name": "fileName", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "404": { + "description": "Not Found" + } + } + } + }, + "/fileTransfers/find": { + "get": { + "tags": [ + "fileTransfers" + ], + "description": "Returns whether the specified file is available for download.", + "operationId": "exists", + "parameters": [ + { + "name": "fileName", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "404": { + "description": "Not Found" + } + } + }, + "head": { + "tags": [ + "fileTransfers" + ], + "description": "Returns whether the specified file is available for download.", + "operationId": "tryExists", + "parameters": [ + { + "name": "fileName", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "404": { + "description": "Not Found" + } + } + } + } + }, + "components": { + "schemas": { + "attributesInCupOfCoffeeResponse": { + "allOf": [ + { + "$ref": "#/components/schemas/attributesInResponse" + }, + { + "type": "object", + "properties": { + "hasSugar": { + "type": "boolean" + }, + "hasMilk": { + "type": "boolean" + } + }, + "additionalProperties": false + } + ], + "additionalProperties": false + }, + "attributesInResponse": { + "required": [ + "openapi:discriminator" + ], + "type": "object", + "properties": { + "openapi:discriminator": { + "allOf": [ + { + "$ref": "#/components/schemas/resourceType" + } + ] + } + }, + "additionalProperties": false, + "discriminator": { + "propertyName": "openapi:discriminator", + "mapping": { + "cupOfCoffees": "#/components/schemas/attributesInCupOfCoffeeResponse" + } + }, + "x-abstract": true + }, + "cupOfCoffeeCollectionResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/resourceCollectionTopLevelLinks" + } + ] + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/dataInCupOfCoffeeResponse" + } + }, + "included": { + "type": "array", + "items": { + "$ref": "#/components/schemas/resourceInResponse" + } + }, + "meta": { + "allOf": [ + { + "$ref": "#/components/schemas/meta" + } + ] + } + }, + "additionalProperties": false + }, + "dataInCupOfCoffeeResponse": { + "allOf": [ + { + "$ref": "#/components/schemas/resourceInResponse" + }, + { + "required": [ + "id" + ], + "type": "object", + "properties": { + "id": { + "minLength": 1, + "type": "string", + "format": "int64" + }, + "attributes": { + "allOf": [ + { + "$ref": "#/components/schemas/attributesInCupOfCoffeeResponse" + } + ] + }, + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/resourceLinks" + } + ] + } + }, + "additionalProperties": false + } + ], + "additionalProperties": false + }, + "email": { + "required": [ + "body", + "from", + "subject", + "to" + ], + "type": "object", + "properties": { + "subject": { + "maxLength": 255, + "type": "string", + "description": "The email subject." + }, + "body": { + "type": "string", + "description": "The email body." + }, + "from": { + "type": "string", + "description": "The email address of the sender.", + "format": "email" + }, + "to": { + "type": "string", + "description": "The email address of the recipient.", + "format": "email" + }, + "sentAtUtc": { + "type": "string", + "description": "The date/time (in UTC) at which this email was sent.", + "format": "date-time", + "readOnly": true + } + }, + "additionalProperties": false + }, + "errorLinks": { + "type": "object", + "properties": { + "about": { + "type": "string", + "nullable": true + }, + "type": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "errorObject": { + "type": "object", + "properties": { + "id": { + "type": "string", + "nullable": true + }, + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/errorLinks" + } + ], + "nullable": true + }, + "status": { + "type": "string" + }, + "code": { + "type": "string", + "nullable": true + }, + "title": { + "type": "string", + "nullable": true + }, + "detail": { + "type": "string", + "nullable": true + }, + "source": { + "allOf": [ + { + "$ref": "#/components/schemas/errorSource" + } + ], + "nullable": true + }, + "meta": { + "allOf": [ + { + "$ref": "#/components/schemas/meta" + } + ] + } + }, + "additionalProperties": false + }, + "errorResponseDocument": { + "required": [ + "errors", + "links" + ], + "type": "object", + "properties": { + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/errorTopLevelLinks" + } + ] + }, + "errors": { + "type": "array", + "items": { + "$ref": "#/components/schemas/errorObject" + } + }, + "meta": { + "allOf": [ + { + "$ref": "#/components/schemas/meta" + } + ] + } + }, + "additionalProperties": false + }, + "errorSource": { + "type": "object", + "properties": { + "pointer": { + "type": "string", + "nullable": true + }, + "parameter": { + "type": "string", + "nullable": true + }, + "header": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "errorTopLevelLinks": { + "type": "object", + "properties": { + "self": { + "type": "string" + }, + "describedby": { + "type": "string" + } + }, + "additionalProperties": false + }, + "httpValidationProblemDetails": { + "type": "object", + "properties": { + "type": { + "type": "string", + "nullable": true + }, + "title": { + "type": "string", + "nullable": true + }, + "status": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "detail": { + "type": "string", + "nullable": true + }, + "instance": { + "type": "string", + "nullable": true + }, + "errors": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "additionalProperties": { } + }, + "meta": { + "type": "object", + "additionalProperties": { + "nullable": true + } + }, + "resourceCollectionTopLevelLinks": { + "type": "object", + "properties": { + "self": { + "type": "string" + }, + "describedby": { + "type": "string" + }, + "first": { + "type": "string" + }, + "last": { + "type": "string" + }, + "prev": { + "type": "string" + }, + "next": { + "type": "string" + } + }, + "additionalProperties": false + }, + "resourceInResponse": { + "required": [ + "type" + ], + "type": "object", + "properties": { + "type": { + "allOf": [ + { + "$ref": "#/components/schemas/resourceType" + } + ] + }, + "meta": { + "allOf": [ + { + "$ref": "#/components/schemas/meta" + } + ] + } + }, + "additionalProperties": false, + "discriminator": { + "propertyName": "type", + "mapping": { + "cupOfCoffees": "#/components/schemas/dataInCupOfCoffeeResponse" + } + }, + "x-abstract": true + }, + "resourceLinks": { + "type": "object", + "properties": { + "self": { + "type": "string" + } + }, + "additionalProperties": false + }, + "resourceType": { + "enum": [ + "cupOfCoffees" + ], + "type": "string" + } + } + } +} \ No newline at end of file diff --git a/test/OpenApiTests/MixedControllers/InMemoryFileStorage.cs b/test/OpenApiTests/MixedControllers/InMemoryFileStorage.cs new file mode 100644 index 0000000000..d0c6a59e7e --- /dev/null +++ b/test/OpenApiTests/MixedControllers/InMemoryFileStorage.cs @@ -0,0 +1,8 @@ +using System.Collections.Concurrent; + +namespace OpenApiTests.MixedControllers; + +public sealed class InMemoryFileStorage +{ + public ConcurrentDictionary Files { get; } = new(); +} diff --git a/test/OpenApiTests/MixedControllers/InMemoryOutgoingEmailsProvider.cs b/test/OpenApiTests/MixedControllers/InMemoryOutgoingEmailsProvider.cs new file mode 100644 index 0000000000..6a04b6c760 --- /dev/null +++ b/test/OpenApiTests/MixedControllers/InMemoryOutgoingEmailsProvider.cs @@ -0,0 +1,8 @@ +using System.Collections.Concurrent; + +namespace OpenApiTests.MixedControllers; + +public sealed class InMemoryOutgoingEmailsProvider +{ + public ConcurrentDictionary SentEmails { get; } = new(); +} diff --git a/test/OpenApiTests/MixedControllers/LoggingTests.cs b/test/OpenApiTests/MixedControllers/LoggingTests.cs new file mode 100644 index 0000000000..6fd4452536 --- /dev/null +++ b/test/OpenApiTests/MixedControllers/LoggingTests.cs @@ -0,0 +1,51 @@ +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using TestBuildingBlocks; +using Xunit; +using Xunit.Abstractions; + +namespace OpenApiTests.MixedControllers; + +public sealed class LoggingTests : IClassFixture> +{ + private readonly OpenApiTestContext _testContext; + + public LoggingTests(OpenApiTestContext testContext, ITestOutputHelper testOutputHelper) + { + _testContext = testContext; + + testContext.UseController(); + + testContext.SetTestOutputHelper(testOutputHelper); + + testContext.ConfigureServices(services => services.AddLogging(builder => + { + var loggerProvider = new CapturingLoggerProvider(LogLevel.Warning); + builder.AddProvider(loggerProvider); + builder.SetMinimumLevel(LogLevel.Warning); + + builder.Services.AddSingleton(loggerProvider); + })); + } + + [Fact] + public async Task Logs_warning_for_unsupported_custom_actions_in_JsonApi_controllers() + { + // Arrange + var loggerProvider = _testContext.Factory.Services.GetRequiredService(); + + // Act + await _testContext.GetSwaggerDocumentAsync(); + + // Assert + IReadOnlyList logLines = loggerProvider.GetLines(); + + logLines.Should().BeEquivalentTo(new[] + { + $"[WARNING] Hiding unsupported custom JSON:API action method [GET] {typeof(CoffeeSummaryController)}.GetSummaryAsync (OpenApiTests) in OpenAPI.", + $"[WARNING] Hiding unsupported custom JSON:API action method [HEAD] {typeof(CoffeeSummaryController)}.GetSummaryAsync (OpenApiTests) in OpenAPI.", + $"[WARNING] Hiding unsupported custom JSON:API action method [DELETE] {typeof(CoffeeSummaryController)}.DeleteOnlyMilkAsync (OpenApiTests) in OpenAPI." + }, options => options.WithStrictOrdering()); + } +} diff --git a/test/OpenApiTests/MixedControllers/MinimalApiStartupFilter.cs b/test/OpenApiTests/MixedControllers/MinimalApiStartupFilter.cs new file mode 100644 index 0000000000..5efa6e20c7 --- /dev/null +++ b/test/OpenApiTests/MixedControllers/MinimalApiStartupFilter.cs @@ -0,0 +1,117 @@ +using System.ComponentModel; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.AspNetCore.Mvc; +using MiniValidation; + +#pragma warning disable format + +namespace OpenApiTests.MixedControllers; + +public sealed class MinimalApiStartupFilter : IStartupFilter +{ + private readonly InMemoryOutgoingEmailsProvider _emailsProvider; + + public MinimalApiStartupFilter(InMemoryOutgoingEmailsProvider emailsProvider) + { + ArgumentNullException.ThrowIfNull(emailsProvider); + + _emailsProvider = emailsProvider; + } + + public Action Configure(Action next) + { + return app => + { + app.UseRouting(); + + app.UseEndpoints(builder => + { + builder.MapPost("/emails/send", HandleSendAsync) + // @formatter:wrap_chained_method_calls chop_always + .WithTags("emails") + .WithName("sendEmail") + .WithDescription("Sends an email to the specified recipient.") + // @formatter:wrap_chained_method_calls restore + ; + + builder.MapGet("/emails/sent-since", HandleSentSinceAsync) + // @formatter:wrap_chained_method_calls chop_always + .WithTags("emails") + .WithName("getSentSince") + .WithDescription("Gets all emails sent since the specified date/time.") + // @formatter:wrap_chained_method_calls restore + ; + + builder.MapMethods("/emails/sent-since", ["HEAD"], TryHandleSentSinceAsync) + // @formatter:wrap_chained_method_calls chop_always + .WithTags("emails") + .WithName("tryGetSentSince") + .WithDescription("Gets all emails sent since the specified date/time.") + // @formatter:wrap_chained_method_calls restore + ; + }); + + next.Invoke(app); + }; + } + + private async Task> HandleSendAsync( + // Handles POST request. + [FromBody] [Description("The email to send.")] + Email email, TimeProvider timeProvider, CancellationToken cancellationToken) + { + if (!MiniValidator.TryValidate(email, out IDictionary errors)) + { + return TypedResults.ValidationProblem(errors); + } + + await Task.Yield(); + cancellationToken.ThrowIfCancellationRequested(); + + DateTimeOffset utcNow = timeProvider.GetUtcNow(); + email.SetSentAt(utcNow); + _emailsProvider.SentEmails.AddOrUpdate(utcNow, _ => email, (_, _) => email); + + return TypedResults.Ok(); + } + + private async Task>, ValidationProblem>> HandleSentSinceAsync( + // Handles GET request. + [FromQuery] [Description("The date/time (in UTC) since which the email was sent.")] + DateTimeOffset sinceUtc, TimeProvider timeProvider, CancellationToken cancellationToken) + { + if (sinceUtc > timeProvider.GetUtcNow()) + { + return TypedResults.ValidationProblem(new Dictionary + { + ["sinceUtc"] = ["The sinceUtc parameter must be in the past."] + }); + } + + await Task.Yield(); + cancellationToken.ThrowIfCancellationRequested(); + + List emails = _emailsProvider.SentEmails.Values.Where(email => email.SentAtUtc >= sinceUtc).ToList(); + + return TypedResults.Ok(emails); + } + + private async Task> TryHandleSentSinceAsync( + // Handles HEAD request. + [FromQuery] [Description("The date/time (in UTC) since which the email was sent.")] + DateTimeOffset sinceUtc, TimeProvider timeProvider, CancellationToken cancellationToken) + { + if (sinceUtc > timeProvider.GetUtcNow()) + { + return TypedResults.BadRequest(); + } + + await Task.Yield(); + cancellationToken.ThrowIfCancellationRequested(); + + return TypedResults.Ok(); + } +} diff --git a/test/OpenApiTests/MixedControllers/MixedControllerFakers.cs b/test/OpenApiTests/MixedControllers/MixedControllerFakers.cs new file mode 100644 index 0000000000..08168422b9 --- /dev/null +++ b/test/OpenApiTests/MixedControllers/MixedControllerFakers.cs @@ -0,0 +1,27 @@ +using Bogus; +using JetBrains.Annotations; +using TestBuildingBlocks; + +// @formatter:wrap_chained_method_calls chop_if_long +// @formatter:wrap_before_first_method_call true + +namespace OpenApiTests.MixedControllers; + +[UsedImplicitly(ImplicitUseTargetFlags.Members)] +public sealed class MixedControllerFakers +{ + private readonly Lazy> _lazyCupOfCoffeeFaker = new(() => new Faker() + .MakeDeterministic() + .RuleFor(cupOfCoffee => cupOfCoffee.HasSugar, faker => faker.Random.Bool()) + .RuleFor(cupOfCoffee => cupOfCoffee.HasMilk, faker => faker.Random.Bool())); + + private readonly Lazy> _lazyEmailFaker = new(() => new Faker() + .MakeDeterministic() + .RuleFor(email => email.Subject, faker => faker.Lorem.Sentence()) + .RuleFor(email => email.Body, faker => faker.Lorem.Paragraphs()) + .RuleFor(email => email.From, faker => faker.Internet.Email()) + .RuleFor(email => email.To, faker => faker.Internet.Email())); + + public Faker CupOfCoffee => _lazyCupOfCoffeeFaker.Value; + public Faker Email => _lazyEmailFaker.Value; +} diff --git a/test/OpenApiTests/MixedControllers/MixedControllerStartup.cs b/test/OpenApiTests/MixedControllers/MixedControllerStartup.cs new file mode 100644 index 0000000000..b0aab9f089 --- /dev/null +++ b/test/OpenApiTests/MixedControllers/MixedControllerStartup.cs @@ -0,0 +1,14 @@ +using JetBrains.Annotations; +using JsonApiDotNetCore.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace OpenApiTests.MixedControllers; + +[UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] +public sealed class MixedControllerStartup : OpenApiStartup +{ + protected override void AddJsonApi(IServiceCollection services) + { + services.AddJsonApi(ConfigureJsonApiOptions, resources: builder => builder.Add()); + } +} diff --git a/test/OpenApiTests/MixedControllers/MixedControllerTests.cs b/test/OpenApiTests/MixedControllers/MixedControllerTests.cs new file mode 100644 index 0000000000..c15552b380 --- /dev/null +++ b/test/OpenApiTests/MixedControllers/MixedControllerTests.cs @@ -0,0 +1,230 @@ +using System.Text.Json; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using TestBuildingBlocks; +using Xunit; +using Xunit.Abstractions; + +namespace OpenApiTests.MixedControllers; + +public sealed class MixedControllerTests : IClassFixture> +{ + private readonly OpenApiTestContext _testContext; + + public MixedControllerTests(OpenApiTestContext testContext, ITestOutputHelper testOutputHelper) + { + _testContext = testContext; + + testContext.UseController(); + testContext.UseController(); + testContext.UseController(); + + testContext.SetTestOutputHelper(testOutputHelper); + testContext.SwaggerDocumentOutputDirectory = $"{GetType().Namespace!.Replace('.', '/')}/GeneratedSwagger"; + + testContext.ConfigureServices(services => + { + services.AddSingleton(); + services.AddSingleton(); + services.TryAddEnumerable(ServiceDescriptor.Transient()); + }); + } + + [Fact] + public async Task Default_JsonApi_endpoints_are_exposed() + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.Should().ContainPath("paths./cupOfCoffees.get"); + document.Should().ContainPath("paths./cupOfCoffees.head"); + document.Should().ContainPath("paths./cupOfCoffees/{id}.delete"); + } + + [Fact] + public async Task Upload_endpoint_is_exposed() + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.Should().ContainPath("paths./fileTransfers.post").Should().BeJson(""" + { + "tags": [ + "fileTransfers" + ], + "description": "Uploads a file. Returns HTTP 400 if the file is empty.", + "operationId": "upload", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "file": { + "type": "string", + "format": "binary" + } + } + }, + "encoding": { + "file": { + "style": "form" + } + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + }, + "400": { + "description": "Bad Request" + } + } + } + """); + } + + [Fact] + public async Task Exists_endpoint_is_exposed() + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.Should().ContainPath("paths./fileTransfers/find.get").Should().BeJson(""" + { + "tags": [ + "fileTransfers" + ], + "description": "Returns whether the specified file is available for download.", + "operationId": "exists", + "parameters": [ + { + "name": "fileName", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "404": { + "description": "Not Found" + } + } + } + """); + + document.Should().ContainPath("paths./fileTransfers/find.head").Should().BeJson(""" + { + "tags": [ + "fileTransfers" + ], + "description": "Returns whether the specified file is available for download.", + "operationId": "tryExists", + "parameters": [ + { + "name": "fileName", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "404": { + "description": "Not Found" + } + } + } + """); + } + + [Fact] + public async Task Download_endpoint_is_exposed() + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.Should().ContainPath("paths./fileTransfers.get").Should().BeJson(""" + { + "tags": [ + "fileTransfers" + ], + "description": "Downloads the file with the specified name. Returns HTTP 404 if not found.", + "operationId": "download", + "parameters": [ + { + "name": "fileName", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/octet-stream": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "404": { + "description": "Not Found" + } + } + } + """); + + document.Should().ContainPath("paths./fileTransfers.head").Should().BeJson(""" + { + "tags": [ + "fileTransfers" + ], + "description": "Downloads the file with the specified name. Returns HTTP 404 if not found.", + "operationId": "tryDownload", + "parameters": [ + { + "name": "fileName", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "404": { + "description": "Not Found" + } + } + } + """); + } +} diff --git a/test/OpenApiTests/NamingConventions/CamelCase/CamelCaseNamingConventionStartup.cs b/test/OpenApiTests/NamingConventions/CamelCase/CamelCaseNamingConventionStartup.cs index cf910b3a53..180404dc48 100644 --- a/test/OpenApiTests/NamingConventions/CamelCase/CamelCaseNamingConventionStartup.cs +++ b/test/OpenApiTests/NamingConventions/CamelCase/CamelCaseNamingConventionStartup.cs @@ -10,9 +10,9 @@ namespace OpenApiTests.NamingConventions.CamelCase; public sealed class CamelCaseNamingConventionStartup : OpenApiStartup where TDbContext : TestableDbContext { - protected override void SetJsonApiOptions(JsonApiOptions options) + protected override void ConfigureJsonApiOptions(JsonApiOptions options) { - base.SetJsonApiOptions(options); + base.ConfigureJsonApiOptions(options); options.IncludeJsonApiVersion = true; options.SerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; diff --git a/test/OpenApiTests/NamingConventions/KebabCase/KebabCaseNamingConventionStartup.cs b/test/OpenApiTests/NamingConventions/KebabCase/KebabCaseNamingConventionStartup.cs index cacd639813..3250965218 100644 --- a/test/OpenApiTests/NamingConventions/KebabCase/KebabCaseNamingConventionStartup.cs +++ b/test/OpenApiTests/NamingConventions/KebabCase/KebabCaseNamingConventionStartup.cs @@ -9,9 +9,9 @@ namespace OpenApiTests.NamingConventions.KebabCase; public sealed class KebabCaseNamingConventionStartup : OpenApiStartup where TDbContext : TestableDbContext { - protected override void SetJsonApiOptions(JsonApiOptions options) + protected override void ConfigureJsonApiOptions(JsonApiOptions options) { - base.SetJsonApiOptions(options); + base.ConfigureJsonApiOptions(options); options.IncludeJsonApiVersion = true; options.SerializerOptions.PropertyNamingPolicy = JsonKebabCaseNamingPolicy.Instance; diff --git a/test/OpenApiTests/NamingConventions/PascalCase/PascalCaseNamingConventionStartup.cs b/test/OpenApiTests/NamingConventions/PascalCase/PascalCaseNamingConventionStartup.cs index 8a367641f2..32cff77507 100644 --- a/test/OpenApiTests/NamingConventions/PascalCase/PascalCaseNamingConventionStartup.cs +++ b/test/OpenApiTests/NamingConventions/PascalCase/PascalCaseNamingConventionStartup.cs @@ -9,9 +9,9 @@ namespace OpenApiTests.NamingConventions.PascalCase; public sealed class PascalCaseNamingConventionStartup : OpenApiStartup where TDbContext : TestableDbContext { - protected override void SetJsonApiOptions(JsonApiOptions options) + protected override void ConfigureJsonApiOptions(JsonApiOptions options) { - base.SetJsonApiOptions(options); + base.ConfigureJsonApiOptions(options); options.IncludeJsonApiVersion = true; options.SerializerOptions.PropertyNamingPolicy = null; diff --git a/test/OpenApiTests/OpenApiStartup.cs b/test/OpenApiTests/OpenApiStartup.cs index e8ee0fdb20..0e4d1b3f50 100644 --- a/test/OpenApiTests/OpenApiStartup.cs +++ b/test/OpenApiTests/OpenApiStartup.cs @@ -14,18 +14,18 @@ public class OpenApiStartup : TestableStartup public override void ConfigureServices(IServiceCollection services) { base.ConfigureServices(services); - services.AddOpenApiForJsonApi(SetupSwaggerGenAction); + services.AddOpenApiForJsonApi(ConfigureSwaggerGenOptions); } - protected override void SetJsonApiOptions(JsonApiOptions options) + protected override void ConfigureJsonApiOptions(JsonApiOptions options) { - base.SetJsonApiOptions(options); + base.ConfigureJsonApiOptions(options); options.UseRelativeLinks = true; options.IncludeTotalResourceCount = true; } - protected virtual void SetupSwaggerGenAction(SwaggerGenOptions options) + protected virtual void ConfigureSwaggerGenOptions(SwaggerGenOptions options) { string documentationPath = Path.ChangeExtension(Assembly.GetExecutingAssembly().Location, ".xml"); options.IncludeXmlComments(documentationPath); diff --git a/test/OpenApiTests/OpenApiTests.csproj b/test/OpenApiTests/OpenApiTests.csproj index ed2a75b5f4..8d9336f54a 100644 --- a/test/OpenApiTests/OpenApiTests.csproj +++ b/test/OpenApiTests/OpenApiTests.csproj @@ -24,6 +24,7 @@ + diff --git a/test/OpenApiTests/ResourceFieldValidation/MsvOffStartup.cs b/test/OpenApiTests/ResourceFieldValidation/MsvOffStartup.cs index 838803f9c4..2ee464fc49 100644 --- a/test/OpenApiTests/ResourceFieldValidation/MsvOffStartup.cs +++ b/test/OpenApiTests/ResourceFieldValidation/MsvOffStartup.cs @@ -8,9 +8,9 @@ namespace OpenApiTests.ResourceFieldValidation; public sealed class MsvOffStartup : OpenApiStartup 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/TestBuildingBlocks/TestableStartup.cs b/test/TestBuildingBlocks/TestableStartup.cs index e5dc7075e8..7a4f750477 100644 --- a/test/TestBuildingBlocks/TestableStartup.cs +++ b/test/TestBuildingBlocks/TestableStartup.cs @@ -9,12 +9,16 @@ public class TestableStartup { public virtual void ConfigureServices(IServiceCollection services) { - IMvcCoreBuilder mvcBuilder = services.AddMvcCore(options => options.MaxModelValidationErrors = 3); + AddJsonApi(services); + } - services.AddJsonApi(SetJsonApiOptions, mvcBuilder: mvcBuilder); + protected virtual void AddJsonApi(IServiceCollection services) + { + IMvcCoreBuilder mvcBuilder = services.AddMvcCore(options => options.MaxModelValidationErrors = 3); + services.AddJsonApi(ConfigureJsonApiOptions, mvcBuilder: mvcBuilder); } - protected virtual void SetJsonApiOptions(JsonApiOptions options) + protected virtual void ConfigureJsonApiOptions(JsonApiOptions options) { options.IncludeExceptionStackTraceInErrors = true; options.IncludeRequestBodyInErrors = true;